@blamejs/pki 0.5.4 → 0.5.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 +42 -0
- package/MIGRATING.md +72 -0
- package/lib/acme.js +47 -19
- 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-sign.js +11 -0
- package/lib/cms-verify.js +22 -8
- package/lib/crl-sign.js +19 -7
- package/lib/est.js +77 -1
- package/lib/guard-all.js +8 -0
- package/lib/guard-async.js +37 -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 +482 -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 +44 -8
- package/lib/path-validate.js +88 -44
- package/lib/pbes2.js +11 -1
- package/lib/pkcs12-build.js +105 -30
- package/lib/pki-build.js +12 -1
- package/lib/schema-cms.js +17 -2
- 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 +14 -0
- package/lib/schema-x509.js +14 -1
- package/lib/sign-scheme.js +22 -5
- package/lib/sigstore.js +10 -0
- package/lib/smime.js +47 -0
- package/lib/trust.js +121 -10
- package/lib/tsp-sign.js +112 -19
- package/lib/validator-cose.js +86 -1
- package/lib/validator-tpm.js +8 -3
- package/lib/webauthn-mds.js +10 -18
- 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-crl.js
CHANGED
|
@@ -217,7 +217,13 @@ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
|
|
|
217
217
|
* var crl = pki.schema.crl.parse(der);
|
|
218
218
|
* crl.revokedCertificates[0].serialNumberHex; // -> "0a3f"
|
|
219
219
|
*/
|
|
220
|
-
|
|
220
|
+
// Recording, for the same reason the certificate parser is: a CRL is one signature over one byte
|
|
221
|
+
// range, and a parsed CRL presents that range and the revocation list it encodes as separate
|
|
222
|
+
// properties. Keep a correctly signed CRL's `tbsBytes` and signature and empty `revokedCertificates`
|
|
223
|
+
// and the signature still verifies while the revocation answer comes from the edited list -- a
|
|
224
|
+
// revoked certificate reported as good. The scope fields (`crlExtensions`, an IDP, a delta
|
|
225
|
+
// indicator) fail the same way. The verdict verbs re-parse from what is recorded here.
|
|
226
|
+
var parse = pkix.makeRecordingParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS }, "crl");
|
|
221
227
|
|
|
222
228
|
/**
|
|
223
229
|
* @primitive pki.schema.crl.pemDecode
|
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
|
@@ -1556,6 +1556,19 @@ function makeParser(opts) {
|
|
|
1556
1556
|
return function (input) { return runParse(input, opts); };
|
|
1557
1557
|
}
|
|
1558
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
|
+
|
|
1559
1572
|
// The X.509 SIGNED{ToBeSigned} macro (RFC 5280 sec. 4.1.1.3): the outer
|
|
1560
1573
|
// SEQUENCE { toBeSigned, signatureAlgorithm AlgorithmIdentifier,
|
|
1561
1574
|
// signatureValue BIT STRING } shared by Certificate, CertificateList and
|
|
@@ -1633,6 +1646,7 @@ module.exports = {
|
|
|
1633
1646
|
pbmac1Params: pbmac1Params,
|
|
1634
1647
|
spki: spki,
|
|
1635
1648
|
makeParser: makeParser,
|
|
1649
|
+
makeRecordingParser: makeRecordingParser,
|
|
1636
1650
|
signedEnvelopeTbs: signedEnvelopeTbs,
|
|
1637
1651
|
rootSequenceChildren: rootSequenceChildren,
|
|
1638
1652
|
assertPolicyQualifiers: assertPolicyQualifiers,
|
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/sigstore.js
CHANGED
|
@@ -522,6 +522,9 @@ var IDENTITY_FIELDS = ["san", "issuer", "sourceRepositoryURI"];
|
|
|
522
522
|
// The guard tests membership with hasOwnProperty, so the permitted set is a lookup object --
|
|
523
523
|
// an array would treat "0"/"1" as the known keys and reject every real field name.
|
|
524
524
|
var IDENTITY_KEYS = { san: 1, issuer: 1, sourceRepositoryURI: 1 };
|
|
525
|
+
// Every option pki.sigstore.verifyBundle reads. Adding one here is the only way to make it
|
|
526
|
+
// accepted, so a capability cannot arrive with its option silently ignored at this boundary.
|
|
527
|
+
var _VERIFY_BUNDLE_OPTS = { fulcioRoots: 1, rekorKeys: 1, identity: 1, predicateType: 1, time: 1 };
|
|
525
528
|
|
|
526
529
|
// Returns WHICH fields were actually compared. A bundle verifies its own signature and log
|
|
527
530
|
// inclusion whoever signed it -- Fulcio issues to anyone who completes an OIDC flow -- so
|
|
@@ -633,6 +636,13 @@ async function verifyBundle(bundle, opts) {
|
|
|
633
636
|
throw new TypeError("verifyBundle: bundle must be an object, JSON string, or Buffer");
|
|
634
637
|
}
|
|
635
638
|
opts = opts || {};
|
|
639
|
+
// An unrecognized option is refused, not swallowed. The identity policy one level down already
|
|
640
|
+
// closes this door for exactly the reason it needs closing here too: cosign spells the signer pin
|
|
641
|
+
// `certificateIdentity`, and a swallowed spelling checks nothing under a name the operator
|
|
642
|
+
// believes pins the signer. At the top level the same slip loses the SLSA predicate pin as well,
|
|
643
|
+
// and `predicateType` has no `identityChecked`-style field, so nothing in the verdict reveals
|
|
644
|
+
// that the pin never ran.
|
|
645
|
+
guard.identifier.assertKnownKeys(opts, _VERIFY_BUNDLE_OPTS, _err, "sigstore/bad-input", "pki.sigstore.verifyBundle has an unknown option ");
|
|
636
646
|
var b = parseBundle(bundle);
|
|
637
647
|
var vm = b.verificationMaterial;
|
|
638
648
|
var env = b.dsseEnvelope;
|
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
|
|