@blamejs/pki 0.9.4 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +7 -6
- package/bin/pki.js +110 -34
- package/lib/acme.js +91 -46
- package/lib/attrcert-sign.js +4 -3
- package/lib/cmp-build.js +16 -6
- package/lib/cmp-session.js +19 -3
- package/lib/cmp-verify.js +25 -2
- package/lib/cms-compress.js +3 -3
- package/lib/cms-digest.js +10 -5
- package/lib/cms-encrypt.js +6 -1
- package/lib/cms-sign.js +16 -0
- package/lib/cms-verify.js +32 -14
- package/lib/constants.js +1 -0
- package/lib/crl-sign.js +3 -3
- package/lib/csr-sign.js +6 -2
- package/lib/ct.js +16 -3
- package/lib/est.js +67 -31
- package/lib/guard-all.js +2 -0
- package/lib/guard-distrust.js +224 -0
- package/lib/guard-identifier.js +27 -1
- package/lib/guard-json.js +1 -1
- package/lib/http-transport.js +299 -36
- package/lib/jose.js +1 -1
- package/lib/lint.js +67 -7
- package/lib/ocsp-verify.js +9 -3
- package/lib/ocsp.js +16 -4
- package/lib/oid.js +4 -0
- package/lib/path-validate.js +119 -18
- package/lib/pkcs12-build.js +6 -1
- package/lib/pki-build.js +38 -6
- package/lib/possession.js +6 -2
- package/lib/related-cert.js +2 -2
- package/lib/revocation-fetch.js +46 -11
- package/lib/scep.js +39 -14
- package/lib/schema-pkix.js +3 -3
- package/lib/sigstore.js +11 -6
- package/lib/smime.js +32 -16
- package/lib/tls-cert-compress.js +6 -5
- package/lib/trust.js +121 -20
- package/lib/trustanchor-build.js +16 -1
- package/lib/tsp-sign.js +15 -2
- package/lib/webauthn-mds.js +29 -8
- package/lib/webauthn.js +46 -14
- package/lib/x509-sign.js +26 -10
- package/package.json +2 -2
- package/sbom.cdx.json +6 -6
package/lib/smime.js
CHANGED
|
@@ -63,7 +63,7 @@ var SIGN_OPTS = Object.assign(Object.create(null), {
|
|
|
63
63
|
});
|
|
64
64
|
var VERIFY_OPTS = Object.assign(Object.create(null), {
|
|
65
65
|
certs: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1, strictMicalg: 1,
|
|
66
|
-
legacyHeaderProtection: 1, expectedSender: 1, allowWeakDigests: 1,
|
|
66
|
+
legacyHeaderProtection: 1, expectedSender: 1, allowWeakDigests: 1, distrustedKeys: 1,
|
|
67
67
|
});
|
|
68
68
|
var ENCRYPT_OPTS = Object.assign(Object.create(null), {
|
|
69
69
|
entity: 1, contentType: 1, protectHeaders: 1, headers: 1, hcp: 1,
|
|
@@ -76,12 +76,23 @@ var DECOMPRESS_OPTS = Object.assign(Object.create(null), { maxOutputBytes: 1 });
|
|
|
76
76
|
/** @internal Checks the option names and RETURNS the options taken once. A verb reads one option to
|
|
77
77
|
* decide whether a rule applies and reads it again for the value the rule uses, and an option
|
|
78
78
|
* reached through an accessor answers each read separately. Callers assign the return value. */
|
|
79
|
+
/** @internal Header protection is turned on below by comparing this option to `true`, so a value
|
|
80
|
+
* of any other type sent the message with the RFC 9788 protection the caller asked for left off.
|
|
81
|
+
* Typed at the one door every verb here passes through, which is before the entity is built. */
|
|
82
|
+
function _assertSmimeSwitches(opts, verb) {
|
|
83
|
+
guard.identifier.booleanOption(opts.protectHeaders, _err, "smime/bad-input",
|
|
84
|
+
"pki.smime." + verb + " opts.protectHeaders");
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
function _knownOpts(opts, known, verb) {
|
|
80
88
|
guard.identifier.assertKnownKeys(opts, known, _err, "smime/bad-input", function (k) {
|
|
81
89
|
return "unknown option " + JSON.stringify(k) + " for pki.smime." + verb + " -- accepted: " +
|
|
82
90
|
Object.keys(known).sort().join(", ");
|
|
83
91
|
});
|
|
84
|
-
|
|
92
|
+
/** @internal Typed on the snapshot, which is the object the verb goes on to read. */
|
|
93
|
+
var settled = guard.identifier.snapshotOptions(opts, known);
|
|
94
|
+
_assertSmimeSwitches(settled, verb);
|
|
95
|
+
return settled;
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
function _isPkcs7(type, kind) {
|
|
@@ -580,9 +591,9 @@ function _hpSurface(content, outerEnt, expectedMode, authenticated, legacyEnable
|
|
|
580
591
|
|
|
581
592
|
function _cmsSignOpts(opts, detached) {
|
|
582
593
|
var o = { detached: detached };
|
|
583
|
-
if (opts.signingTime
|
|
594
|
+
if (opts.signingTime != null) o.signingTime = opts.signingTime;
|
|
584
595
|
if (opts.sid) o.sid = opts.sid;
|
|
585
|
-
if (opts.signedAttributes
|
|
596
|
+
if (opts.signedAttributes != null) o.signedAttributes = opts.signedAttributes;
|
|
586
597
|
if (opts.additionalSignedAttributes) o.additionalSignedAttributes = opts.additionalSignedAttributes;
|
|
587
598
|
return o;
|
|
588
599
|
}
|
|
@@ -654,7 +665,7 @@ function _base64Body(der) {
|
|
|
654
665
|
* @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
|
|
655
666
|
* @opts contentType the wrapped entity's Content-Type (default `text/plain; charset=utf-8`).
|
|
656
667
|
* @opts signingTime a `Date` for the CMS signing-time attribute, or false to omit it.
|
|
657
|
-
* @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`), inlining `opts.headers` on the signed payload + the outer display headers.
|
|
668
|
+
* @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`), inlining `opts.headers` on the signed payload + the outer display headers. A value other than `true`, `false`, `null` or an absent option is refused `smime/bad-input`.
|
|
658
669
|
* @opts headers the Non-Structural fields to protect + display: an object `{ Name: value }` or an array `[{ name, value }]` (Subject / From / To / Date / ...); used with `protectHeaders`.
|
|
659
670
|
* @opts hcp the Header Confidentiality Policy applied to the OUTER display copies: `"hcp_baseline"` (default) or `"hcp_no_confidentiality"`. A signed message's payload is not encrypted, so this governs presentation, not secrecy; used with `protectHeaders`.
|
|
660
671
|
* @opts sid forwarded to cms.sign: the SignerIdentifier form, `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
|
|
@@ -770,6 +781,9 @@ function _capped(msg) {
|
|
|
770
781
|
* @opts time the instant the signer's chain is judged at (default now). Only read with `trustAnchors`.
|
|
771
782
|
* @opts requiredEku key purposes the SIGNER certificate must carry. Defaults to `["emailProtection"]`.
|
|
772
783
|
* @opts checkPurpose the purpose the ANCHOR's own trust metadata must permit. Defaults to `"emailProtection"`.
|
|
784
|
+
* @opts distrustedKeys SHA-256 digests of SubjectPublicKeyInfo DER (64 hexadecimal characters or 32 bytes
|
|
785
|
+
* each, see `pki.trust.spkiDigest`), forwarded to `cms.verify` with `trustAnchors`. A
|
|
786
|
+
* listed key on the signer's chain, the anchor included, reports the signer as not trusted.
|
|
773
787
|
* @opts strictMicalg reject a `multipart/signed` whose `micalg` disagrees with the SignerInfo digest.
|
|
774
788
|
* @opts legacyHeaderProtection opt in to detecting a LEGACY RFC 8551 header-protected message (RFC 9788 sec. 4.10): a Cryptographic Payload that is a bare `message/rfc822` wrap with no `hp=` parameter. When set, a precisely-identified legacy message surfaces the inner message's headers under `headerProtection.legacy = { headers, mode, fromMismatch, confidential }`, where `headers` is an ordered `[{ name, value }]` array (retaining legally-repeated fields such as `Received`) and the mode is inferred from the envelope (`clear` here). They are not placed under `protectedHeaders`, and `present` stays `false`. Consuming `headerProtection.legacy.headers` is an explicit choice: a legacy message is structurally indistinguishable from an ordinary forwarded `message/rfc822`, so this is a heuristic (RFC 9788 sec. 4.10.2: "not based on any strong end-to-end guarantees"); cross-check `legacy.fromMismatch`. Anything not precisely identified (a nested crypto layer, an `hp=` on the inner message, a non-`message/rfc822` payload, a duplicate of a singleton field, or a duplicate Content-Type) reports `legacy: null`. Off by default. The signed-and-encrypted form (RFC 9788 Appendix C.3.17) is a documented gap (`legacy: null` at `decrypt`; surfaces as `clear` only via the caller's re-`verify` step), because the non-recursive layered API exposes no single seam holding both the inner signature verdict and the outer header section.
|
|
775
789
|
* @example
|
|
@@ -784,6 +798,7 @@ function _capped(msg) {
|
|
|
784
798
|
async function verify(message, opts) {
|
|
785
799
|
opts = opts || _create(null);
|
|
786
800
|
opts = _knownOpts(opts, VERIFY_OPTS, "verify");
|
|
801
|
+
var distrustedKeys = guard.distrust.keys(opts.distrustedKeys, _err, "smime/bad-input", "pki.smime.verify opts.distrustedKeys");
|
|
787
802
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
788
803
|
var ct = ent.contentType;
|
|
789
804
|
var vOpts = {};
|
|
@@ -792,8 +807,9 @@ async function verify(message, opts) {
|
|
|
792
807
|
vOpts.trustAnchors = opts.trustAnchors;
|
|
793
808
|
vOpts.requiredEku = opts.requiredEku != null ? opts.requiredEku : ["emailProtection"];
|
|
794
809
|
vOpts.checkPurpose = opts.checkPurpose != null ? opts.checkPurpose : "emailProtection";
|
|
810
|
+
if (distrustedKeys !== null) vOpts.distrustedKeys = distrustedKeys;
|
|
795
811
|
}
|
|
796
|
-
if (opts.time
|
|
812
|
+
if (opts.time != null) vOpts.time = opts.time;
|
|
797
813
|
if (opts.allowWeakDigests === true) vOpts.allowWeakDigests = true;
|
|
798
814
|
if (_isPkcs7(ct.type, "mime")) {
|
|
799
815
|
if (ct.params["smime-type"] && ct.params["smime-type"] !== "signed-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(ct.params["smime-type"]) + " (only signed-data)");
|
|
@@ -843,10 +859,10 @@ function _envelopedTypeOf(der, badParseCode) {
|
|
|
843
859
|
|
|
844
860
|
function _cmsEncryptOpts(opts) {
|
|
845
861
|
var o = {};
|
|
846
|
-
if (opts.contentEncryptionAlgorithm
|
|
847
|
-
if (opts.oaepHash
|
|
848
|
-
if (opts.keyIdentifier
|
|
849
|
-
if (opts.ukm
|
|
862
|
+
if (opts.contentEncryptionAlgorithm != null) o.contentEncryptionAlgorithm = opts.contentEncryptionAlgorithm;
|
|
863
|
+
if (opts.oaepHash != null) o.oaepHash = opts.oaepHash;
|
|
864
|
+
if (opts.keyIdentifier != null) o.keyIdentifier = opts.keyIdentifier;
|
|
865
|
+
if (opts.ukm != null) o.ukm = opts.ukm;
|
|
850
866
|
return o;
|
|
851
867
|
}
|
|
852
868
|
|
|
@@ -875,7 +891,7 @@ function _cmsEncryptOpts(opts) {
|
|
|
875
891
|
*
|
|
876
892
|
* @opts entity treat `content` as a complete MIME entity (default: wrap it as text/plain).
|
|
877
893
|
* @opts contentType the wrapped entity's MIME Content-Type (default `text/plain; charset=utf-8`).
|
|
878
|
-
* @opts protectHeaders enable RFC 9788 header protection (`hp="cipher"`): inline `opts.headers` inside the ciphertext, emit HCP-processed outer copies, and embed the authenticated HP-Outer records (RFC 9788 sec. 2.2) documenting which fields were left visible outside.
|
|
894
|
+
* @opts protectHeaders enable RFC 9788 header protection (`hp="cipher"`): inline `opts.headers` inside the ciphertext, emit HCP-processed outer copies, and embed the authenticated HP-Outer records (RFC 9788 sec. 2.2) documenting which fields were left visible outside. A value other than `true`, `false`, `null` or an absent option is refused `smime/bad-input`.
|
|
879
895
|
* @opts headers the Non-Structural fields to protect (object `{ Name: value }` or array `[{ name, value }]`); the real values, hidden by the HCP.
|
|
880
896
|
* @opts hcp the Header Confidentiality Policy: `"hcp_baseline"` (the default, obscuring Subject and removing Comments/Keywords) or `"hcp_no_confidentiality"` (leave all outer values visible). Per RFC 9788 sec. 3.2.1 / sec. 11.4, `hcp_baseline` deliberately does not strip `Bcc` (removing it can break deliverability to a Bcc'd recipient); to keep a blind recipient out of the plaintext outer headers, omit `Bcc` from `opts.headers`.
|
|
881
897
|
* @opts contentEncryptionAlgorithm forwarded to cms.encrypt: `"aes-256-gcm"` (default) / `"aes-128-gcm"` / `"aes-256-cbc"` / `"aes-128-cbc"`.
|
|
@@ -961,12 +977,12 @@ async function decrypt(message, keyMaterial, opts) {
|
|
|
961
977
|
var smimeType = _envelopedTypeOf(der, "smime/unsupported-type");
|
|
962
978
|
if (opts.strictSmimeType && st && stLower !== smimeType.toLowerCase()) throw _err("smime/smime-type-mismatch", "the header smime-type " + JSON.stringify(st) + " disagrees with the CMS body (" + smimeType + ")");
|
|
963
979
|
var cmsOpts = {};
|
|
964
|
-
if (opts.recipientIndex
|
|
965
|
-
if (opts.maxIterations
|
|
980
|
+
if (opts.recipientIndex != null) cmsOpts.recipientIndex = opts.recipientIndex;
|
|
981
|
+
if (opts.maxIterations != null) cmsOpts.maxIterations = opts.maxIterations;
|
|
966
982
|
/** @internal Forwarded rather than re-decided here: an S/MIME body is a CMS structure and the rule about
|
|
967
983
|
* RSAES-PKCS1-v1_5 over an unauthenticated content belongs to the verb that opens it. An
|
|
968
984
|
* `openssl smime -encrypt` or `openssl cms -encrypt` message is exactly that combination by default. */
|
|
969
|
-
if (opts.allowUnauthenticatedRsa15
|
|
985
|
+
if (opts.allowUnauthenticatedRsa15 != null) cmsOpts.allowUnauthenticatedRsa15 = opts.allowUnauthenticatedRsa15;
|
|
970
986
|
var res = await cms.decrypt(der, keyMaterial, cmsOpts);
|
|
971
987
|
return Object.assign({
|
|
972
988
|
content: res.content, smimeType: smimeType, authenticated: res.authenticated,
|
|
@@ -1003,7 +1019,7 @@ async function compress(content, opts) {
|
|
|
1003
1019
|
opts = _knownOpts(opts, COMPRESS_OPTS, "compress");
|
|
1004
1020
|
var entity = _entityBytes(content, opts);
|
|
1005
1021
|
var cOpts = {};
|
|
1006
|
-
if (opts.level
|
|
1022
|
+
if (opts.level != null) cOpts.level = opts.level;
|
|
1007
1023
|
var der = _toBuf(await cms.compress(entity, cOpts));
|
|
1008
1024
|
var head = _pkcs7MimeHead("compressed-data", "smime.p7z");
|
|
1009
1025
|
return _capped(Buffer.concat([head, _base64Body(der), mime.CRLF]));
|
|
@@ -1042,7 +1058,7 @@ async function decompress(message, opts) {
|
|
|
1042
1058
|
if (st && st.toLowerCase() !== "compressed-data") throw _err("smime/unsupported-type", "unsupported smime-type " + JSON.stringify(st) + " (only compressed-data)");
|
|
1043
1059
|
var der = _decodeCms(ent);
|
|
1044
1060
|
var dOpts = {};
|
|
1045
|
-
if (opts.maxOutputBytes
|
|
1061
|
+
if (opts.maxOutputBytes != null) dOpts.maxOutputBytes = opts.maxOutputBytes;
|
|
1046
1062
|
var res = await cms.decompress(der, dOpts);
|
|
1047
1063
|
return { content: res.content, contentType: res.contentType, contentTypeName: res.contentTypeName, compressionAlgorithm: res.compressionAlgorithm };
|
|
1048
1064
|
}
|
package/lib/tls-cert-compress.js
CHANGED
|
@@ -88,7 +88,7 @@ var HEADER_BYTES = 8;
|
|
|
88
88
|
|
|
89
89
|
function _resolveCap(opts) {
|
|
90
90
|
var cap = C.LIMITS.TLS_CERT_MSG_MAX_BYTES;
|
|
91
|
-
if (opts.maxOutputBytes
|
|
91
|
+
if (opts.maxOutputBytes != null) {
|
|
92
92
|
var mo = opts.maxOutputBytes;
|
|
93
93
|
if (typeof mo !== "number" || !isFinite(mo) || mo <= 0 || Math.floor(mo) !== mo) {
|
|
94
94
|
throw _err("tls/bad-input", "opts.maxOutputBytes must be a positive integer");
|
|
@@ -99,6 +99,7 @@ function _resolveCap(opts) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
function _resolveAllowed(opts) {
|
|
102
|
+
// allow:caller-option-presence-tested-against-undefined -- absence accepts every codec this build supports, so a null must be refused rather than widen the allowlist
|
|
102
103
|
if (opts.allowedAlgorithms === undefined) return SUPPORTED.slice();
|
|
103
104
|
var list = opts.allowedAlgorithms;
|
|
104
105
|
if (!Array.isArray(list)) throw _err("tls/bad-input", "opts.allowedAlgorithms must be an array of algorithm names or numbers");
|
|
@@ -263,7 +264,7 @@ function decompressCertificate(bytes, opts) {
|
|
|
263
264
|
*/
|
|
264
265
|
function parseCertificateMessage(bytes, opts) {
|
|
265
266
|
opts = opts || intrinsic.create(null);
|
|
266
|
-
var type = opts.certificateType
|
|
267
|
+
var type = opts.certificateType == null ? "X509" : opts.certificateType;
|
|
267
268
|
if (type !== "X509" && type !== "RawPublicKey") {
|
|
268
269
|
throw _err("tls/bad-input", "opts.certificateType must be \"X509\" or \"RawPublicKey\"");
|
|
269
270
|
}
|
|
@@ -330,7 +331,7 @@ function compressCertificate(certificateMessage, opts) {
|
|
|
330
331
|
" bytes, over the " + C.LIMITS.TLS_CERT_MSG_MAX_BYTES + "-byte handshake framing limit");
|
|
331
332
|
}
|
|
332
333
|
var view = guard.bytes.snapshot(viewed, TlsError, "tls/bad-input", "the certificate message");
|
|
333
|
-
var sel = opts.algorithm
|
|
334
|
+
var sel = opts.algorithm == null ? "zlib" : opts.algorithm;
|
|
334
335
|
var name = (typeof sel === "number") ? ALG_BY_NUMBER[sel] : sel;
|
|
335
336
|
if (typeof name !== "string" || SUPPORTED.indexOf(name) === -1) {
|
|
336
337
|
throw _err("tls/unsupported-algorithm", "opts.algorithm names an algorithm this toolkit cannot compress: " + JSON.stringify(sel));
|
|
@@ -338,12 +339,12 @@ function compressCertificate(certificateMessage, opts) {
|
|
|
338
339
|
if (view.length < MIN_CERT_MSG_BYTES) {
|
|
339
340
|
throw _err("tls/bad-input", "a Certificate message is at least " + MIN_CERT_MSG_BYTES + " bytes (RFC 8446 sec. 4.4.2)");
|
|
340
341
|
}
|
|
341
|
-
if (opts.level
|
|
342
|
+
if (opts.level != null && (typeof opts.level !== "number" || !isFinite(opts.level) || Math.floor(opts.level) !== opts.level)) {
|
|
342
343
|
throw _err("tls/bad-input", "opts.level must be an integer");
|
|
343
344
|
}
|
|
344
345
|
var stream;
|
|
345
346
|
try {
|
|
346
|
-
stream = COMPRESS[name](view, opts.level
|
|
347
|
+
stream = COMPRESS[name](view, opts.level != null ? LEVEL_OPT[name](opts.level) : undefined);
|
|
347
348
|
} catch (e) {
|
|
348
349
|
throw _err("tls/bad-input", "the certificate message could not be compressed (check opts.level)", e);
|
|
349
350
|
}
|
package/lib/trust.js
CHANGED
|
@@ -1035,6 +1035,7 @@ function anchor(entry, opts) {
|
|
|
1035
1035
|
!entry.name || !intrinsic.isArray(entry.name.rdns))) {
|
|
1036
1036
|
throw E("trust/bad-input", "anchor expects a trust-store entry ({ name, publicKey, algorithm, ... })");
|
|
1037
1037
|
}
|
|
1038
|
+
// allow:caller-option-presence-tested-against-undefined -- absence means no purpose restricts the bundle, so a null must be refused
|
|
1038
1039
|
if (opts.purpose !== undefined) {
|
|
1039
1040
|
if (_arrIndexOf(PURPOSES, opts.purpose) === -1) {
|
|
1040
1041
|
throw E("trust/bad-input", "anchor: opts.purpose must be one of " + _arrJoin(PURPOSES, " | "));
|
|
@@ -1226,7 +1227,7 @@ var _BUNDLE_PATHS = [
|
|
|
1226
1227
|
];
|
|
1227
1228
|
|
|
1228
1229
|
var _SYSTEM_ANCHOR_OPTS = intrinsic.assign(intrinsic.create(null), {
|
|
1229
|
-
path: 1, allowNodeBundle: 1, maxBytes: 1,
|
|
1230
|
+
path: 1, allowNodeBundle: 1, maxBytes: 1, distrustedKeys: 1,
|
|
1230
1231
|
});
|
|
1231
1232
|
|
|
1232
1233
|
/** @internal What an operator runs to produce a bundle this verb can read, per platform. Named in
|
|
@@ -1306,9 +1307,52 @@ function _readBundleText(chosen, maxBytes) {
|
|
|
1306
1307
|
return raw;
|
|
1307
1308
|
}
|
|
1308
1309
|
|
|
1310
|
+
/**
|
|
1311
|
+
* @primitive pki.trust.spkiDigest
|
|
1312
|
+
* @signature pki.trust.spkiDigest(cert) -> string
|
|
1313
|
+
* @since 0.9.6
|
|
1314
|
+
* @status stable
|
|
1315
|
+
* @spec RFC 5280
|
|
1316
|
+
* @related pki.path.validate, pki.trust.systemAnchors, pki.transport.https
|
|
1317
|
+
*
|
|
1318
|
+
* The SHA-256 digest of a certificate's DER SubjectPublicKeyInfo in canonical form, as 64 lower-case
|
|
1319
|
+
* hexadecimal characters. Canonical form is an uncompressed EC point with its named curve and an RSA
|
|
1320
|
+
* key with NULL parameters, which is how nearly every certificate encodes a key, so for those it is
|
|
1321
|
+
* the digest of the bytes the certificate carries and the SPKI SHA-256 root programs publish. This is
|
|
1322
|
+
* the entry `distrustedKeys` takes on `pki.path.validate`, `pki.path.build`,
|
|
1323
|
+
* `pki.trust.systemAnchors`, the TLS policy of `pki.transport.https`, and the verbs that validate a
|
|
1324
|
+
* chain on a caller's behalf. Every certificate that carries the same key has the same digest, however
|
|
1325
|
+
* it encodes the key, so one entry covers a CA certificate and each of its cross-signatures. `cert` is
|
|
1326
|
+
* DER bytes, a PEM string, or a `pki.schema.x509.parse` result. An identifier that restricts a key to
|
|
1327
|
+
* one use names the same key: id-ecDH and id-ecMQV read as ecPublicKey, and RSASSA-PSS and RSAES-OAEP
|
|
1328
|
+
* read as rsaEncryption. A key node does not read, such as a composite or stateful hash-based key, is
|
|
1329
|
+
* digested as the certificate encodes it, and a composite key is its own key, so listing one of its
|
|
1330
|
+
* component keys does not list the composite.
|
|
1331
|
+
*
|
|
1332
|
+
* @example
|
|
1333
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
1334
|
+
* var der = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
1335
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
1336
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"] } },
|
|
1337
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
1338
|
+
* pki.trust.spkiDigest(der).length; // 64
|
|
1339
|
+
*/
|
|
1340
|
+
function spkiDigest(cert) {
|
|
1341
|
+
if (cert === undefined || cert === null) throw E("trust/bad-input", "spkiDigest takes a certificate (DER, PEM, or a parsed certificate)");
|
|
1342
|
+
var spki;
|
|
1343
|
+
if (typeof cert === "string") spki = x509.parse(x509.pemDecode(cert, "CERTIFICATE")).subjectPublicKeyInfo.bytes;
|
|
1344
|
+
else if (guard.bytes.isByteSource(cert)) spki = x509.parse(guard.bytes.snapshotSource(cert, E, "trust/bad-input", "the certificate")).subjectPublicKeyInfo.bytes;
|
|
1345
|
+
else if (typeof cert === "object" && cert.subjectPublicKeyInfo && guard.bytes.isByteSource(cert.subjectPublicKeyInfo.bytes)) {
|
|
1346
|
+
spki = guard.bytes.snapshotSource(cert.subjectPublicKeyInfo.bytes, E, "trust/bad-input", "the certificate's subjectPublicKeyInfo.bytes");
|
|
1347
|
+
} else {
|
|
1348
|
+
throw E("trust/bad-input", "spkiDigest takes a certificate (DER, PEM, or a parsed certificate)");
|
|
1349
|
+
}
|
|
1350
|
+
return guard.distrust.keyDigest(spki);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1309
1353
|
/**
|
|
1310
1354
|
* @primitive pki.trust.systemAnchors
|
|
1311
|
-
* @signature pki.trust.systemAnchors(opts?) -> { anchors, source, path, skipped }
|
|
1355
|
+
* @signature pki.trust.systemAnchors(opts?) -> { anchors, intermediates, source, path, skipped }
|
|
1312
1356
|
* @since 0.8.15
|
|
1313
1357
|
* @status stable
|
|
1314
1358
|
* @spec RFC 7468 (PEM), RFC 5280
|
|
@@ -1321,16 +1365,35 @@ function _readBundleText(chosen, maxBytes) {
|
|
|
1321
1365
|
* bundle carries that is not a certificate is listed in `skipped` with the label that named it,
|
|
1322
1366
|
* rather than dropped.
|
|
1323
1367
|
*
|
|
1368
|
+
* Only a self-issued certificate becomes an anchor. A certificate whose issuer name differs from its
|
|
1369
|
+
* subject name is listed in `skipped` with the reason `trust/not-self-issued` and returned, as DER,
|
|
1370
|
+
* in `intermediates`, which `pki.path.build` takes as `candidates`. A Windows store export includes
|
|
1371
|
+
* the Intermediate Certification Authorities store, which holds certificates the host has seen
|
|
1372
|
+
* rather than ones it trusts, and OpenSSL's default verification also requires a self-issued
|
|
1373
|
+
* certificate at the top of a chain. A certificate whose names cannot be compared, because an
|
|
1374
|
+
* attribute value carries a control byte, is listed in `skipped` with that reason and is returned in
|
|
1375
|
+
* neither list.
|
|
1376
|
+
*
|
|
1377
|
+
* A certificate whose key is listed in `opts.distrustedKeys` is neither an anchor nor an
|
|
1378
|
+
* intermediate; it is listed in `skipped` with the reason `trust/distrusted-key`. A bundle left with
|
|
1379
|
+
* no anchor is refused with `trust/no-anchors`.
|
|
1380
|
+
*
|
|
1324
1381
|
* A host whose roots are not in a readable PEM file is refused, and the refusal names the command
|
|
1325
1382
|
* that exports them. macOS keeps its roots in a binary keychain and Windows in a registry-backed
|
|
1326
1383
|
* store, and reading either means running another program, which this toolkit does not do. Falling
|
|
1327
1384
|
* back to Node's set instead would report anchors the host does not use, so that takes the
|
|
1328
|
-
* explicit `opts.allowNodeBundle`.
|
|
1385
|
+
* explicit `opts.allowNodeBundle`. Node's set is the Mozilla list as of the Node release that
|
|
1386
|
+
* compiled it, so a root that Mozilla has since removed stays in it until Node is upgraded;
|
|
1387
|
+
* `opts.distrustedKeys` removes one sooner.
|
|
1329
1388
|
*
|
|
1330
1389
|
* @opts
|
|
1331
1390
|
* - `path` -- read this file instead of searching the per-platform list.
|
|
1332
|
-
* - `allowNodeBundle` --
|
|
1391
|
+
* - `allowNodeBundle` -- `true` returns Node's compiled-in set when no bundle can be read.
|
|
1392
|
+
* It takes `true`, `false`, `null` or an absent option, and any other value is refused with
|
|
1393
|
+
* `trust/bad-input`.
|
|
1333
1394
|
* - `maxBytes` -- cap the bundle read (default `C.LIMITS.TRUST_MAX_BYTES`).
|
|
1395
|
+
* - `distrustedKeys` -- SHA-256 digests of SubjectPublicKeyInfo values (64 hex characters or 32
|
|
1396
|
+
* bytes each, see `pki.trust.spkiDigest`) whose certificates are left out.
|
|
1334
1397
|
*
|
|
1335
1398
|
* @example
|
|
1336
1399
|
* // Read the host's bundle where there is one, and say so when there is not. Check `source`
|
|
@@ -1347,9 +1410,12 @@ function systemAnchors(opts) {
|
|
|
1347
1410
|
"unknown systemAnchors option ");
|
|
1348
1411
|
var maxBytes = guard.limits.cap(opts.maxBytes, "systemAnchors: opts.maxBytes",
|
|
1349
1412
|
LIMITS.TRUST_MAX_BYTES, { E: E, code: "trust/bad-input", min: 1, max: LIMITS.TRUST_MAX_BYTES });
|
|
1413
|
+
var allowNodeBundle = guard.identifier.booleanOption(opts.allowNodeBundle, E, "trust/bad-input",
|
|
1414
|
+
"systemAnchors: opts.allowNodeBundle");
|
|
1415
|
+
var distrusted = guard.distrust.keys(opts.distrustedKeys, E, "trust/bad-input", "systemAnchors: opts.distrustedKeys");
|
|
1350
1416
|
|
|
1351
1417
|
var chosen = null;
|
|
1352
|
-
if (opts.path
|
|
1418
|
+
if (opts.path != null) {
|
|
1353
1419
|
if (typeof opts.path !== "string" || opts.path.length === 0) {
|
|
1354
1420
|
throw E("trust/bad-input", "systemAnchors: opts.path must be a non-empty string");
|
|
1355
1421
|
}
|
|
@@ -1364,7 +1430,7 @@ function systemAnchors(opts) {
|
|
|
1364
1430
|
* set a validation runs against for a different one. */
|
|
1365
1431
|
try { text = _readBundleText(chosen, maxBytes); }
|
|
1366
1432
|
catch (e) {
|
|
1367
|
-
if (
|
|
1433
|
+
if (!allowNodeBundle || !e || e.code !== "trust/no-readable-store") throw e;
|
|
1368
1434
|
text = null;
|
|
1369
1435
|
chosen = null;
|
|
1370
1436
|
}
|
|
@@ -1382,15 +1448,36 @@ function systemAnchors(opts) {
|
|
|
1382
1448
|
}
|
|
1383
1449
|
|
|
1384
1450
|
if (text === null) {
|
|
1385
|
-
if (
|
|
1451
|
+
if (!allowNodeBundle) {
|
|
1386
1452
|
throw E("trust/no-readable-store", "this host keeps no CA bundle this toolkit can read" +
|
|
1387
1453
|
(chosen === null ? "" : " at " + guard.text.showValue(chosen)) + "; " + _exportHint() +
|
|
1388
1454
|
", or pass opts.allowNodeBundle to use the Mozilla set compiled into Node, which is NOT " +
|
|
1389
1455
|
"what this host trusts");
|
|
1390
1456
|
}
|
|
1391
|
-
return _anchorsFromPems(nodeTls.rootCertificates, "node-bundled", null);
|
|
1457
|
+
return _anchorsFromPems(nodeTls.rootCertificates, "node-bundled", null, distrusted);
|
|
1392
1458
|
}
|
|
1393
|
-
return _anchorsFromBundle(text, chosen);
|
|
1459
|
+
return _anchorsFromBundle(text, chosen, distrusted);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
/** @internal Why a parsed bundle certificate is not an anchor, or null when it is one. A
|
|
1463
|
+
* distrusted key is reported first, so a listed intermediate is not offered as a candidate either.
|
|
1464
|
+
* The name comparison is the RFC 5280 sec. 7.1 one, and a name it cannot compare (a control byte
|
|
1465
|
+
* in an attribute value) is reported rather than thrown, so one malformed entry does not refuse the
|
|
1466
|
+
* whole bundle. */
|
|
1467
|
+
function _systemEntryRefusal(cert, distrusted) {
|
|
1468
|
+
if (guard.distrust.has(distrusted, cert.subjectPublicKeyInfo.bytes)) {
|
|
1469
|
+
return { intermediate: false, reason: "trust/distrusted-key: the certificate's key is listed in opts.distrustedKeys" };
|
|
1470
|
+
}
|
|
1471
|
+
var selfIssued;
|
|
1472
|
+
try { selfIssued = guard.name.dnEqual(cert.subject.rdns, cert.issuer.rdns, E, "trust/bad-input", "the bundle certificate"); }
|
|
1473
|
+
catch (e) {
|
|
1474
|
+
return { intermediate: false, reason: (e && e.code ? e.code : "trust/bad-input") + ": " + guard.text.describeThrown(e) };
|
|
1475
|
+
}
|
|
1476
|
+
if (!selfIssued) {
|
|
1477
|
+
return { intermediate: true, reason: "trust/not-self-issued: the issuer name differs from the subject name, so the " +
|
|
1478
|
+
"certificate is not a root; it is returned in intermediates for chain building" };
|
|
1479
|
+
}
|
|
1480
|
+
return null;
|
|
1394
1481
|
}
|
|
1395
1482
|
|
|
1396
1483
|
/** @internal A line that reads as a BEGIN boundary but does not start one, because RFC 7468 sec. 2
|
|
@@ -1429,7 +1516,7 @@ function _reportUnusableBoundaries(bytes, rows, skipped) {
|
|
|
1429
1516
|
/** @internal Every certificate block becomes an anchor and every other block is reported, because
|
|
1430
1517
|
* a bundle an operator assembled by hand is exactly where a key or a CRL ends up, and a reader
|
|
1431
1518
|
* that drops one silently leaves them believing it was read. */
|
|
1432
|
-
function _anchorsFromBundle(text, chosen) {
|
|
1519
|
+
function _anchorsFromBundle(text, chosen, distrusted) {
|
|
1433
1520
|
var rows;
|
|
1434
1521
|
try { rows = pkix.pemDecodeBundle(text, {}, errors.PemError); }
|
|
1435
1522
|
catch (e) {
|
|
@@ -1440,7 +1527,7 @@ function _anchorsFromBundle(text, chosen) {
|
|
|
1440
1527
|
throw E("trust/bad-bundle", "the CA bundle at " + guard.text.showValue(chosen) +
|
|
1441
1528
|
" is not readable as PEM: " + guard.text.describeThrown(e), e);
|
|
1442
1529
|
}
|
|
1443
|
-
var anchors = [], skipped = [];
|
|
1530
|
+
var anchors = [], skipped = [], intermediates = [];
|
|
1444
1531
|
_reportUnusableBoundaries(text, rows, skipped);
|
|
1445
1532
|
for (var i = 0; i < rows.length; i++) {
|
|
1446
1533
|
var row = rows[i];
|
|
@@ -1479,40 +1566,53 @@ function _anchorsFromBundle(text, chosen) {
|
|
|
1479
1566
|
guard.list.append(skipped, { index: row.index, offset: row.offset, label: row.label, reason: why });
|
|
1480
1567
|
continue;
|
|
1481
1568
|
}
|
|
1569
|
+
var refusal = _systemEntryRefusal(cert, distrusted);
|
|
1570
|
+
if (refusal !== null) {
|
|
1571
|
+
guard.list.append(skipped, { index: row.index, offset: row.offset, label: row.label, reason: refusal.reason });
|
|
1572
|
+
if (refusal.intermediate) guard.list.append(intermediates, intrinsic.bufferFrom(row.der));
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1482
1575
|
guard.list.append(anchors, _mkAnchor(cert, {
|
|
1483
1576
|
distrustAfter: {}, purposes: null, label: null, mozillaCaPolicy: false,
|
|
1484
1577
|
}));
|
|
1485
1578
|
}
|
|
1486
1579
|
if (anchors.length === 0) {
|
|
1487
1580
|
throw E("trust/no-anchors", "the CA bundle at " + guard.text.showValue(chosen) +
|
|
1488
|
-
" carries no certificate, so it names no trust anchor");
|
|
1581
|
+
" carries no self-issued certificate this read admits, so it names no trust anchor");
|
|
1489
1582
|
}
|
|
1490
|
-
return _systemResult(anchors, "ca-bundle", chosen, skipped);
|
|
1583
|
+
return _systemResult(anchors, intermediates, "ca-bundle", chosen, skipped);
|
|
1491
1584
|
}
|
|
1492
1585
|
|
|
1493
|
-
function _anchorsFromPems(pems, source, chosen) {
|
|
1494
|
-
var anchors = [], skipped = [];
|
|
1586
|
+
function _anchorsFromPems(pems, source, chosen, distrusted) {
|
|
1587
|
+
var anchors = [], skipped = [], intermediates = [];
|
|
1495
1588
|
for (var i = 0; i < pems.length; i++) {
|
|
1496
|
-
var cert = null, why = null;
|
|
1497
|
-
try {
|
|
1589
|
+
var cert = null, why = null, der = null;
|
|
1590
|
+
try { der = x509.pemDecode(pems[i], "CERTIFICATE"); cert = x509.parse(der); }
|
|
1498
1591
|
catch (e) { why = (e && e.code ? e.code : "trust/not-a-certificate") + ": " + guard.text.describeThrown(e); }
|
|
1499
1592
|
if (cert === null) {
|
|
1500
1593
|
guard.list.append(skipped, { index: i, offset: null, label: "CERTIFICATE", reason: why });
|
|
1501
1594
|
continue;
|
|
1502
1595
|
}
|
|
1596
|
+
var refusal = _systemEntryRefusal(cert, distrusted);
|
|
1597
|
+
if (refusal !== null) {
|
|
1598
|
+
guard.list.append(skipped, { index: i, offset: null, label: "CERTIFICATE", reason: refusal.reason });
|
|
1599
|
+
if (refusal.intermediate) guard.list.append(intermediates, intrinsic.bufferFrom(der));
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1503
1602
|
guard.list.append(anchors, _mkAnchor(cert, {
|
|
1504
1603
|
distrustAfter: {}, purposes: null, label: null, mozillaCaPolicy: false,
|
|
1505
1604
|
}));
|
|
1506
1605
|
}
|
|
1507
|
-
if (anchors.length === 0) throw E("trust/no-anchors", "the " + source + " set carries no certificate");
|
|
1508
|
-
return _systemResult(anchors, source, chosen, skipped);
|
|
1606
|
+
if (anchors.length === 0) throw E("trust/no-anchors", "the " + source + " set carries no self-issued certificate this read admits");
|
|
1607
|
+
return _systemResult(anchors, intermediates, source, chosen, skipped);
|
|
1509
1608
|
}
|
|
1510
1609
|
|
|
1511
1610
|
/** @internal Defined rather than assigned, and a fresh list each call: the caller owns what it is
|
|
1512
1611
|
* handed, so emptying it must not change what the next call returns. */
|
|
1513
|
-
function _systemResult(anchors, source, chosen, skipped) {
|
|
1612
|
+
function _systemResult(anchors, intermediates, source, chosen, skipped) {
|
|
1514
1613
|
var out = intrinsic.create(null);
|
|
1515
1614
|
guard.verdict.set(out, "anchors", anchors);
|
|
1615
|
+
guard.verdict.set(out, "intermediates", intermediates);
|
|
1516
1616
|
guard.verdict.set(out, "source", source);
|
|
1517
1617
|
guard.verdict.set(out, "path", chosen);
|
|
1518
1618
|
guard.verdict.set(out, "skipped", skipped);
|
|
@@ -1528,4 +1628,5 @@ module.exports = intrinsic.freeze({
|
|
|
1528
1628
|
systemAnchors: systemAnchors,
|
|
1529
1629
|
discoversBundles: discoversBundles,
|
|
1530
1630
|
anchor: anchor,
|
|
1631
|
+
spkiDigest: spkiDigest,
|
|
1531
1632
|
});
|
package/lib/trustanchor-build.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
|
+
// codebase-patterns:allow-file caller-option-presence-tested-against-undefined
|
|
5
|
+
/** @internal Every member of a trust anchor RESTRICTS what it may certify: a policy set,
|
|
6
|
+
* policy flags, name constraints, a path length. A `null` here is a malformed spec rather
|
|
7
|
+
* than an option left out, because reading it as an omission would drop a restriction the
|
|
8
|
+
* caller named and build a WIDER anchor than they described without saying so. Presence is
|
|
9
|
+
* asked against `undefined` alone, and a null member is refused by the builder's own typed
|
|
10
|
+
* error, which the nested-record vector in schema-trustanchor.test.js pins. */
|
|
4
11
|
/**
|
|
5
12
|
* @module pki.trustanchor
|
|
6
13
|
* @nav Trust
|
|
@@ -98,7 +105,15 @@ function _certPathDer(spec) {
|
|
|
98
105
|
throw _err("trustanchor/bad-input", "certPath.taName must be a non-empty distinguished name (RFC 5914 sec. 2)");
|
|
99
106
|
}
|
|
100
107
|
var kids = [b.raw(nameDer)];
|
|
101
|
-
/** @internal
|
|
108
|
+
/** @internal Presence is asked as "not undefined" here, and deliberately not as "not
|
|
109
|
+
* absent", which is how a caller option is read everywhere else. These members RESTRICT
|
|
110
|
+
* what the anchor may certify: a policy set, policy flags, name constraints and a path
|
|
111
|
+
* length. Reading a `null` as an omission would drop the restriction the caller named and
|
|
112
|
+
* build a WIDER anchor than they described, without saying so. A member given as `null`
|
|
113
|
+
* is a malformed spec and is refused by the builder's own typed error, which the
|
|
114
|
+
* nested-record vector in schema-trustanchor.test.js pins.
|
|
115
|
+
*
|
|
116
|
+
* The RFC 5914 module is IMPLICIT TAGS, so a context tag REPLACES the member's own
|
|
102
117
|
* universal tag rather than wrapping it. `b.implicit` re-tags the value it is given; only the
|
|
103
118
|
* three members the module writes EXPLICIT beside get a wrapper. */
|
|
104
119
|
if (spec.certificate !== undefined) {
|
package/lib/tsp-sign.js
CHANGED
|
@@ -123,7 +123,9 @@ function _signingCertV2(certDer, hashName) {
|
|
|
123
123
|
* @opts nonce The request nonce to echo (a number or BigInt), for replay protection.
|
|
124
124
|
* @opts accuracy The genTime +/- accuracy, as `{ seconds?, millis?, micros? }`, naming at
|
|
125
125
|
* least one field; omit it to make no accuracy claim.
|
|
126
|
-
* @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean).
|
|
126
|
+
* @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean). A value
|
|
127
|
+
* other than `true`, `false`, `null` or an absent option is refused
|
|
128
|
+
* `tsp/bad-input`.
|
|
127
129
|
* @opts certHashAlgorithm The ESSCertIDv2 hash algorithm name. Default `sha256`.
|
|
128
130
|
* @opts digestAlgorithm / pss Passed through to `pki.cms.sign` (the signer digest algorithm, RSA-PSS padding).
|
|
129
131
|
* @opts additionalSignedAttributes Extra signed attributes to carry on the token, as `[{ type, values }]` (forwarded to `pki.cms.sign`).
|
|
@@ -163,6 +165,11 @@ function _sign(messageImprint, tsa, opts) {
|
|
|
163
165
|
}
|
|
164
166
|
guard.identifier.assertKnownKeys(opts, _SIGN_OPTS, _err, "tsp/bad-input", "pki.tsp.sign has an unknown option ");
|
|
165
167
|
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.sign options must be an object");
|
|
168
|
+
/** @internal The ordering claim is emitted below only for the boolean `true`, so a value of any
|
|
169
|
+
* other type left the TSTInfo saying the opposite of what the caller asked, which is what RFC
|
|
170
|
+
* 3161 sec. 2.4.2 has a relying party read to compare two tokens from this TSA. `certReq` on the
|
|
171
|
+
* request verb has been typed since it was written; this is the same read. */
|
|
172
|
+
guard.identifier.booleanOption(opts.ordering, _err, "tsp/bad-input", "pki.tsp.sign opts.ordering");
|
|
166
173
|
if (tsa && typeof tsa === "object" && !Buffer.isBuffer(tsa)) {
|
|
167
174
|
guard.identifier.assertKnownKeys(tsa, _TSA_KEYS, _err, "tsp/bad-input", function (k) {
|
|
168
175
|
return "unknown TSA field " + JSON.stringify(k) +
|
|
@@ -643,6 +650,9 @@ function _buildTsaChains(leaf, pool) {
|
|
|
643
650
|
* certificate when `certReq` was false) and an intermediate absent from the
|
|
644
651
|
* token can still verify and chain.
|
|
645
652
|
* @opts revocationChecker Passed through to `pki.path.validate`.
|
|
653
|
+
* @opts distrustedKeys SHA-256 digests of SubjectPublicKeyInfo DER (64 hexadecimal characters or
|
|
654
|
+
* 32 bytes each, see `pki.trust.spkiDigest`). A listed key on the TSA chain,
|
|
655
|
+
* the anchor included, fails the token with `tsp/untrusted-tsa`.
|
|
646
656
|
* @example
|
|
647
657
|
* var pair = await pki.key.generate("Ed25519");
|
|
648
658
|
* var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
|
|
@@ -657,7 +667,7 @@ function _buildTsaChains(leaf, pool) {
|
|
|
657
667
|
* res.valid; // boolean; pass opts.trustAnchors to also chain the TSA cert to a root
|
|
658
668
|
* res.genTime; // Date, read from the verified eContent
|
|
659
669
|
*/
|
|
660
|
-
var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, trustAnchors: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1, allowWeakDigests: 1 });
|
|
670
|
+
var _VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), { certs: 1, trustAnchors: 1, nonce: 1, reqPolicy: 1, revocationChecker: 1, allowWeakDigests: 1, distrustedKeys: 1 });
|
|
661
671
|
|
|
662
672
|
function _anyCritical(extensions) {
|
|
663
673
|
return guard.list.anyMatches(extensions, function (e) { return !!e && !!e.critical; });
|
|
@@ -677,6 +687,8 @@ async function verify(token, data, opts) {
|
|
|
677
687
|
if (opts.certs != null && (!Array.isArray(opts.certs) || !guard.list.allMatch(opts.certs, function (c) { return guard.bytes.isByteSource(c); }))) {
|
|
678
688
|
throw _err("tsp/bad-input", "pki.tsp.verify opts.certs must be an array of DER certificate Buffers");
|
|
679
689
|
}
|
|
690
|
+
/** @internal Typed here, ahead of the path step, whose throws all become `tsp/untrusted-tsa`. */
|
|
691
|
+
var distrustedKeys = guard.distrust.keys(opts.distrustedKeys, _err, "tsp/bad-input", "pki.tsp.verify opts.distrustedKeys");
|
|
680
692
|
var tokenDer = _tokenDer(token);
|
|
681
693
|
var parsed = schemaTsp.parseToken(tokenDer);
|
|
682
694
|
var tst = parsed.tstInfo;
|
|
@@ -725,6 +737,7 @@ async function verify(token, data, opts) {
|
|
|
725
737
|
async function validateAt(chain, when) {
|
|
726
738
|
var res = await pathValidate.validate(chain, {
|
|
727
739
|
time: when, trustAnchors: opts.trustAnchors, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
|
|
740
|
+
distrustedKeys: distrustedKeys,
|
|
728
741
|
});
|
|
729
742
|
if (_moreEstablished(res.revocationChecked, revocationChecked)) revocationChecked = res.revocationChecked;
|
|
730
743
|
if (anchorConstraints == null && res.anchorConstraints != null) anchorConstraints = res.anchorConstraints;
|