@blamejs/pki 0.5.3 → 0.5.5
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 +43 -1
- package/MIGRATING.md +31 -0
- package/README.md +3 -3
- package/lib/attrcert-sign.js +11 -7
- package/lib/cmp-session.js +23 -10
- package/lib/cmp-verify.js +12 -4
- package/lib/cms-decrypt.js +71 -30
- package/lib/cms-encrypt.js +7 -11
- package/lib/crl-sign.js +170 -24
- package/lib/est.js +77 -1
- package/lib/guard-all.js +6 -0
- package/lib/guard-encoding.js +35 -6
- package/lib/guard-identifier.js +27 -1
- package/lib/guard-json.js +44 -8
- package/lib/guard-name.js +31 -8
- package/lib/guard-parsed.js +443 -0
- package/lib/hpke.js +36 -3
- package/lib/jose.js +8 -0
- package/lib/lint.js +20 -4
- package/lib/merkle.js +9 -0
- package/lib/ocsp.js +38 -8
- package/lib/path-validate.js +201 -72
- package/lib/pkcs12-build.js +34 -7
- package/lib/pki-build.js +12 -1
- package/lib/schema-crl.js +7 -1
- package/lib/schema-ocsp.js +6 -1
- package/lib/schema-pkcs12.js +7 -2
- package/lib/schema-pkix.js +76 -0
- package/lib/schema-x509.js +14 -1
- package/lib/sign-scheme.js +22 -5
- package/lib/smime.js +47 -0
- package/lib/trust.js +121 -10
- package/lib/tsp-sign.js +40 -19
- package/lib/validator-cose.js +86 -1
- package/lib/validator-tpm.js +8 -3
- package/lib/webauthn-mds.js +19 -29
- package/lib/webauthn.js +40 -18
- package/lib/x509-sign.js +6 -2
- package/package.json +5 -1
- package/sbom.cdx.json +6 -6
package/lib/schema-ocsp.js
CHANGED
|
@@ -517,7 +517,12 @@ var parseRequest = pkix.makeParser({ pemLabel: "OCSP REQUEST", PemError: PemErro
|
|
|
517
517
|
* res.responseStatus.name; // -> "successful"
|
|
518
518
|
* res.basicResponse.responses[0].certStatus.type; // -> "good" | "revoked" | "unknown"
|
|
519
519
|
*/
|
|
520
|
-
|
|
520
|
+
// The result records the input it was derived from. A verdict verb reads the
|
|
521
|
+
// signature, the algorithm that verifies it and the bytes it covers off this object as
|
|
522
|
+
// three separate properties, and re-derives all three from the recorded input rather
|
|
523
|
+
// than trusting the object it was handed -- so an object rebuilt or edited after
|
|
524
|
+
// parsing cannot make the three describe different responses.
|
|
525
|
+
var parseResponse = pkix.makeRecordingParser({ pemLabel: "OCSP RESPONSE", PemError: PemError, ErrorClass: OcspError, prefix: "ocsp", what: "OCSP response", topSchema: OCSP_RESPONSE, ns: NS }, "ocspResponse");
|
|
521
526
|
|
|
522
527
|
/**
|
|
523
528
|
* @primitive pki.schema.ocsp.pemDecode
|
package/lib/schema-pkcs12.js
CHANGED
|
@@ -523,10 +523,15 @@ function _buildBag(rec, state, ctx) {
|
|
|
523
523
|
* var store = pki.schema.pkcs12.parse(der);
|
|
524
524
|
* store.safeBags.map(function (b) { return b.type; });
|
|
525
525
|
*/
|
|
526
|
-
|
|
526
|
+
// The result records the input it was derived from. pki.pkcs12.verifyMac and open read
|
|
527
|
+
// the MACed byte range and the bags returned as verified off this object as two
|
|
528
|
+
// separate properties, and re-derive both from the recorded input rather than trusting
|
|
529
|
+
// the object -- so "verify this" and "return that" cannot come to name different
|
|
530
|
+
// stores, whether by rebuilding the object or by editing it after parsing.
|
|
531
|
+
var parse = pkix.makeRecordingParser({
|
|
527
532
|
pemLabel: "PKCS12", PemError: PemError, ErrorClass: Pkcs12Error,
|
|
528
533
|
prefix: "pkcs12", what: "PFX", topSchema: PFX, ns: NS, ber: true,
|
|
529
|
-
});
|
|
534
|
+
}, "pkcs12Store");
|
|
530
535
|
|
|
531
536
|
/**
|
|
532
537
|
* @primitive pki.schema.pkcs12.pemDecode
|
package/lib/schema-pkix.js
CHANGED
|
@@ -669,6 +669,66 @@ function distributionPointName(ns, node, code) {
|
|
|
669
669
|
throw ns.E(code, "DistributionPointName must be fullName [0] or nameRelativeToCRLIssuer [1] (RFC 5280 sec. 4.2.1.13)");
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
// A certificate's keyUsage as the named booleans the shared sec. 4.2.1 decoder produces, or null
|
|
673
|
+
// when the certificate carries no keyUsage extension -- which places no restriction (sec. 4.2.1.3),
|
|
674
|
+
// a distinct answer from "carries one that permits nothing".
|
|
675
|
+
//
|
|
676
|
+
// Declared here because "may this certificate do X" is asked at five boundaries and the answer has
|
|
677
|
+
// to be the same at all of them. keyUsage is a NamedBitList: DER drops its trailing zero bits
|
|
678
|
+
// (X.690 sec. 11.2.2) and sec. 4.2.1.3 requires at least one bit set. A boundary that reads the
|
|
679
|
+
// bits with a plain BIT STRING read applies neither, so the same certificate is authorized there
|
|
680
|
+
// and rejected as malformed by the issuing side and the path validator -- one extension with two
|
|
681
|
+
// readings, and the permissive one deciding.
|
|
682
|
+
//
|
|
683
|
+
// `E(code, message, cause)` is the caller's typed error factory, so each boundary keeps its own
|
|
684
|
+
// domain while the RULE stays single-homed.
|
|
685
|
+
var _KU_DECODER = new WeakMap(); // one decoder table per namespace, built on first use
|
|
686
|
+
function keyUsageOf(ns, cert, E, code, label) {
|
|
687
|
+
var exts = (cert && cert.extensions) || [];
|
|
688
|
+
var want = ns.oid.byName("keyUsage");
|
|
689
|
+
for (var i = 0; i < exts.length; i++) {
|
|
690
|
+
if (exts[i].oid !== want || exts[i].value == null) continue;
|
|
691
|
+
var dec = _KU_DECODER.get(ns);
|
|
692
|
+
if (!dec) { dec = certExtensionDecoders(ns).byOid[want]; _KU_DECODER.set(ns, dec); }
|
|
693
|
+
try { return dec(exts[i].value); }
|
|
694
|
+
catch (e) { throw E(code, "the " + label + " keyUsage extension is malformed", e); }
|
|
695
|
+
}
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// IssuingDistributionPoint ::= SEQUENCE { distributionPoint [0] OPTIONAL,
|
|
700
|
+
// onlyContainsUserCerts [1] DEFAULT FALSE, onlyContainsCACerts [2] DEFAULT FALSE,
|
|
701
|
+
// onlySomeReasons [3] ReasonFlags OPTIONAL, indirectCRL [4] DEFAULT FALSE,
|
|
702
|
+
// onlyContainsAttributeCerts [5] DEFAULT FALSE } (RFC 5280 sec. 5.2.5).
|
|
703
|
+
//
|
|
704
|
+
// Declared here rather than in a consumer because the extension answers a
|
|
705
|
+
// question -- which certificates does this CRL speak for -- that both the path
|
|
706
|
+
// validator and the standalone CRL verbs act on, and a scope flag read by a hand
|
|
707
|
+
// walk over the raw children is read by the reader's own rules instead of the
|
|
708
|
+
// encoding's. Each flag is an IMPLICIT BOOLEAN, so `implicitBoolean` is what
|
|
709
|
+
// holds it to one content octet of 0x00 or 0xFF (X.690 sec. 11.1); a hand walk
|
|
710
|
+
// testing a content byte reads an EMPTY [4] as absent and a multi-octet one by
|
|
711
|
+
// whichever byte it happens to index, and "absent" is the reading that grants
|
|
712
|
+
// permission to answer. `trailing` supplies the rest of the grammar: strictly
|
|
713
|
+
// ascending tags, each field at most once, nothing outside 0..5.
|
|
714
|
+
//
|
|
715
|
+
// A present DEFAULT-FALSE flag encoding FALSE is well-formed at the leaf but is
|
|
716
|
+
// still an encoding DER forbids (X.690 sec. 11.5 omits the default), so the
|
|
717
|
+
// caller decides what that means for it -- reported as a value of `false`
|
|
718
|
+
// against a `present` of true, never silently normalized away.
|
|
719
|
+
function issuingDistributionPoint(code) {
|
|
720
|
+
return schema.seq([
|
|
721
|
+
schema.trailing([
|
|
722
|
+
{ tag: 0, name: "distributionPoint", schema: schema.any() },
|
|
723
|
+
{ tag: 1, name: "onlyContainsUserCerts", schema: schema.implicitBoolean(1) },
|
|
724
|
+
{ tag: 2, name: "onlyContainsCACerts", schema: schema.implicitBoolean(2) },
|
|
725
|
+
{ tag: 3, name: "onlySomeReasons", schema: schema.implicitBitString(3) },
|
|
726
|
+
{ tag: 4, name: "indirectCRL", schema: schema.implicitBoolean(4) },
|
|
727
|
+
{ tag: 5, name: "onlyContainsAttributeCerts", schema: schema.implicitBoolean(5) },
|
|
728
|
+
], { minTag: 0, maxTag: 5, unexpectedCode: code, orderCode: code }),
|
|
729
|
+
], { assert: "sequence", code: code, what: "IssuingDistributionPoint" });
|
|
730
|
+
}
|
|
731
|
+
|
|
672
732
|
// certExtensionDecoders(ns) -- the ns-parameterized RFC 5280 sec. 4.2.1 extension
|
|
673
733
|
// VALUE decoders. `x509.parse` surfaces each extension as { oid, name, critical,
|
|
674
734
|
// value } with `value` the raw inner OCTET-STRING content (a Buffer); the path
|
|
@@ -1496,6 +1556,19 @@ function makeParser(opts) {
|
|
|
1496
1556
|
return function (input) { return runParse(input, opts); };
|
|
1497
1557
|
}
|
|
1498
1558
|
|
|
1559
|
+
// makeRecordingParser(opts, kind) -> a parser that also records the bytes it read.
|
|
1560
|
+
//
|
|
1561
|
+
// The pairing is here rather than repeated per format because it is one decision, not four: a
|
|
1562
|
+
// structure whose parse feeds a VERDICT needs its fields bound to the byte string they came from,
|
|
1563
|
+
// since a parsed structure presents the signed range, the signature and the fields that range
|
|
1564
|
+
// encodes as separate properties -- and a caller can hand back a genuine range beside substituted
|
|
1565
|
+
// fields. `kind` is the tag the matching door re-derives under, so a certificate cannot be presented
|
|
1566
|
+
// where a CRL is expected. The record lives in guard-parsed, off the object, unreachable through it.
|
|
1567
|
+
function makeRecordingParser(opts, kind) {
|
|
1568
|
+
return guard.parsed.recordingParser(kind, makeParser(opts), opts.ErrorClass,
|
|
1569
|
+
opts.prefix + "/bad-input", "a " + opts.what);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1499
1572
|
// The X.509 SIGNED{ToBeSigned} macro (RFC 5280 sec. 4.1.1.3): the outer
|
|
1500
1573
|
// SEQUENCE { toBeSigned, signatureAlgorithm AlgorithmIdentifier,
|
|
1501
1574
|
// signatureValue BIT STRING } shared by Certificate, CertificateList and
|
|
@@ -1573,6 +1646,7 @@ module.exports = {
|
|
|
1573
1646
|
pbmac1Params: pbmac1Params,
|
|
1574
1647
|
spki: spki,
|
|
1575
1648
|
makeParser: makeParser,
|
|
1649
|
+
makeRecordingParser: makeRecordingParser,
|
|
1576
1650
|
signedEnvelopeTbs: signedEnvelopeTbs,
|
|
1577
1651
|
rootSequenceChildren: rootSequenceChildren,
|
|
1578
1652
|
assertPolicyQualifiers: assertPolicyQualifiers,
|
|
@@ -1587,6 +1661,8 @@ module.exports = {
|
|
|
1587
1661
|
generalName: generalName,
|
|
1588
1662
|
generalNames: generalNames,
|
|
1589
1663
|
distributionPointName: distributionPointName,
|
|
1664
|
+
issuingDistributionPoint: issuingDistributionPoint,
|
|
1665
|
+
keyUsageOf: keyUsageOf,
|
|
1590
1666
|
generalizedTime: generalizedTime,
|
|
1591
1667
|
utf8Text: utf8Text,
|
|
1592
1668
|
rawNonEmptySequence: rawNonEmptySequence,
|
package/lib/schema-x509.js
CHANGED
|
@@ -231,7 +231,20 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
|
|
|
231
231
|
* cert.validity.notAfter; // Date
|
|
232
232
|
* cert.signatureAlgorithm.name; // "Ed25519" (the algorithm the issuer signed with)
|
|
233
233
|
*/
|
|
234
|
-
|
|
234
|
+
// The parser RECORDS the bytes it read, off the returned object, so a verb that computes a verdict
|
|
235
|
+
// can re-derive from them instead of trusting the object it was handed.
|
|
236
|
+
//
|
|
237
|
+
// Completeness alone cannot carry a certificate's meaning. A certificate is one signature over one
|
|
238
|
+
// byte range, but a parsed certificate presents that range (`tbsBytes`), the signature, and every
|
|
239
|
+
// field the range encodes as separate properties. Keep a real CA certificate's signed bytes and
|
|
240
|
+
// signature and replace only `subjectPublicKeyInfo`, and the signature check still passes over the
|
|
241
|
+
// original range while the substituted key is what gets used to verify the next certificate in the
|
|
242
|
+
// chain -- a forged chain out of a genuine certificate. Emptying `extensions` is the same move
|
|
243
|
+
// against basicConstraints, keyUsage, name constraints, and the unknown-critical rule.
|
|
244
|
+
//
|
|
245
|
+
// Recording the source is what makes the object safe to accept: the verdict verbs parse it again
|
|
246
|
+
// from these bytes, so anything done to the object afterwards is discarded rather than believed.
|
|
247
|
+
var parse = pkix.makeRecordingParser({ pemLabel: "CERTIFICATE", PemError: PemError, ErrorClass: CertificateError, prefix: "x509", what: "certificate", topSchema: CERTIFICATE, ns: NS }, "certificate");
|
|
235
248
|
|
|
236
249
|
// matches(root): does the decoded DER look like a Certificate? A CSR and a CRL
|
|
237
250
|
// share the outer SEQUENCE-of-3 envelope, so the discriminator is inside the
|
package/lib/sign-scheme.js
CHANGED
|
@@ -22,6 +22,7 @@ var pkcs8 = require("./schema-pkcs8");
|
|
|
22
22
|
var webcrypto = require("./webcrypto");
|
|
23
23
|
var subtle = webcrypto.webcrypto.subtle;
|
|
24
24
|
var validator = require("./validator-all");
|
|
25
|
+
var guard = require("./guard-all");
|
|
25
26
|
var compositeSig = require("./composite-sig");
|
|
26
27
|
var b = asn1.build;
|
|
27
28
|
function O(name) { return oid.byName(name); }
|
|
@@ -213,12 +214,28 @@ function _importKey(key, imp, E) {
|
|
|
213
214
|
// signs with. One whose material this process cannot reach is refused with that as the reason.
|
|
214
215
|
return webcrypto.adoptKey(key, imp, ["sign"], E, "bad-input");
|
|
215
216
|
}
|
|
216
|
-
|
|
217
|
+
// A Uint8Array is copied here and a PEM string is decoded here, so both leave a SECOND copy of a
|
|
218
|
+
// private key that nothing else can reach -- wiped once the engine has imported it, which is the
|
|
219
|
+
// only thing that reads it. A caller's own Buffer is passed through untouched: they hold a live
|
|
220
|
+
// reference and will use it again, so clearing it would destroy their key.
|
|
221
|
+
var der, owned = false;
|
|
217
222
|
if (Buffer.isBuffer(key)) der = key;
|
|
218
|
-
else if (key instanceof Uint8Array) der = Buffer.from(key);
|
|
219
|
-
else if (typeof key === "string") {
|
|
220
|
-
|
|
221
|
-
|
|
223
|
+
else if (key instanceof Uint8Array) { der = Buffer.from(key); owned = true; }
|
|
224
|
+
else if (typeof key === "string") {
|
|
225
|
+
try { der = pkcs8.pemDecode(key); }
|
|
226
|
+
catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
|
|
227
|
+
owned = true;
|
|
228
|
+
} else throw E("bad-input", "a signer key must be a CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
|
|
229
|
+
var imported = subtle.importKey("pkcs8", der, imp, false, ["sign"]);
|
|
230
|
+
if (!owned) return imported;
|
|
231
|
+
// Wiped on the reject path too: a malformed key is not a way to leave the copy in memory.
|
|
232
|
+
return imported.then(function (k) {
|
|
233
|
+
guard.secret.zeroize(der, E, "bad-input", "the signer private-key copy");
|
|
234
|
+
return k;
|
|
235
|
+
}, function (e) {
|
|
236
|
+
guard.secret.zeroize(der, E, "bad-input", "the signer private-key copy");
|
|
237
|
+
throw e;
|
|
238
|
+
});
|
|
222
239
|
}
|
|
223
240
|
|
|
224
241
|
// signOverTbs(scheme, key, signedBytes, E) -> Promise<Buffer> the raw signature over signedBytes.
|
package/lib/smime.js
CHANGED
|
@@ -44,6 +44,43 @@ var SmimeError = frameworkError.SmimeError;
|
|
|
44
44
|
|
|
45
45
|
function _err(code, msg, cause) { return new SmimeError(code, msg, cause); }
|
|
46
46
|
|
|
47
|
+
// ---- the option surface each verb accepts -----------------------------------
|
|
48
|
+
//
|
|
49
|
+
// A misspelled option is the one input that reads as an omission rather than as a value: nothing is
|
|
50
|
+
// out of range, nothing fails to parse, and the caller who asked for something stricter silently
|
|
51
|
+
// gets the looser default. `protectHeaders` misspelled sends the headers a caller meant to protect
|
|
52
|
+
// as ordinary display copies; `strictMicalg` misspelled accepts the mismatch it was set to reject;
|
|
53
|
+
// `entity` misspelled wraps a caller's complete MIME entity inside another one.
|
|
54
|
+
//
|
|
55
|
+
// The tables are per VERB, not per module, because the surfaces genuinely differ -- `form` means
|
|
56
|
+
// something on sign and nothing on encrypt -- and a merged table would accept each verb's options at
|
|
57
|
+
// every other one, which is the same silence in a wider form. Each is the keys that verb's body and
|
|
58
|
+
// the helpers it hands `opts` to actually read: the value goes through _entityBytes, _cmsSignOpts or
|
|
59
|
+
// _cmsEncryptOpts as readily as it is read here, so a table built from the verb's own lines alone
|
|
60
|
+
// would refuse options that work.
|
|
61
|
+
var SIGN_OPTS = {
|
|
62
|
+
form: 1, entity: 1, contentType: 1, signingTime: 1, protectHeaders: 1, headers: 1, hcp: 1,
|
|
63
|
+
sid: 1, signedAttributes: 1, additionalSignedAttributes: 1,
|
|
64
|
+
};
|
|
65
|
+
var VERIFY_OPTS = {
|
|
66
|
+
certs: 1, trustAnchors: 1, time: 1, requiredEku: 1, checkPurpose: 1, strictMicalg: 1,
|
|
67
|
+
legacyHeaderProtection: 1,
|
|
68
|
+
};
|
|
69
|
+
var ENCRYPT_OPTS = {
|
|
70
|
+
entity: 1, contentType: 1, protectHeaders: 1, headers: 1, hcp: 1,
|
|
71
|
+
contentEncryptionAlgorithm: 1, oaepHash: 1, keyIdentifier: 1, ukm: 1,
|
|
72
|
+
};
|
|
73
|
+
var DECRYPT_OPTS = { recipientIndex: 1, maxIterations: 1, strictSmimeType: 1, legacyHeaderProtection: 1 };
|
|
74
|
+
var COMPRESS_OPTS = { entity: 1, contentType: 1, level: 1 };
|
|
75
|
+
var DECOMPRESS_OPTS = { maxOutputBytes: 1 };
|
|
76
|
+
|
|
77
|
+
function _knownOpts(opts, known, verb) {
|
|
78
|
+
guard.identifier.assertKnownKeys(opts, known, _err, "smime/bad-input", function (k) {
|
|
79
|
+
return "unknown option " + JSON.stringify(k) + " for pki.smime." + verb + " -- accepted: " +
|
|
80
|
+
Object.keys(known).sort().join(", ");
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
47
84
|
// RFC 8551 uses application/pkcs7-<kind>; OpenSSL's legacy `smime` command emits the PKCS#7
|
|
48
85
|
// application/x-pkcs7-<kind>. Accept both on the RECEIVE side (we always EMIT the RFC 8551 form).
|
|
49
86
|
function _isPkcs7(type, kind) {
|
|
@@ -520,6 +557,10 @@ function _base64Body(der) {
|
|
|
520
557
|
* @opts signingTime a `Date` for the CMS signing-time attribute, or false to omit it.
|
|
521
558
|
* @opts protectHeaders enable RFC 9788 header protection (`hp="clear"`) -- inline `opts.headers` on the signed payload + the outer display headers.
|
|
522
559
|
* @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`.
|
|
560
|
+
* @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`.
|
|
561
|
+
* @opts sid forwarded to cms.sign: the SignerIdentifier form, `"issuerAndSerial"` (default) or `"subjectKeyIdentifier"`.
|
|
562
|
+
* @opts signedAttributes forwarded: `false` omits the signed-attributes set entirely (a bare-signature SignedData).
|
|
563
|
+
* @opts additionalSignedAttributes forwarded: extra signed attributes to carry, as `[{ oid, values }]`.
|
|
523
564
|
* @example
|
|
524
565
|
* var pair = await pki.key.generate("Ed25519");
|
|
525
566
|
* var signerKeyPkcs8 = await pki.key.export(pair.privateKey);
|
|
@@ -529,6 +570,7 @@ function _base64Body(der) {
|
|
|
529
570
|
*/
|
|
530
571
|
async function sign(content, signers, opts) {
|
|
531
572
|
opts = opts || {};
|
|
573
|
+
_knownOpts(opts, SIGN_OPTS, "sign");
|
|
532
574
|
// RFC 9788 header protection (opts.protectHeaders): the inner Cryptographic Payload gains hp="clear" +
|
|
533
575
|
// the inlined protected fields, and the outer frame carries the display copies. Off by default => the
|
|
534
576
|
// shipped path, byte-for-byte.
|
|
@@ -624,6 +666,7 @@ function _capped(msg) {
|
|
|
624
666
|
*/
|
|
625
667
|
async function verify(message, opts) {
|
|
626
668
|
opts = opts || {};
|
|
669
|
+
_knownOpts(opts, VERIFY_OPTS, "verify");
|
|
627
670
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
628
671
|
var ct = ent.contentType;
|
|
629
672
|
// Forwarded, not re-decided here. This verb documents itself as pki.cms.verify's verdict plus the
|
|
@@ -762,6 +805,7 @@ function _cmsEncryptOpts(opts) {
|
|
|
762
805
|
*/
|
|
763
806
|
async function encrypt(content, recipients, opts) {
|
|
764
807
|
opts = opts || {};
|
|
808
|
+
_knownOpts(opts, ENCRYPT_OPTS, "encrypt");
|
|
765
809
|
// RFC 9788 header protection (opts.protectHeaders): the inner payload carries hp="cipher" + the REAL
|
|
766
810
|
// headers (inside the ciphertext); the outer frame carries only the Header-Confidentiality-Policy-processed
|
|
767
811
|
// display copies (hcp_baseline obscures Subject to [...], removes Comments/Keywords). Off => shipped path.
|
|
@@ -824,6 +868,7 @@ async function encrypt(content, recipients, opts) {
|
|
|
824
868
|
*/
|
|
825
869
|
async function decrypt(message, keyMaterial, opts) {
|
|
826
870
|
opts = opts || {};
|
|
871
|
+
_knownOpts(opts, DECRYPT_OPTS, "decrypt");
|
|
827
872
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
828
873
|
var ct = ent.contentType;
|
|
829
874
|
if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not an encrypted S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
|
|
@@ -870,6 +915,7 @@ async function decrypt(message, keyMaterial, opts) {
|
|
|
870
915
|
*/
|
|
871
916
|
async function compress(content, opts) {
|
|
872
917
|
opts = opts || {};
|
|
918
|
+
_knownOpts(opts, COMPRESS_OPTS, "compress");
|
|
873
919
|
var entity = _entityBytes(content, opts);
|
|
874
920
|
var cOpts = {};
|
|
875
921
|
if (opts.level !== undefined) cOpts.level = opts.level;
|
|
@@ -903,6 +949,7 @@ async function compress(content, opts) {
|
|
|
903
949
|
*/
|
|
904
950
|
async function decompress(message, opts) {
|
|
905
951
|
opts = opts || {};
|
|
952
|
+
_knownOpts(opts, DECOMPRESS_OPTS, "decompress");
|
|
906
953
|
var ent = mime.parse(message, SmimeError, "smime/bad-mime");
|
|
907
954
|
var ct = ent.contentType;
|
|
908
955
|
if (!_isPkcs7(ct.type, "mime")) throw _err("smime/unsupported-type", "not a compressed S/MIME message (Content-Type " + JSON.stringify(ct.type) + ")");
|
package/lib/trust.js
CHANGED
|
@@ -289,7 +289,7 @@ function _trustEntry(obj) {
|
|
|
289
289
|
|
|
290
290
|
function _mkAnchor(cert, meta) {
|
|
291
291
|
var spki = cert.subjectPublicKeyInfo;
|
|
292
|
-
|
|
292
|
+
var entry = {
|
|
293
293
|
name: cert.subject, // the object with .rdns (name chaining)
|
|
294
294
|
publicKey: spki.bytes, // the full SPKI SEQUENCE TLV
|
|
295
295
|
algorithm: spki.algorithm.oid, // the SPKI public-key algorithm OID
|
|
@@ -300,6 +300,76 @@ function _mkAnchor(cert, meta) {
|
|
|
300
300
|
label: meta.label,
|
|
301
301
|
mozillaCaPolicy: meta.mozillaCaPolicy,
|
|
302
302
|
};
|
|
303
|
+
// A trust anchor IS the pair (name, key) -- RFC 5280 sec. 6.1.1 -- so those two fields are one
|
|
304
|
+
// fact about one certificate, and they were derived here from one. `anchor()` re-derives them
|
|
305
|
+
// rather than reading them back, so an entry rebuilt with a substituted key cannot carry the
|
|
306
|
+
// store's NAME and its per-purpose trust metadata over to a key the store never vouched for.
|
|
307
|
+
// Recorded off the object, so rebuilding the entry loses the record along with the binding.
|
|
308
|
+
// COPIED, not aliased. The record and the entry would otherwise hold the same Buffer and the same
|
|
309
|
+
// name object, so overwriting the entry's publicKey in place -- otherSpki.copy(entry.publicKey)
|
|
310
|
+
// for an equal-length key -- would overwrite the record with it, and re-deriving would hand back
|
|
311
|
+
// exactly the substituted key. A record that changes with the thing it is meant to pin is not one.
|
|
312
|
+
//
|
|
313
|
+
// The store's METADATA is recorded with them, and for the stronger reason: `purposes` IS the
|
|
314
|
+
// authorization -- it is what the purpose gate reads to decide whether this root may vouch for
|
|
315
|
+
// TLS -- and `distrustAfter` is the date that authorization ends. Pinning the key while reading
|
|
316
|
+
// the authorization off the mutable entry pins the less important half: `entry.purposes.serverAuth
|
|
317
|
+
// = true` would then produce a server-auth anchor from a root the store marked for e-mail only,
|
|
318
|
+
// and deleting a distrust date would outlast the store's own policy. Everything the anchor asserts
|
|
319
|
+
// now comes from what the store read.
|
|
320
|
+
_DERIVED_FROM.set(entry, {
|
|
321
|
+
// The RDN entries are copied too, not just the arrays holding them: a shallow slice shares
|
|
322
|
+
// every attribute object, so editing one in place would still reach the record.
|
|
323
|
+
name: _copyName(cert.subject),
|
|
324
|
+
publicKey: Buffer.from(spki.bytes),
|
|
325
|
+
algorithm: spki.algorithm.oid,
|
|
326
|
+
parameters: spki.algorithm.parameters == null ? spki.algorithm.parameters : Buffer.from(spki.algorithm.parameters),
|
|
327
|
+
purposes: _copyPurposes(meta.purposes),
|
|
328
|
+
distrustAfter: _copyDistrustAfter(meta.distrustAfter),
|
|
329
|
+
});
|
|
330
|
+
return entry;
|
|
331
|
+
}
|
|
332
|
+
var _DERIVED_FROM = new WeakMap();
|
|
333
|
+
|
|
334
|
+
// The per-purpose distrust dates, with fresh Date objects. A Date is mutable, so handing back the
|
|
335
|
+
// entry's own would let a consumer move the date this anchor is judged against.
|
|
336
|
+
// The three trust bits, normalized to booleans. Read by the purpose gate and handed back on every
|
|
337
|
+
// anchor, so it is built fresh from whichever source is authoritative rather than shared.
|
|
338
|
+
function _copyPurposes(src) {
|
|
339
|
+
return {
|
|
340
|
+
serverAuth: !!src && src.serverAuth === true,
|
|
341
|
+
emailProtection: !!src && src.emailProtection === true,
|
|
342
|
+
codeSigning: !!src && src.codeSigning === true,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function _copyDistrustAfter(src) {
|
|
347
|
+
var out = {};
|
|
348
|
+
if (!src || typeof src !== "object") return out;
|
|
349
|
+
Object.keys(src).forEach(function (k) {
|
|
350
|
+
out[k] = src[k] instanceof Date ? new Date(src[k].getTime()) : src[k];
|
|
351
|
+
});
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// A parsed Name, copied FAITHFULLY: every field the parser assigns, at every level, with nothing
|
|
356
|
+
// mutable shared. Used on both sides of the record -- writing it in and handing it out -- so
|
|
357
|
+
// neither direction shares an object with the caller.
|
|
358
|
+
//
|
|
359
|
+
// Faithful rather than "the fields the path validator reads", because this Name is handed back on
|
|
360
|
+
// pki.trust.anchor and is the same structure pki.schema.x509.parse produces. A copy that keeps the
|
|
361
|
+
// subset one consumer needs silently degrades it for every other reader -- an attribute would lose
|
|
362
|
+
// its registry `name`, so a caller walking anchor.name.rdns would see a different shape depending
|
|
363
|
+
// on whether the anchor came from a store entry or straight from the parser. Each attribute value
|
|
364
|
+
// is a string and so needs no copy of its own; the DER does.
|
|
365
|
+
function _copyName(name) {
|
|
366
|
+
if (!name || !Array.isArray(name.rdns)) return name;
|
|
367
|
+
var out = Object.assign({}, name);
|
|
368
|
+
out.rdns = name.rdns.map(function (rdn) {
|
|
369
|
+
return Array.isArray(rdn) ? rdn.map(function (atv) { return Object.assign({}, atv); }) : rdn;
|
|
370
|
+
});
|
|
371
|
+
out.bytes = Buffer.isBuffer(name.bytes) ? Buffer.from(name.bytes) : name.bytes;
|
|
372
|
+
return out;
|
|
303
373
|
}
|
|
304
374
|
|
|
305
375
|
function _datesEqual(x, y) {
|
|
@@ -700,26 +770,67 @@ function parseCcadbCsv(text) {
|
|
|
700
770
|
* { time: new Date("2026-06-01T00:00:00Z"), trustAnchor: anchor, checkPurpose: "serverAuth" });
|
|
701
771
|
*/
|
|
702
772
|
function anchor(entry, opts) {
|
|
703
|
-
if (!entry || typeof entry !== "object"
|
|
704
|
-
typeof entry.algorithm !== "string" || !entry.name || !Array.isArray(entry.name.rdns)) {
|
|
773
|
+
if (!entry || typeof entry !== "object") {
|
|
705
774
|
throw E("trust/bad-input", "anchor expects a trust-store entry ({ name, publicKey, algorithm, ... })");
|
|
706
775
|
}
|
|
707
776
|
opts = opts || {};
|
|
777
|
+
// Where this entry came from a store, everything the anchor asserts is read from the record the
|
|
778
|
+
// store minted, not from the entry -- the entry is a plain object the caller holds and can write
|
|
779
|
+
// to. `purposes` is the authorization itself, so an entry whose serverAuth bit was flipped after
|
|
780
|
+
// parsing must not open the gate, and a distrust date deleted from it must not outlive the
|
|
781
|
+
// store's policy.
|
|
782
|
+
var derived = _DERIVED_FROM.get(entry);
|
|
783
|
+
var meta = derived || entry;
|
|
784
|
+
// The tuple-shape check runs on the ENTRY only when there is no record, because only then are the
|
|
785
|
+
// entry's own fields what the anchor is built from. With a record, an entry a caller has since
|
|
786
|
+
// emptied still anchors to exactly what the store read: the answer does not depend on the object
|
|
787
|
+
// the caller is holding, in either direction. Without one, the caller is asserting a bare
|
|
788
|
+
// (name, key) anchor of their own and its shape is what there is to check.
|
|
789
|
+
if (!derived && (!Buffer.isBuffer(entry.publicKey) || typeof entry.algorithm !== "string" ||
|
|
790
|
+
!entry.name || !Array.isArray(entry.name.rdns))) {
|
|
791
|
+
throw E("trust/bad-input", "anchor expects a trust-store entry ({ name, publicKey, algorithm, ... })");
|
|
792
|
+
}
|
|
708
793
|
if (opts.purpose !== undefined) {
|
|
709
794
|
if (PURPOSES.indexOf(opts.purpose) === -1) {
|
|
710
795
|
throw E("trust/bad-input", "anchor: opts.purpose must be one of " + PURPOSES.join(" | "));
|
|
711
796
|
}
|
|
712
|
-
if (!
|
|
797
|
+
if (!meta.purposes || meta.purposes[opts.purpose] !== true) {
|
|
713
798
|
throw E("trust/purpose-not-trusted", "this root is not a trusted delegator for " + opts.purpose);
|
|
714
799
|
}
|
|
715
800
|
}
|
|
801
|
+
// Where this entry came from a store, the (name, key) pair is re-derived from the certificate it
|
|
802
|
+
// was read out of rather than read back off the entry. Both halves are one fact about one
|
|
803
|
+
// certificate, so an entry rebuilt with a substituted publicKey would otherwise carry the store's
|
|
804
|
+
// name and its per-purpose trust metadata onto a key the store never vouched for. An entry a
|
|
805
|
+
// caller built themselves has no such record and is their own assertion, which is what a bare
|
|
806
|
+
// trust-anchor tuple is.
|
|
807
|
+
//
|
|
808
|
+
// An entry carrying the store's METADATA is claiming to be a store entry. The metadata is the
|
|
809
|
+
// root program's statement -- these purposes, until this date -- and it is a statement about a
|
|
810
|
+
// KEY. Without the record there is nothing binding it to the key the entry now names, so a copy
|
|
811
|
+
// with a substituted publicKey would carry the program's word onto a key it never saw. A caller
|
|
812
|
+
// asserting a bare (name, key) anchor of their own carries no metadata and is unaffected.
|
|
813
|
+
if (!derived && (entry.purposes != null || (entry.distrustAfter && Object.keys(entry.distrustAfter).length))) {
|
|
814
|
+
throw E("trust/bad-input", "this entry carries a trust store's per-purpose metadata but is not the entry the store produced -- it has been rebuilt, and the metadata is a statement about the key the store read, not about whichever key the copy now names. Pass pki.trust.parseCertdata / parseCcadbCsv output unmodified");
|
|
815
|
+
}
|
|
816
|
+
// Copies OUT as well as in. Handing back the record's own Buffer lets a consumer write through
|
|
817
|
+
// the returned anchor into the record -- copy an equal-length SPKI over anchor.publicKey and the
|
|
818
|
+
// next anchor() call re-derives the substituted key while still carrying the store's purposes and
|
|
819
|
+
// distrust dates. Guarding only the write INTO the record leaves the same door open in the other
|
|
820
|
+
// direction, so every call hands back fresh values.
|
|
716
821
|
return {
|
|
717
|
-
name:
|
|
718
|
-
publicKey:
|
|
719
|
-
algorithm:
|
|
720
|
-
parameters:
|
|
721
|
-
|
|
722
|
-
|
|
822
|
+
name: _copyName(meta.name),
|
|
823
|
+
publicKey: Buffer.isBuffer(meta.publicKey) ? Buffer.from(meta.publicKey) : meta.publicKey,
|
|
824
|
+
algorithm: meta.algorithm,
|
|
825
|
+
parameters: Buffer.isBuffer(meta.parameters) ? Buffer.from(meta.parameters)
|
|
826
|
+
: (meta.parameters !== undefined ? meta.parameters : null),
|
|
827
|
+
// The METADATA comes from the same source as the key, and is copied for the same reason on the
|
|
828
|
+
// way out: handing back the record's own object let a consumer write
|
|
829
|
+
// `anchor(entry).purposes.serverAuth = true` and have the next call pass a gate the store never
|
|
830
|
+
// opened. `distrustAfter` holds Dates, which are mutable in the same way. Everything on this
|
|
831
|
+
// object is fresh, so two anchors from one entry share nothing.
|
|
832
|
+
distrustAfter: _copyDistrustAfter(meta.distrustAfter),
|
|
833
|
+
purposes: _copyPurposes(meta.purposes),
|
|
723
834
|
};
|
|
724
835
|
}
|
|
725
836
|
|
package/lib/tsp-sign.js
CHANGED
|
@@ -23,11 +23,12 @@ var pathValidate = require("./path-validate");
|
|
|
23
23
|
var pkiX509 = require("./schema-x509");
|
|
24
24
|
var smime = require("./schema-smime");
|
|
25
25
|
var schemaTsp = require("./schema-tsp");
|
|
26
|
-
var schema = require("./schema-engine");
|
|
27
26
|
var guard = require("./guard-all");
|
|
28
27
|
var frameworkError = require("./framework-error");
|
|
29
28
|
|
|
29
|
+
var pkix = require("./schema-pkix");
|
|
30
30
|
var TspError = frameworkError.TspError;
|
|
31
|
+
var _NS = pkix.makeNS("tsp", TspError, oid);
|
|
31
32
|
var b = asn1.build;
|
|
32
33
|
function _err(code, message, cause) { return new TspError(code, message, cause); }
|
|
33
34
|
function O(name) { return oid.byName(name); }
|
|
@@ -484,19 +485,12 @@ function _checkTsaCertUsage(tsaCertDer) {
|
|
|
484
485
|
// A keyUsage that forbids signing cannot mint a token (RFC 5280 sec. 4.2.1.3); an absent keyUsage
|
|
485
486
|
// is unrestricted. Require digitalSignature (bit 0) or nonRepudiation/contentCommitment (bit 1) --
|
|
486
487
|
// the signing bits, the TSA analogue of the OCSP-responder keyUsage gate.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// bits) that the shared certExtensionDecoders keyUsage decoder applies, so a non-minimal
|
|
494
|
-
// encoding other paths reject cannot slip a "permits signing" verdict through here.
|
|
495
|
-
schema.assertMinimalNamedBits(ku.unusedBits, ku.bytes, function (m) { throw _err("tsp/bad-key-usage", m); });
|
|
496
|
-
} catch (_e) { return "tsp/bad-key-usage"; }
|
|
497
|
-
var byte0 = ku.bytes.length ? ku.bytes[0] : 0;
|
|
498
|
-
if (!((byte0 >> 7) & 1) && !((byte0 >> 6) & 1)) return "tsp/bad-key-usage"; // no digitalSignature / nonRepudiation
|
|
499
|
-
}
|
|
488
|
+
// Through the shared reader, which applies the NamedBitList rules (X.690 sec. 11.2.2 minimal
|
|
489
|
+
// encoding, sec. 4.2.1.3 at least one bit set) this boundary was applying only half of.
|
|
490
|
+
var ku;
|
|
491
|
+
try { ku = pkix.keyUsageOf(_NS, cert, _err, "tsp/bad-key-usage", "TSA certificate"); }
|
|
492
|
+
catch (_e) { return "tsp/bad-key-usage"; }
|
|
493
|
+
if (ku && !ku.digitalSignature && !ku.nonRepudiation) return "tsp/bad-key-usage";
|
|
500
494
|
return true;
|
|
501
495
|
}
|
|
502
496
|
|
|
@@ -570,7 +564,7 @@ function _buildTsaChains(leaf, pool) {
|
|
|
570
564
|
* PEM (never a parsed object -- every checked field is read from the CMS-verified eContent, so a
|
|
571
565
|
* mutated parsed structure cannot desynchronize the checks from the signed bytes). `data` is the
|
|
572
566
|
* original bytes (hashed under the token's messageImprint algorithm) or a precomputed
|
|
573
|
-
* `{ hashAlgorithm, hashedMessage }`. Returns `{ valid, genTime, accuracy, serialNumber,
|
|
567
|
+
* `{ hashAlgorithm, hashedMessage }`. Returns `{ valid, trusted, genTime, accuracy, serialNumber,
|
|
574
568
|
* serialNumberHex, policy, nonce, tsa, tstInfo, signer, code?, reason? }`. `valid` is true only
|
|
575
569
|
* when the CMS signature, the imprint match, the eContentType, the ESSCertID(V2) binding, the
|
|
576
570
|
* RFC 3161 sec. 2.3 critical single-`timeStamping` extendedKeyUsage rule, the requested nonce (when
|
|
@@ -578,6 +572,13 @@ function _buildTsaChains(leaf, pool) {
|
|
|
578
572
|
* validation all pass. A conformance / trust failure of a well-formed token is a
|
|
579
573
|
* `{ valid:false, code }` verdict; malformed or config input throws a typed `TspError`.
|
|
580
574
|
*
|
|
575
|
+
* `trusted` is the second claim and is kept apart from the first. `valid` says the token's
|
|
576
|
+
* signature and structural bindings hold; `trusted` says the timestamp authority chained to an
|
|
577
|
+
* anchor this caller named. Without `trustAnchor` there is nothing to chain to and `trusted` is
|
|
578
|
+
* `false` -- a definite answer rather than a missing one, on the refusal branch as well as the
|
|
579
|
+
* accepting one. A timestamp is archived precisely to be re-read years later, and one boolean
|
|
580
|
+
* cannot answer both questions then.
|
|
581
|
+
*
|
|
581
582
|
* @opts trustAnchor Anchor `{ name, publicKey, algorithm }` -- runs `pki.path.validate` on the
|
|
582
583
|
* TSA certificate chain ordered from the token's embedded certificates
|
|
583
584
|
* (validity at genTime, requiredEku timeStamping, revocation), so a TSA under
|
|
@@ -625,8 +626,12 @@ async function verify(token, data, opts) {
|
|
|
625
626
|
// structure and decodes the TSTInfo FROM the raw eContent; a structural defect throws.
|
|
626
627
|
var parsed = schemaTsp.parseToken(tokenDer);
|
|
627
628
|
var tst = parsed.tstInfo;
|
|
629
|
+
// `trusted: false` on every refusal too, not only on the accepting return. A caller reading
|
|
630
|
+
// `res.trusted` must get an answer on both branches -- an undefined on the failure path is the
|
|
631
|
+
// same "cannot tell what was checked" the field was added to remove, and `!res.trusted` reading
|
|
632
|
+
// true by accident is not the same as its reading true because nothing anchored the TSA.
|
|
628
633
|
function fail(code, reason) {
|
|
629
|
-
return { valid: false, code: code, reason: reason || null, genTime: tst.genTime, accuracy: tst.accuracy, serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex, policy: tst.policy, nonce: tst.nonce, tsa: tst.tsa, tstInfo: tst, signer: null };
|
|
634
|
+
return { valid: false, trusted: false, code: code, reason: reason || null, genTime: tst.genTime, accuracy: tst.accuracy, serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex, policy: tst.policy, nonce: tst.nonce, tsa: tst.tsa, tstInfo: tst, signer: null };
|
|
630
635
|
}
|
|
631
636
|
// M12 -- the CMS signature over the exact RFC 5652 sec. 5.4 preimage (message-digest bound to the
|
|
632
637
|
// authenticated eContent). cms.verify re-parses the same bytes; a failure is a fail-closed verdict.
|
|
@@ -673,6 +678,14 @@ async function verify(token, data, opts) {
|
|
|
673
678
|
// key-param inheritance, requiredEku, optional revocation), only when a trustAnchor is supplied.
|
|
674
679
|
// The path is ordered from the token's embedded certificates (leaf + any intermediates), so a TSA
|
|
675
680
|
// issued under an intermediate CA -- not just directly under the anchor -- validates.
|
|
681
|
+
// `trusted` is the SECOND claim, kept apart from `valid`. `valid` says the token's signature and
|
|
682
|
+
// its structural bindings hold; whether the timestamp authority is one this caller accepts is
|
|
683
|
+
// answered only by the chain below, and only when an anchor was supplied. Collapsing the two into
|
|
684
|
+
// one boolean meant an archived verdict could not be re-read to tell whether the TSA was ever
|
|
685
|
+
// trusted -- which is exactly what a timestamp is archived to answer. Without an anchor there is
|
|
686
|
+
// nothing to chain to and `trusted` is false: a definite answer rather than a missing one, the
|
|
687
|
+
// same shape pki.cms.verify and pki.cmp.verify return.
|
|
688
|
+
var trusted = false;
|
|
676
689
|
if (opts.trustAnchor) {
|
|
677
690
|
var pathRes = null;
|
|
678
691
|
// tst.genTime floors to millisecond precision. When genTime carries sub-millisecond digits the true
|
|
@@ -693,22 +706,30 @@ async function verify(token, data, opts) {
|
|
|
693
706
|
// path.validate validates a FIXED path, so backtracking over same-subject issuer candidates
|
|
694
707
|
// happens here -- accept the TSA certificate if ANY enumerated chain validates to the anchor at
|
|
695
708
|
// both window endpoints.
|
|
709
|
+
//
|
|
710
|
+
// checkPurpose names timeStamping alongside requiredEku, and the pairing is the point. The EKU
|
|
711
|
+
// constrains the TSA CERTIFICATE; checkPurpose selects the ANCHOR's own trust metadata, which
|
|
712
|
+
// pki.path consults only when a purpose is named -- so asking one without the other checks one
|
|
713
|
+
// end of the chain and not the other, and a root explicitly distrusted for timestamping would
|
|
714
|
+
// still answer trusted. The purpose is not a caller choice here: this verb validates timestamp
|
|
715
|
+
// tokens and nothing else, so there is exactly one purpose its anchors can be judged under.
|
|
696
716
|
var chains = _buildTsaChains(pkiX509.parse(tsaCertDer), pool);
|
|
697
717
|
for (var ci = 0; ci < chains.length && !(pathRes && pathRes.valid); ci++) {
|
|
698
718
|
pathRes = await pathValidate.validate(chains[ci], {
|
|
699
|
-
time: floorT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], revocationChecker: opts.revocationChecker,
|
|
719
|
+
time: floorT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
|
|
700
720
|
});
|
|
701
721
|
if (pathRes.valid && ceilT !== floorT) {
|
|
702
722
|
pathRes = await pathValidate.validate(chains[ci], {
|
|
703
|
-
time: ceilT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], revocationChecker: opts.revocationChecker,
|
|
723
|
+
time: ceilT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], checkPurpose: "timeStamping", revocationChecker: opts.revocationChecker,
|
|
704
724
|
});
|
|
705
725
|
}
|
|
706
726
|
}
|
|
707
727
|
} catch (e) { return fail("tsp/untrusted-tsa", (e && e.message) || String(e)); }
|
|
708
728
|
if (!pathRes || !pathRes.valid) return fail("tsp/untrusted-tsa", "the TSA certificate did not validate to the trust anchor at genTime");
|
|
729
|
+
trusted = true;
|
|
709
730
|
}
|
|
710
731
|
return {
|
|
711
|
-
valid: true, genTime: tst.genTime, accuracy: tst.accuracy,
|
|
732
|
+
valid: true, trusted: trusted, genTime: tst.genTime, accuracy: tst.accuracy,
|
|
712
733
|
serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex,
|
|
713
734
|
policy: tst.policy, policyName: tst.policyName, nonce: tst.nonce, tsa: tst.tsa,
|
|
714
735
|
tstInfo: tst, signer: { cert: tsaCertDer, sid: signer.sid },
|