@blamejs/pki 0.8.24 → 0.8.26

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/oid.js CHANGED
@@ -180,7 +180,8 @@ var FAMILIES = Object.assign(Object.create(null), {
180
180
 
181
181
  pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
182
182
  serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9,
183
- secureShellClient: 21, secureShellServer: 22, cmcCA: 27, cmcRA: 28, cmcArchive: 29, cmKGA: 32, bundleSecurity: 35 } },
183
+ secureShellClient: 21, secureShellServer: 22, cmcCA: 27, cmcRA: 28, cmcArchive: 29, cmKGA: 32,
184
+ brandIndicatorforMessageIdentification: 31, bundleSecurity: 35 } },
184
185
 
185
186
  pkinitKp: { base: [1, 3, 6, 1, 5, 2, 3], of: { pkinitClientAuth: 4, pkinitKdc: 5 } },
186
187
 
@@ -607,6 +608,16 @@ var _PARAMS_ABSENT = new Set();
607
608
  "Ed25519", "Ed448", "X25519", "X448",
608
609
  "id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024",
609
610
  "hkdfWithSha256", "hkdfWithSha384", "hkdfWithSha512",
611
+ /** @internal RFC 3279 sec. 2.2.3: "When the ecdsa-with-SHA1 algorithm identifier appears as the
612
+ * algorithm field in an AlgorithmIdentifier, the encoding MUST omit the parameters field", and
613
+ * sec. 2.2.2 says SHALL for id-dsa-with-sha1. RFC 5758 sec. 3.2 repeats it for the SHA-2 set: "the
614
+ * encoding MUST omit the parameters field". Unlike the `*WithRSAEncryption` identifiers, which
615
+ * RFC 4055 requires to CARRY a NULL, these omit the field entirely. */
616
+ "ecdsaWithSHA1", "ecdsaWithSHA256", "ecdsaWithSHA384", "ecdsaWithSHA512", "dsaWithSha1",
617
+ /** @internal RFC 8702 sec. 2: "when using the id-shake128 or id-shake256 algorithm identifiers, the
618
+ * parameters MUST be absent", sec. 3.1 for the CMS digest fields, and sec. 3.2 for the two
619
+ * RSASSA-PSS-SHAKE signature identifiers. */
620
+ "shake128", "shake256", "id-RSASSA-PSS-SHAKE128", "id-RSASSA-PSS-SHAKE256",
610
621
  "id-alg-hss-lms-hashsig", "id-alg-xmss-hashsig", "id-alg-xmssmt-hashsig",
611
622
  "id-MLDSA44-RSA2048-PSS-SHA256", "id-MLDSA44-RSA2048-PKCS15-SHA256",
612
623
  "id-MLDSA44-Ed25519-SHA512", "id-MLDSA44-ECDSA-P256-SHA256",
@@ -458,7 +458,14 @@ function emailMatch(constraint, mailbox) {
458
458
  if (constraint.indexOf("@") !== -1) {
459
459
  var cb = splitMailbox(constraint);
460
460
  if (cb === "ambiguous" || cb === null || mb === null) return "unsupported";
461
- return mb[0] === cb[0] && stripTrailingDot(mb[1].toLowerCase()) === stripTrailingDot(cb[1].toLowerCase());
461
+ /** @internal A constraint naming a whole mailbox asks the RFC 5280 sec. 7.5 question, so it is
462
+ * asked of the one place that holds that rule. A trailing dot on either domain is removed first,
463
+ * since sec. 4.2.1.10 compares a host and `a.example` and `a.example.` are one host. */
464
+ var verdict = guard.name.emailEqual(mb[0] + "@" + stripTrailingDot(mb[1]),
465
+ cb[0] + "@" + stripTrailingDot(cb[1]));
466
+ if (verdict === "match") return true;
467
+ if (verdict === "no-match") return false;
468
+ return "unsupported";
462
469
  }
463
470
  if (mb === null) return "unsupported";
464
471
  var host = stripTrailingDot(mb[1].toLowerCase());
@@ -119,9 +119,30 @@ var HOLDER = schema.seq([
119
119
  ], { minTag: 0, maxTag: 2, unexpectedCode: "attrcert/bad-holder", orderCode: "attrcert/bad-holder" }),
120
120
  ], {
121
121
  assert: "sequence", code: "attrcert/bad-holder", what: "Holder",
122
- build: function (m) {
122
+ /** @internal Sec. 4.2.2 states three requirements for the holder's baseCertificateID issuer that
123
+ * sec. 4.2.3 states in the same words for the AC issuer: "The PKC issuer MUST have a non-empty
124
+ * distinguished name that is to be present as the single value of the
125
+ * holder.baseCertificateID.issuer construct in the directoryName field." V2_FORM's build enforces
126
+ * the AC issuer's copy, so the holder's is enforced here. It is checked in THIS build rather than
127
+ * in ISSUER_SERIAL, because that schema also reads a targetInformation targetCertificate, which
128
+ * sec. 4.2.2 says nothing about. */
129
+ build: function (m, ctx) {
130
+ var base = m.fields.baseCertificateID.present ? m.fields.baseCertificateID.value.result : null;
131
+ if (base !== null) {
132
+ var names = base.issuer.names;
133
+ if (names.length !== 1) {
134
+ throw ctx.E("attrcert/bad-holder", "the baseCertificateID issuer must contain one and only one GeneralName (RFC 5755 sec. 4.2.2)");
135
+ }
136
+ if (names[0].tagNumber !== 4) {
137
+ throw ctx.E("attrcert/bad-holder", "the baseCertificateID issuer GeneralName must be a directoryName [4] (RFC 5755 sec. 4.2.2)");
138
+ }
139
+ var dirName = pkix.decodeNested(names[0].bytes, ctx, "the baseCertificateID issuer directoryName").children[0];
140
+ if (!dirName.children || dirName.children.length < 1) {
141
+ throw ctx.E("attrcert/bad-holder", "the baseCertificateID issuer directoryName must be a non-empty distinguished name (RFC 5755 sec. 4.2.2)");
142
+ }
143
+ }
123
144
  return {
124
- baseCertificateID: m.fields.baseCertificateID.present ? m.fields.baseCertificateID.value.result : null,
145
+ baseCertificateID: base,
125
146
  entityName: m.fields.entityName.present ? m.fields.entityName.value.result : null,
126
147
  objectDigestInfo: m.fields.objectDigestInfo.present ? m.fields.objectDigestInfo.value.result : null,
127
148
  };
@@ -605,18 +626,27 @@ function validateExtensionValue(extnOid, extnValueDer) {
605
626
  }
606
627
 
607
628
  /** @internal The criticality RFC 5755 fixes for each extension it profiles for an attribute
608
- * certificate. The AC signer refuses to write an extension against this table and the extension
609
- * door reports one that arrived against it, from this one row set. */
629
+ * certificate: sec. 4.3's six, and the ProxyInfo whose criticality sec. 7.2 fixes the same way. The
630
+ * AC signer refuses to write an extension against this table, the extension door reports one that
631
+ * arrived against it, and the RFC 5755 lint profile takes its own view of it, from this one row set.
632
+ * Each row carries the clause number and the citation is rendered from it, so a reader cannot be
633
+ * given a clause that names a different section than the rule it states.
634
+ * An extension whose clause says MAY has no row, because a MAY fixes no criticality. AAControls is
635
+ * that case: sec. 7.4 states "criticality MAY be TRUE" and places it in "CA and AC issuer PKCs",
636
+ * so for an attribute certificate it is one of the extensions sec. 4.2.9 governs, where a
637
+ * non-critical one conforms and a critical one does not. That is profile membership, which the lint
638
+ * profile answers from the keys of this table, and not a criticality to enforce here. */
610
639
  var AC_FIXED_CRITICALITY_ROWS = [
611
- ["acAuditIdentity", true, "RFC 5755 sec. 4.3.1"], ["targetInformation", true, "RFC 5755 sec. 4.3.2"],
612
- ["authorityKeyIdentifier", false, "RFC 5755 sec. 4.3.3"], ["authorityInfoAccess", false, "RFC 5755 sec. 4.3.4"],
613
- ["cRLDistributionPoints", false, "RFC 5755 sec. 4.3.5"], ["noRevAvail", false, "RFC 5755 sec. 4.3.6"],
614
- ["acProxying", true, "RFC 5755 sec. 7.3"], ["aaControls", true, "RFC 5755 sec. 7.4"],
640
+ ["acAuditIdentity", true, "4.3.1"], ["targetInformation", true, "4.3.2"],
641
+ ["authorityKeyIdentifier", false, "4.3.3"], ["authorityInfoAccess", false, "4.3.4"],
642
+ ["cRLDistributionPoints", false, "4.3.5"], ["noRevAvail", false, "4.3.6"],
643
+ ["acProxying", true, "7.2"],
615
644
  ];
616
645
  function extensionCriticality(oidRegistry) {
617
646
  var table = intrinsic.create(null);
618
647
  intrinsic.forEach(AC_FIXED_CRITICALITY_ROWS, function (r) {
619
- table[oidRegistry.byName(r[0])] = { critical: r[1], citation: r[2] };
648
+ table[oidRegistry.byName(r[0])] =
649
+ { name: r[0], critical: r[1], clause: r[2], citation: "RFC 5755 sec. " + r[2] };
620
650
  });
621
651
  return table;
622
652
  }
package/lib/schema-cms.js CHANGED
@@ -1092,8 +1092,31 @@ function parseCertsOnly(der, E, prefix, maxCerts, requireCert) {
1092
1092
  };
1093
1093
  }
1094
1094
 
1095
+ /** @internal The DER encoding of ASN.1 NULL, which RFC 5754 sec. 2 makes interchangeable with absent
1096
+ * parameters on a digest AlgorithmIdentifier: "Implementations MUST accept SHA2 AlgorithmIdentifiers
1097
+ * with absent parameters. Implementations MUST accept SHA2 AlgorithmIdentifiers with NULL
1098
+ * parameters." */
1099
+ var DIGEST_DER_NULL = Buffer.from([0x05, 0x00]);
1100
+ function digestParamsAbsentOrNull(p) {
1101
+ if (p === null || p === undefined) return true;
1102
+ return Buffer.isBuffer(p) && p.equals(DIGEST_DER_NULL);
1103
+ }
1104
+ /** @internal Whether two digest AlgorithmIdentifiers name one algorithm, under the rule above. Two
1105
+ * identifiers carrying the same OID and different parameters are different digests, so the
1106
+ * parameters are read rather than ignored; absent and an explicit NULL are the one exception the
1107
+ * clause grants. Every place that compares or LISTS a digest answers from here, so a set cannot be
1108
+ * written under one rule and read under another. */
1109
+ function sameDigestAlgorithm(a, c) {
1110
+ if (!a || !c || a.oid !== c.oid) return false;
1111
+ if (digestParamsAbsentOrNull(a.parameters)) return digestParamsAbsentOrNull(c.parameters);
1112
+ if (digestParamsAbsentOrNull(c.parameters)) return false;
1113
+ return Buffer.isBuffer(a.parameters) && Buffer.isBuffer(c.parameters) &&
1114
+ a.parameters.equals(c.parameters);
1115
+ }
1116
+
1095
1117
  module.exports = {
1096
1118
  parse: parse,
1119
+ sameDigestAlgorithm: sameDigestAlgorithm, // @internal
1097
1120
  pemDecode: pemDecode,
1098
1121
  pemEncode: pemEncode,
1099
1122
  matches: matches,
package/lib/schema-tsp.js CHANGED
@@ -34,10 +34,12 @@
34
34
  * inputs, single-signer token composition over CMS, fail-closed.
35
35
  */
36
36
 
37
+ var nodeCrypto = require("crypto");
37
38
  var asn1 = require("./asn1-der");
38
39
  var schema = require("./schema-engine");
39
40
  var pkix = require("./schema-pkix");
40
41
  var oid = require("./oid");
42
+ var smime = require("./schema-smime");
41
43
  var cms = require("./schema-cms");
42
44
  var frameworkError = require("./framework-error");
43
45
 
@@ -500,6 +502,85 @@ function matches(root) {
500
502
  return true;
501
503
  }
502
504
 
505
+ /** @internal The output length RFC 8702 sec. 2 fixes for each XOF where it names a message digest.
506
+ * A certHash of any other length is not that digest, and the length comparison below refuses it. */
507
+ var ESS_XOF_BYTES = Object.assign(Object.create(null), { shake128: 32, shake256: 64 });
508
+
509
+ /** @internal Which digest an ESS certHash names. RFC 5035 sec. 4's ESSCertIDv2 carries a hashAlgorithm
510
+ * AlgorithmIdentifier and fixes no set of algorithms for it, and RFC 2634 sec. 5.4's ESSCertID carries
511
+ * no such field at all, where SHA-1 is implied. Both attributes satisfy RFC 3161 sec. 2.4.2, so both
512
+ * are read here, and each name comes from the OID registry rather than the token, so the registry is
513
+ * the set of names this is asked about. A hand-kept table of four names left the signer of a token
514
+ * naming any other registered digest unresolved and every row that reads the signer certificate
515
+ * silent, so the question asked is whether this build computes the digest.
516
+ * The digest answers WHICH of the certificates a token already carries the token says signed it,
517
+ * which is a different question from whether that binding is strong enough to trust: a sender who
518
+ * could find a collision here also chose the certificates it would collide, and `pki.tsp.verify`
519
+ * keeps its own narrower table for the binding it accepts. */
520
+ var ESS_NODE_DIGESTS = Object.create(null);
521
+ nodeCrypto.getHashes().forEach(function (n) { ESS_NODE_DIGESTS[n] = true; });
522
+ function essDigest(name) {
523
+ if (typeof name !== "string" || ESS_NODE_DIGESTS[name] !== true) return null;
524
+ var xof = ESS_XOF_BYTES[name];
525
+ /** @internal One read of the name answers both whether the digest can be computed and how to
526
+ * compute it, so the hash every candidate is put through is the one that read settled. */
527
+ return function () {
528
+ return xof ? nodeCrypto.createHash(name, { outputLength: xof }) : nodeCrypto.createHash(name);
529
+ };
530
+ }
531
+
532
+ /** @internal Which of the certificates a token embeds is the one that signed it. RFC 3161 sec. 2.4.2
533
+ * requires the ESS SigningCertificate or SigningCertificateV2 signed attribute and `parseToken`
534
+ * refuses a token carrying neither, so the certHash is present, and it names exactly one
535
+ * certificate. The SignerInfo sid names no certificate uniquely: an issuer with a serial, and a
536
+ * subject key identifier, are both values an issuer wrote, two certificates can carry either, and
537
+ * the `certificates` SET is outside the signature, so a reader that takes the first sid match
538
+ * answers from an order the sender chose. Returns the signer's DER, or null when no embedded
539
+ * certificate hashes to the ESS value, which is the answer a token supplying its certificate out of
540
+ * band gets, and when two attributes name two different embedded certificates. The hash is a public
541
+ * value and is compared by content. */
542
+ function signerCertificateDer(token) {
543
+ var attrs = (token.signerInfo && token.signerInfo.signedAttrs) || [];
544
+ if (!Array.isArray(attrs)) return null;
545
+ var found = null;
546
+ for (var i = 0; i < attrs.length; i++) {
547
+ if (attrs[i].type !== OID_SIGNING_CERT_V2 && attrs[i].type !== OID_SIGNING_CERT) continue;
548
+ var named = _essNamedCertificate(token, attrs[i]);
549
+ if (named === null) continue;
550
+ /** @internal RFC 5035 sec. 7: the two attributes "are generated and evaluated independently" and
551
+ * "the contents could conceivably be in conflict", where a signer holds several certificates over
552
+ * one key. Two that name two different certificates leave which of them signed the token
553
+ * unsettled, so this answers with nothing and the caller says so, rather than taking one of
554
+ * them. */
555
+ if (found !== null) return Buffer.compare(found, named) === 0 ? found : null;
556
+ found = named;
557
+ }
558
+ return found;
559
+ }
560
+
561
+ /** @internal The certificate one ESS attribute names, or null when the token embeds none that hashes
562
+ * to its value. Each attribute is read on its own, so a token carrying both, which RFC 5035 sec. 7
563
+ * permits so that a legacy reader keeps its protection against certificate substitution, resolves
564
+ * from whichever of them names a certificate it carries. */
565
+ function _essNamedCertificate(token, attr) {
566
+ var decoded;
567
+ // allow:swallow-unverified no token reaches here with an unreadable ESS attribute, since parseToken refuses one as tsp/bad-token; this answers for a caller that resolves against a token it parsed elsewhere
568
+ try { decoded = smime.decodeAttribute(attr); } catch (_e) { return null; }
569
+ var essCert = decoded && decoded.certs && decoded.certs[0];
570
+ if (!essCert || !essCert.hashAlgorithm || !essCert.certHash) return null;
571
+ var digest = essDigest(essCert.hashAlgorithm.name);
572
+ if (digest === null) return null;
573
+ var want = Buffer.from(essCert.certHash);
574
+ var certs = Array.isArray(token.certificates) ? token.certificates : [];
575
+ for (var j = 0; j < certs.length; j++) {
576
+ var element = certs[j];
577
+ if (!element || element.tagClass !== "universal" || !Buffer.isBuffer(element.bytes)) continue;
578
+ var actual = digest().update(element.bytes).digest();
579
+ if (actual.length === want.length && Buffer.compare(actual, want) === 0) return element.bytes;
580
+ }
581
+ return null;
582
+ }
583
+
503
584
  module.exports = {
504
585
  parse: parse,
505
586
  parseResponse: parse,
@@ -507,6 +588,7 @@ module.exports = {
507
588
  decodeGeneralName: decodeGeneralName, // @internal
508
589
  parseTstInfo: parseTstInfo,
509
590
  parseToken: parseToken,
591
+ signerCertificateDer: signerCertificateDer, // @internal
510
592
  pemDecode: pemDecode,
511
593
  pemEncode: pemEncode,
512
594
  matches: matches,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.24",
3
+ "version": "0.8.26",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:cdbd209f-fdc3-4d13-8b62-96f8d9222905",
5
+ "serialNumber": "urn:uuid:cbd1318b-e7fc-430e-9a9c-ec7c99161ac6",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-24T16:24:07.579Z",
8
+ "timestamp": "2026-09-28T05:44:25.737Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.8.24",
22
+ "bom-ref": "@blamejs/pki@0.8.26",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.24",
25
+ "version": "0.8.26",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.8.24",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.26",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.8.24",
57
+ "ref": "@blamejs/pki@0.8.26",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]