@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/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
- var parse = pkix.makeParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS });
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
@@ -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
- var parseResponse = pkix.makeParser({ pemLabel: "OCSP RESPONSE", PemError: PemError, ErrorClass: OcspError, prefix: "ocsp", what: "OCSP response", topSchema: OCSP_RESPONSE, ns: NS });
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
@@ -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
- var parse = pkix.makeParser({
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
@@ -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,
@@ -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
- var parse = pkix.makeParser({ pemLabel: "CERTIFICATE", PemError: PemError, ErrorClass: CertificateError, prefix: "x509", what: "certificate", topSchema: CERTIFICATE, ns: NS });
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
@@ -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
- var der;
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") { try { der = pkcs8.pemDecode(key); } catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); } }
220
- else throw E("bad-input", "a signer key must be a CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
221
- return subtle.importKey("pkcs8", der, imp, false, ["sign"]);
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
- return {
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" || !Buffer.isBuffer(entry.publicKey) ||
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 (!entry.purposes || entry.purposes[opts.purpose] !== true) {
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: entry.name,
718
- publicKey: entry.publicKey,
719
- algorithm: entry.algorithm,
720
- parameters: entry.parameters !== undefined ? entry.parameters : null,
721
- distrustAfter: entry.distrustAfter || {},
722
- purposes: entry.purposes || { serverAuth: false, emailProtection: false, codeSigning: false },
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