@blamejs/pki 0.2.30 → 0.2.32

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 CHANGED
@@ -4,6 +4,31 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.2.32 — 2026-07-16
8
+
9
+ X.509 certificates now decode the Microsoft Active Directory Certificate Services enrollment extensions, and pki.lint's critical-extension check is aligned with certification-path validation.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.x509.parse decodes the Microsoft Active Directory Certificate Services enrollment extensions ([MS-WCCE] / [MS-CRTD]): the v2 certificate template (szOID-CERTIFICATE_TEMPLATE -- template OID plus major/minor version), the legacy v1 template name (szOID-ENROLL_CERTTYPE_EXTENSION), the CA version (szOID-CERTSRV_CA_VERSION, surfaced as the raw value and as the CA key index / certificate index split), the previous-CA-certificate hash (szOID-CERTSRV_PREVIOUS_CERT_HASH), and the application policies (szOID-APPLICATION_CERT_POLICIES, decoded as RFC 5280 certificate policies). Each is rendered by pki.inspect and fails closed with a typed error on a malformed shape. The version fields accept the full 32-bit range Active Directory uses, not a signed subset.
14
+
15
+ ### Changed
16
+
17
+ - pki.lint's unrecognized-critical-extension check now mirrors certification-path validation: it reports a critical extension whose semantics the path validator does not process -- an authority/subject key identifier, a freshest-CRL pointer, an SCT list, a qualified-certificate statement, or a Microsoft enterprise-CA extension -- rather than treating any extension it can merely decode as processed. precertificatePoison, which RFC 6962 requires to be critical, is not reported. A conforming relying party must reject a critical extension it cannot process, so a certificate whose critical enterprise or qualified-certificate constraints this toolkit does not enforce is now surfaced by the linter.
18
+
19
+ ## v0.2.31 — 2026-07-16
20
+
21
+ X.509 certificates now decode the RFC 3739 / ETSI EN 319 412-5 qualified-certificate qcStatements extension, and the sigstore verifier gains a low-order EdDSA public-key gate.
22
+
23
+ ### Added
24
+
25
+ - The toolkit decodes the RFC 3739 sec. 3.2.6 / ETSI EN 319 412-5 qualified-certificate qcStatements extension (id-pe-qcStatements). The decoded statements -- QcCompliance (EU-qualified), QcLimitValue (reliance limit), QcSSCD (key in a QSCD), QcType (certificate purpose: esign / eseal / web), QcRetentionPeriod, QcPDS (disclosure-statement URLs), QcCClegislation (country of qualification), QcIdentMethod, QcQSCDlegislation, and the PKIX SemanticsInformation -- are decoded and validated by the toolkit's certificate-extension decoders and rendered by pki.inspect. An unknown statementId is preserved opaque (its raw bytes, semantics not executed); a malformed shape fails closed with a typed error.
26
+
27
+ ### Security
28
+
29
+ - The sigstore keyless-bundle verifier now routes an Ed25519/Ed448 Fulcio leaf or Rekor log public key through the shared full-order Edwards-point gate before verification. A low-order OKP key (which node imports without complaint and which can verify a forged EdDSA signature) is rejected at key-parse (sigstore/bad-key) instead of being handed to verify -- the same defense the webauthn and certification-path EdDSA verifiers already apply.
30
+ - Certification-path validation rejects a CRITICAL qcStatements extension (RFC 5280 sec. 6.1.4). A critical qualified-certificate statement asserts semantics a relying party must enforce -- a QcLimitValue reliance limit, a QcType certificate purpose -- and the validator does not enforce them, so treating the extension as processed would let a caller rely on a certificate outside its asserted qualified-certificate constraints; it is left unprocessed and fails as an unrecognized critical extension. A non-critical qcStatements is informational and does not affect the verdict; the extension is still decoded for pki.inspect and lint, and the linter flags a critical qcStatements as an unrecognized critical extension for the same reason -- structural decodability is not validation processing.
31
+
7
32
  ## v0.2.30 — 2026-07-16
8
33
 
9
34
  C509 CBOR-encoded certificates arrive as pki.schema.c509.parse: decode the compact CBOR profile of X.509 in both its natively-signed and X.509-re-encoded forms, reconstructing the original DER byte-for-byte so the original signature still verifies.
package/README.md CHANGED
@@ -203,7 +203,7 @@ is callable today; nothing below is a stub.
203
203
  | `pki.oid` | Two-way OID ↔ name registry — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`; seeded with RFC 5280 + NIST PQC arcs |
204
204
  | `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
205
205
  | `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
206
- | `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields — `parse`, `pemDecode`, `pemEncode` |
206
+ | `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque) and the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), fail-closed — `parse`, `pemDecode`, `pemEncode` |
207
207
  | `pki.schema.c509` | Parse C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed) |
208
208
  | `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode`, `pemEncode` |
209
209
  | `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
package/lib/inspect.js CHANGED
@@ -317,6 +317,37 @@ var EXT_RENDERERS = {
317
317
  if (decoded.pathLenConstraint != null) s += ", pathlen:" + decoded.pathLenConstraint;
318
318
  return inner + s;
319
319
  },
320
+ qcStatements: function (decoded, inner) {
321
+ return decoded.map(function (s) {
322
+ var label = inner + (s.name || s.statementId), info = s.info;
323
+ if (!info) return label; // presence-only (QcCompliance / QcSSCD)
324
+ if (info.opaque) return label + " (opaque)";
325
+ if (typeof info.amount !== "undefined") return label + ": " + info.amount + " " + info.currency + (info.exponent ? " x10^" + info.exponent : ""); // QcLimitValue
326
+ if (typeof info.years !== "undefined") return label + ": " + info.years + " years"; // QcRetentionPeriod
327
+ if (info.typeNames) return label + ": " + info.typeNames.map(function (n, i) { return n || info.types[i]; }).join(", "); // QcType
328
+ if (info.methodNames) return label + ": " + info.methodNames.map(function (n, i) { return n || info.methods[i]; }).join(", "); // QcIdentMethod
329
+ if (info.locations) return label + ": " + info.locations.map(function (l) { return l.url + " (" + l.language + ")"; }).join(", "); // QcPDS
330
+ if (info.countries) return label + ": " + info.countries.join(", "); // QcCClegislation / QcQSCDlegislation
331
+ if (typeof info.semanticsIdentifier !== "undefined") { // SemanticsInformation (id-qcs)
332
+ var nra = info.nameRegistrationAuthorities && info.nameRegistrationAuthorities.length;
333
+ return label + (info.semanticsIdentifier ? ": " + info.semanticsIdentifier : "") + (nra ? " (" + nra + " NRA)" : "");
334
+ }
335
+ return label;
336
+ }).join("\n");
337
+ },
338
+ msCertificateTemplate: function (decoded, inner) {
339
+ var v = decoded.templateMajorVersion === null ? "" : " v" + decoded.templateMajorVersion + "." + (decoded.templateMinorVersion === null ? 0 : decoded.templateMinorVersion);
340
+ return inner + "Template: " + (decoded.name || decoded.templateID) + v;
341
+ },
342
+ msEnrollCertType: function (decoded, inner) {
343
+ return inner + "Cert Type: " + _clean(String(decoded));
344
+ },
345
+ msCaVersion: function (decoded, inner) {
346
+ return inner + "CA Version: V" + decoded.caKeyIndex + "." + decoded.certIndex;
347
+ },
348
+ msPreviousCertHash: function (decoded, inner) {
349
+ return inner + _hexColon(Buffer.isBuffer(decoded) ? decoded : Buffer.alloc(0), { upper: true });
350
+ },
320
351
  subjectAltName: _renderAltName,
321
352
  issuerAltName: _renderAltName,
322
353
  certificatePolicies: function (decoded, inner) {
@@ -404,6 +435,8 @@ var EXT_RENDERERS = {
404
435
  return akiLines.length ? akiLines.join("\n") : inner + "keyid:(none)";
405
436
  },
406
437
  };
438
+ // [MS-WCCE] szOID-APPLICATION_CERT_POLICIES decodes as certificatePolicies, so it renders identically.
439
+ EXT_RENDERERS.msApplicationPolicies = EXT_RENDERERS.certificatePolicies;
407
440
 
408
441
  // Coverage residual -- the EXT_RENDERERS entry fallbacks are unreachable because each shared
409
442
  // decoder (and asn1.read.oid) already narrows the shape before the renderer runs:
package/lib/lint.js CHANGED
@@ -29,6 +29,7 @@ var guard = require("./guard-all");
29
29
  var oid = require("./oid");
30
30
  var x509 = require("./schema-x509");
31
31
  var pkix = require("./schema-pkix");
32
+ var path = require("./path-validate");
32
33
  var C = require("./constants");
33
34
  var ipUtils = require("./ip-utils");
34
35
 
@@ -237,14 +238,32 @@ function _subjectCNs(cert) {
237
238
  return out;
238
239
  }
239
240
 
240
- // An extension is "recognized" (RFC 5280 4.2: a consumer MUST reject a critical extension
241
- // it does NOT recognize / cannot process) when the toolkit has a DECODER for it -- the
242
- // shared certExtensionDecoders table IS the set of extensions this toolkit processes. This
243
- // is registry-driven (no curated list to omit a legitimately-critical extension such as
244
- // policyMappings, which the table decodes) and correctly scoped to EXTENSION OIDs: an
245
- // algorithm / EKU-purpose OID the name registry happens to resolve is NOT in the decoder
246
- // table, so a critical extension carrying such an OID is still flagged.
247
- function _isUnknownExtension(extOid) { return !EXT_DECODERS[extOid]; }
241
+ // An extension's CRITICALITY is honoured (RFC 5280 4.2: a consumer MUST reject a critical extension
242
+ // it cannot PROCESS) only when this toolkit actually processes its semantics. That authority is
243
+ // path-validate's PROCESSED_EXTENSIONS (the RFC 5280 sec. 6.1 path-processing set), NOT the decoder
244
+ // table: certExtensionDecoders also DECODES many extensions purely for display (qcStatements, the MS
245
+ // enterprise-CA extensions, the SCT list, the *KeyIdentifiers, freshestCRL, issuerAltName) whose
246
+ // critical semantics are not enforced -- path-validate rejects a critical instance of any of those
247
+ // as unrecognized-critical, so the linter flags them too (structural decodability is not validation
248
+ // processing). The one decode-only extension that is LEGITIMATELY critical is precertificatePoison
249
+ // (RFC 6962 sec. 3.1 REQUIRES it critical), so it is allow-listed. Everything else -- a processed
250
+ // extension, or an unregistered OID (algorithm / EKU-purpose OID the name registry resolves but that
251
+ // is no extension) -- falls out correctly: processed -> recognized, unregistered -> unknown. Driving
252
+ // this off the validator's set (not the decoder table) means a newly-decoded extension is
253
+ // flagged-when-critical automatically until its semantics are actually processed.
254
+ var _CRITICAL_LEGIT = {};
255
+ _CRITICAL_LEGIT[oid.byName("precertificatePoison")] = true;
256
+ // Mirrors path-validate's unrecognizedCriticalExtension. An extension not in PROCESSED_EXTENSIONS is
257
+ // unknown everywhere (precertificatePoison is the one legitimately-critical exception). An extension
258
+ // that IS processed for an intermediate but is unprocessed on the TARGET (policyMappings) is also
259
+ // flagged: path-validate decides target status purely by path position (i === n), so ANY certificate
260
+ // -- a CA included -- can be validated as the final/target certificate, where a critical instance is
261
+ // rejected. A linter has no path context and cannot rule that out, so it flags these regardless of
262
+ // cA-ness (policyMappings is SHOULD-be-non-critical everywhere, so this never mis-flags conformant input).
263
+ function _isUnknownExtension(extOid) {
264
+ if (path.PROCESSED_EXTENSIONS[extOid] !== true) return _CRITICAL_LEGIT[extOid] !== true;
265
+ return path.TARGET_UNPROCESSED_IF_CRITICAL[extOid] === true;
266
+ }
248
267
 
249
268
  // RSA-modulus / EC-curve helpers for the weak-key lint. Each returns null when the key
250
269
  // material cannot be read as expected (a malformed RSA modulus, or EC parameters that are
package/lib/oid.js CHANGED
@@ -80,7 +80,27 @@ var FAMILIES = {
80
80
  // aaControls sec. 7.4, ac-proxying sec. 7.2).
81
81
  pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: {
82
82
  authorityInfoAccess: 1, acAuditIdentity: 4, aaControls: 6, acProxying: 10,
83
- acmeIdentifier: 31 } },
83
+ acmeIdentifier: 31,
84
+ // id-pe-qcStatements (RFC 3739 sec. 3.2.6) -- the qualified-certificate-statements extension.
85
+ qcStatements: 3 } },
86
+
87
+ // RFC 3739 sec. 3.2.6.1 id-qcs -- the PKIX QCStatement statementIds carrying SemanticsInformation.
88
+ pkixQcSyntax: { base: [1, 3, 6, 1, 5, 5, 7, 11], of: { qcsPkixQCSyntaxV1: 1, qcsPkixQCSyntaxV2: 2 } },
89
+
90
+ // ETSI EN 319 412-5 qualified-certificate statements (esi4-qcStatement-N) on the id-etsi-qcs arc.
91
+ etsiQcs: { base: [0, 4, 0, 1862, 1], of: {
92
+ qcCompliance: 1, qcLimitValue: 2, qcRetentionPeriod: 3, qcSSCD: 4, qcPDS: 5,
93
+ qcType: 6, qcCClegislation: 7, qcIdentMethod: 8, qcQSCDlegislation: 9 } },
94
+
95
+ // ETSI EN 319 412-5 QcType member OIDs (the certificate-purpose types) on the id-etsi-qct arc.
96
+ etsiQcType: { base: [0, 4, 0, 1862, 1, 6], of: { qctEsign: 1, qctEseal: 2, qctWeb: 3 } },
97
+
98
+ // [MS-WCCE] AD CS enrollment extensions on the id-microsoft-21 arc (szOID-CERTSRV / szOID-CERTIFICATE_TEMPLATE / szOID-APPLICATION_CERT_POLICIES).
99
+ msEnrollment: { base: [1, 3, 6, 1, 4, 1, 311, 21], of: {
100
+ msCaVersion: 1, msPreviousCertHash: 2, msCertificateTemplate: 7, msApplicationPolicies: 10 } },
101
+
102
+ // [MS-CRTD] legacy v1 enroll cert-type name on the id-microsoft-20 arc (szOID-ENROLL_CERTTYPE_EXTENSION).
103
+ msEnrollmentLegacy: { base: [1, 3, 6, 1, 4, 1, 311, 20], of: { msEnrollCertType: 2 } },
84
104
 
85
105
  // id-aca -- RFC 5755 attribute-certificate attribute types on the id-pkix 10 arc:
86
106
  // authenticationInfo sec. 4.4.1 .. group sec. 4.4.4, plus encAttrs sec. 7.1 (the encrypted-
@@ -91,10 +91,21 @@ var PROCESSED_EXTENSIONS = {};
91
91
  // validateCriticalExtensionStructure via the registered decoder. freshestCRL
92
92
  // stays OUT: sec. 4.2.1.15 requires it non-critical and the validator does not
93
93
  // consult it (no delta merge), so a critical instance fails unrecognized.
94
+ // qcStatements is deliberately NOT processed: a critical QC statement (QcLimitValue reliance limit,
95
+ // QcType certificate purpose) asserts qualified-certificate semantics a relying party MUST enforce, and
96
+ // this validator does not enforce them (nor expose a handler to). Marking it processed would let a caller
97
+ // treat a certificate as valid outside its asserted critical QC constraints, so a critical qcStatements
98
+ // fails as an unrecognized-critical extension (RFC 5280 sec. 6.1.4); a non-critical instance is
99
+ // informational and does not affect the verdict. The extension is still decoded for pki.inspect / lint.
94
100
  [OID.basicConstraints, OID.keyUsage, OID.nameConstraints, OID.certificatePolicies,
95
101
  OID.policyMappings, OID.policyConstraints, OID.inhibitAnyPolicy, OID.subjectAltName,
96
102
  OID.extKeyUsage, OID.cRLDistributionPoints].
97
103
  forEach(function (o) { PROCESSED_EXTENSIONS[o] = true; });
104
+ // Frozen after seeding: this exact object is both consulted by the critical-extension check here and
105
+ // exported (for pki.lint to stay consistent). A caller must not be able to add an OID -- doing so
106
+ // would make an attacker's critical, decoder-less extension pass as "processed" and skip both the
107
+ // unrecognized-critical check and structural validation. Freezing makes any such write a no-op.
108
+ Object.freeze(PROCESSED_EXTENSIONS);
98
109
 
99
110
  // ---- signature verify bridge (NEW 6) ---------------------------------------
100
111
 
@@ -1055,6 +1066,7 @@ function applyPolicyMappings(state, mappings, i) {
1055
1066
  // a terminal CA cert is conforming and is NOT treated as unprocessed here.)
1056
1067
  var TARGET_UNPROCESSED_IF_CRITICAL = {};
1057
1068
  TARGET_UNPROCESSED_IF_CRITICAL[OID.policyMappings] = true;
1069
+ Object.freeze(TARGET_UNPROCESSED_IF_CRITICAL);
1058
1070
 
1059
1071
  function unrecognizedCriticalExtension(cert, isTarget) {
1060
1072
  for (var i = 0; i < cert.extensions.length; i++) {
@@ -2017,4 +2029,13 @@ module.exports = {
2017
2029
  crlChecker: crlChecker,
2018
2030
  ocspChecker: ocspChecker,
2019
2031
  verifyOcspResponse: verifyOcspResponse,
2032
+ // The set of extension OIDs whose CRITICAL semantics this validator processes (RFC 5280 sec. 6.1).
2033
+ // Exposed so a linter can distinguish "processed" from "merely decoded" and stay consistent with
2034
+ // the path-validation verdict on a critical extension -- a decoder in certExtensionDecoders is NOT
2035
+ // by itself proof the criticality is honoured. Both sets are frozen so a caller cannot mutate them.
2036
+ PROCESSED_EXTENSIONS: PROCESSED_EXTENSIONS,
2037
+ // Extensions that ARE processed for an intermediate CA but are unprocessed on the target/leaf, so a
2038
+ // critical instance on the target fails closed (RFC 5280 sec. 6.1.5(f)) -- policyMappings is
2039
+ // prepare-next-only and SHOULD-be-non-critical. A linter mirrors this for a leaf certificate.
2040
+ TARGET_UNPROCESSED_IF_CRITICAL: TARGET_UNPROCESSED_IF_CRITICAL,
2020
2041
  };
@@ -571,7 +571,10 @@ function distributionPointName(ns, node, code) {
571
571
  // inside. Each decoder takes that raw Buffer and returns the decoded value, or
572
572
  // throws a typed `<prefix>/bad-*` code, mirroring the CRL's fail-closed
573
573
  // `decodeExt` pattern (asn1.decode -> read.* / schema.walk, wrapped). Returns
574
- // { byOid } keyed by dotted OID so a consumer dispatches by ext.oid.
574
+ // { byOid } keyed by dotted OID so a consumer dispatches by ext.oid. The decoded
575
+ // set spans the RFC 5280 sec. 4.2.1 extensions plus the RFC 3739 sec. 3.2.6 /
576
+ // ETSI EN 319 412-5 qualified-certificate `qcStatements` extension (each known
577
+ // statement decoded, an unknown statementId preserved opaque).
575
578
  var _T = asn1.TAGS;
576
579
 
577
580
  // assertPolicyQualifiers(qNode, fail) -- RFC 5280 sec. 4.2.1.4: a PolicyInformation's
@@ -916,7 +919,171 @@ function certExtensionDecoders(ns) {
916
919
  if (typeof d !== "string") throw new TypeError("certExtensionDecoders: " + JSON.stringify(nm) + " is not a registered OID name");
917
920
  return d;
918
921
  }
922
+ // qcStatements ::= SEQUENCE OF QCStatement (RFC 3739 sec. 3.2.6). QCStatement ::= SEQUENCE {
923
+ // statementId OBJECT IDENTIFIER, statementInfo ANY DEFINED BY statementId OPTIONAL }. Known statementIds
924
+ // (RFC 3739 id-qcs + the ETSI EN 319 412-5 esi4 catalog) decode their statementInfo; an unknown statementId
925
+ // is preserved OPAQUE (the RFC 3739 open type), never rejected. The DEFINED-BY selection is an OID re-dispatch.
926
+ function _qcStr(node, tag, C, what) {
927
+ if (!node || node.tagClass !== "universal" || node.tagNumber !== tag) throw ns.E(C, what);
928
+ try { return asn1.read.string(node); } catch (e) { throw ns.E(C, what, e); }
929
+ }
930
+ function _qcOidSeq(node, C, what) { // a non-empty SEQUENCE OF OBJECT IDENTIFIER
931
+ if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, what + " must be a non-empty SEQUENCE OF OBJECT IDENTIFIER");
932
+ return node.children.map(function (c) {
933
+ if (c.tagClass !== "universal" || c.tagNumber !== _T.OBJECT_IDENTIFIER) throw ns.E(C, what + " members must be OBJECT IDENTIFIERs");
934
+ try { return asn1.read.oid(c); } catch (e) { throw ns.E(C, what + " member is not a valid OID", e); }
935
+ });
936
+ }
937
+ function _qcPresenceOnly(nm) { return function (node, C) { if (node) throw ns.E(C, nm + " carries no statementInfo (ETSI EN 319 412-5)"); return null; }; }
938
+ function _qcSemantics(node, C) { // SemanticsInformation, WITH COMPONENTS at least one field present
939
+ if (node === null) return null; // statementInfo OPTIONAL for id-qcs syntax statements
940
+ if (node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, "SemanticsInformation must be a SEQUENCE with at least one field (RFC 3739 sec. 3.2.6.1)");
941
+ var sid = null, i = 0;
942
+ if (node.children[0].tagClass === "universal" && node.children[0].tagNumber === _T.OBJECT_IDENTIFIER) {
943
+ try { sid = asn1.read.oid(node.children[0]); } catch (e) { throw ns.E(C, "SemanticsInformation semanticsIdentifier must be an OID", e); }
944
+ i++;
945
+ }
946
+ var nras = [];
947
+ if (node.children[i]) {
948
+ // nameRegistrationAuthorities ::= SEQUENCE SIZE (1..MAX) OF GeneralName -- decode + fully validate
949
+ // each element through the shared GeneralName decoder (context tag [0]..[8] + per-alternative
950
+ // structure), rather than surfacing arbitrary bytes as a name-registration authority.
951
+ nras = schema.walk(generalNames(ns, { decodeValue: true, code: C }), node.children[i], ns).result.names;
952
+ i++;
953
+ }
954
+ if (i !== node.children.length) throw ns.E(C, "SemanticsInformation has unexpected trailing fields");
955
+ return { semanticsIdentifier: sid, nameRegistrationAuthorities: nras };
956
+ }
957
+ var qcInfoByOid = {};
958
+ qcInfoByOid[O("qcCompliance")] = _qcPresenceOnly("QcCompliance");
959
+ qcInfoByOid[O("qcSSCD")] = _qcPresenceOnly("QcSSCD");
960
+ qcInfoByOid[O("qcsPkixQCSyntaxV1")] = _qcSemantics;
961
+ qcInfoByOid[O("qcsPkixQCSyntaxV2")] = _qcSemantics;
962
+ qcInfoByOid[O("qcType")] = function (node, C) {
963
+ var oids = _qcOidSeq(node, C, "QcType"); // SIZE(1) in EN 319 412-5 V2.5.1; accept >=1 for legacy certs
964
+ return { types: oids, typeNames: oids.map(function (d) { return ns.oid.name(d) || null; }) };
965
+ };
966
+ qcInfoByOid[O("qcRetentionPeriod")] = function (node, C) {
967
+ if (!node) throw ns.E(C, "QcRetentionPeriod requires an INTEGER statementInfo");
968
+ return { years: guard.range.uint31(readInt(node, C, "QcRetentionPeriod"), ns.E, C, "QcRetentionPeriod (years)") };
969
+ };
970
+ qcInfoByOid[O("qcLimitValue")] = function (node, C) { // MonetaryValue ::= SEQUENCE { currency, amount, exponent }
971
+ if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || node.children.length !== 3) throw ns.E(C, "QcLimitValue must be a MonetaryValue SEQUENCE { currency, amount, exponent }");
972
+ var cur = node.children[0], currency;
973
+ if (cur.tagClass === "universal" && cur.tagNumber === _T.PRINTABLE_STRING) {
974
+ // Route through _qcStr so a disallowed PrintableString byte surfaces the decoder's typed
975
+ // bad-qc-statement error, not a leaked asn1/bad-printable-string from the raw leaf read.
976
+ currency = _qcStr(cur, _T.PRINTABLE_STRING, C, "QcLimitValue alphabetic currency must be a PrintableString");
977
+ if (currency.length !== 3) throw ns.E(C, "QcLimitValue alphabetic currency must be a 3-letter ISO 4217 code");
978
+ } else if (cur.tagClass === "universal" && cur.tagNumber === _T.INTEGER) {
979
+ currency = guard.range.int(readInt(cur, C, "QcLimitValue numeric currency"), 1n, 999n, ns.E, C, "QcLimitValue numeric currency (ISO 4217, 1..999)");
980
+ } else throw ns.E(C, "QcLimitValue currency must be a PrintableString(3) or INTEGER(1..999)");
981
+ return {
982
+ currency: currency,
983
+ // MonetaryValue amount and exponent are unconstrained INTEGERs (the amount non-negative in practice);
984
+ // bound each only to the safe-integer range so it narrows to a Number losslessly -- an artificial
985
+ // tighter bound (uint31, or a small exponent window) would false-reject a valid MonetaryValue, and a
986
+ // wider one would round silently and mis-state the limit.
987
+ amount: guard.range.int(readInt(node.children[1], C, "QcLimitValue amount"), 0n, 9007199254740991n, ns.E, C, "QcLimitValue amount"),
988
+ exponent: guard.range.int(readInt(node.children[2], C, "QcLimitValue exponent"), -9007199254740991n, 9007199254740991n, ns.E, C, "QcLimitValue exponent"),
989
+ };
990
+ };
991
+ qcInfoByOid[O("qcCClegislation")] = function (node, C) { // SEQUENCE OF CountryName (PrintableString SIZE 2)
992
+ if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, "QcCClegislation must be a non-empty SEQUENCE OF CountryName");
993
+ return { countries: node.children.map(function (c) { var v = _qcStr(c, _T.PRINTABLE_STRING, C, "QcCClegislation CountryName must be a PrintableString"); if (v.length !== 2) throw ns.E(C, "QcCClegislation CountryName must be a 2-letter ISO 3166-1 code"); return v; }) };
994
+ };
995
+ qcInfoByOid[O("qcPDS")] = function (node, C) { // PdsLocations ::= SEQUENCE OF PdsLocation { url IA5, language Printable(2) }
996
+ if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, "QcPDS must be a non-empty SEQUENCE OF PdsLocation");
997
+ return { locations: node.children.map(function (loc) {
998
+ if (loc.tagClass !== "universal" || loc.tagNumber !== _T.SEQUENCE || !loc.children || loc.children.length !== 2) throw ns.E(C, "a QcPDS PdsLocation must be a SEQUENCE { url, language }");
999
+ var lang = _qcStr(loc.children[1], _T.PRINTABLE_STRING, C, "a QcPDS language must be a PrintableString");
1000
+ if (lang.length !== 2) throw ns.E(C, "a QcPDS language must be a 2-letter ISO 639-1 code");
1001
+ return { url: _qcStr(loc.children[0], _T.IA5_STRING, C, "a QcPDS url must be an IA5String"), language: lang };
1002
+ }) };
1003
+ };
1004
+ // ETSI EN 319 412-5 V2.5.1 additions: QcIdentMethod (SEQUENCE OF OID, like QcType) and
1005
+ // QcQSCDlegislation (SEQUENCE OF CountryName, like QcCClegislation) -- registered ids, decoded.
1006
+ qcInfoByOid[O("qcIdentMethod")] = function (node, C) {
1007
+ var oids = _qcOidSeq(node, C, "QcIdentMethod");
1008
+ return { methods: oids, methodNames: oids.map(function (d) { return ns.oid.name(d) || null; }) };
1009
+ };
1010
+ qcInfoByOid[O("qcQSCDlegislation")] = function (node, C) {
1011
+ if (!node || node.tagClass !== "universal" || node.tagNumber !== _T.SEQUENCE || !node.children || !node.children.length) throw ns.E(C, "QcQSCDlegislation must be a non-empty SEQUENCE OF CountryName");
1012
+ return { countries: node.children.map(function (c) { var v = _qcStr(c, _T.PRINTABLE_STRING, C, "QcQSCDlegislation CountryName must be a PrintableString"); if (v.length !== 2) throw ns.E(C, "QcQSCDlegislation CountryName must be a 2-letter ISO 3166-1 code"); return v; }) };
1013
+ };
1014
+ function qcStatements(buf) {
1015
+ var C = ns.prefix + "/bad-qc-statements", CS = ns.prefix + "/bad-qc-statement";
1016
+ var kids = seqChildren(buf, C, "QCStatements");
1017
+ if (!kids.length) throw ns.E(C, "QCStatements must contain at least one QCStatement (RFC 3739 sec. 3.2.6)");
1018
+ var out = []; // QCStatements ::= SEQUENCE OF QCStatement -- RFC 3739 imposes no statementId uniqueness; decode every statement in wire order (a linter may flag a repeat, the decoder must not drop it)
1019
+ for (var i = 0; i < kids.length; i++) {
1020
+ var st = kids[i];
1021
+ if (st.tagClass !== "universal" || st.tagNumber !== _T.SEQUENCE || !st.children || st.children.length < 1 || st.children.length > 2) {
1022
+ throw ns.E(CS, "each QCStatement must be a SEQUENCE of a statementId and an OPTIONAL statementInfo");
1023
+ }
1024
+ var id;
1025
+ try { id = asn1.read.oid(st.children[0]); } catch (e) { throw ns.E(CS, "a QCStatement statementId must be an OBJECT IDENTIFIER", e); }
1026
+ var infoNode = st.children.length === 2 ? st.children[1] : null;
1027
+ var decoder = qcInfoByOid[id];
1028
+ var info = decoder ? decoder(infoNode, CS) : { opaque: true, bytes: infoNode ? infoNode.bytes : null };
1029
+ out.push({ statementId: id, name: ns.oid.name(id) || null, info: info });
1030
+ }
1031
+ return out;
1032
+ }
1033
+
1034
+ // [MS-WCCE] szOID-CERTIFICATE_TEMPLATE -- CertificateTemplateOID ::= SEQUENCE {
1035
+ // templateID OID, templateMajorVersion INTEGER OPTIONAL, templateMinorVersion INTEGER OPTIONAL }.
1036
+ // The two versions are DWORDs (0..2^32-1), NOT uint31 -- a CA key/template revision with bit 31
1037
+ // set is a legal DWORD, so bound to the whole safe-integer range, never guard.range.uint31.
1038
+ function msCertificateTemplate(buf) {
1039
+ var C = ns.prefix + "/bad-ms-certificate-template";
1040
+ var kids = seqChildren(buf, C, "CertificateTemplate");
1041
+ if (!kids.length || kids.length > 3) throw ns.E(C, "CertificateTemplateOID must be a SEQUENCE of a templateID and up to two version INTEGERs");
1042
+ if (kids[0].tagClass !== "universal" || kids[0].tagNumber !== _T.OBJECT_IDENTIFIER) throw ns.E(C, "CertificateTemplateOID templateID must be an OBJECT IDENTIFIER");
1043
+ var id;
1044
+ try { id = asn1.read.oid(kids[0]); } catch (e) { throw ns.E(C, "CertificateTemplateOID templateID must be an OBJECT IDENTIFIER", e); }
1045
+ function ver(i, what) {
1046
+ if (!kids[i]) return null;
1047
+ if (kids[i].tagClass !== "universal" || kids[i].tagNumber !== _T.INTEGER) throw ns.E(C, what + " must be an INTEGER");
1048
+ return guard.range.int(readInt(kids[i], C, what), 0n, 4294967295n, ns.E, C, what);
1049
+ }
1050
+ return { templateID: id, name: ns.oid.name(id) || null, templateMajorVersion: ver(1, "templateMajorVersion"), templateMinorVersion: ver(2, "templateMinorVersion") };
1051
+ }
1052
+
1053
+ // [MS-CRTD] szOID-ENROLL_CERTTYPE_EXTENSION -- the legacy v1 template name as a BARE BMPString.
1054
+ // asn1.read.string accepts any universal string tag, so the explicit BMP_STRING assert is the guard.
1055
+ function msEnrollCertType(buf) {
1056
+ var C = ns.prefix + "/bad-ms-enroll-cert-type";
1057
+ var n = decodeTop(buf, C, "EnrollCertType");
1058
+ if (n.tagClass !== "universal" || n.tagNumber !== _T.BMP_STRING) throw ns.E(C, "the enroll cert-type name must be a BMPString");
1059
+ try { return asn1.read.string(n); } catch (e) { throw ns.E(C, "the enroll cert-type name must be a well-formed BMPString", e); }
1060
+ }
1061
+
1062
+ // [MS-WCCE] szOID-CERTSRV_CA_VERSION -- INTEGER (0..2^32-1) = (caKeyIndex << 16) | certIndex.
1063
+ function msCaVersion(buf) {
1064
+ var C = ns.prefix + "/bad-ms-ca-version";
1065
+ var n = decodeTop(buf, C, "CACertVersion");
1066
+ if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "CACertVersion must be an INTEGER");
1067
+ var v = guard.range.int(readInt(n, C, "CACertVersion"), 0n, 4294967295n, ns.E, C, "CACertVersion (DWORD)");
1068
+ return { caVersion: v, caKeyIndex: v >>> 16, certIndex: v & 0xffff };
1069
+ }
1070
+
1071
+ // [MS-WCCE] szOID-CERTSRV_PREVIOUS_CERT_HASH -- the SHA-1 thumbprint of the prior CA certificate.
1072
+ // AD CS stamps the Windows certificate THUMBPRINT here, which Windows computes with SHA-1 regardless
1073
+ // of the certificate's signature algorithm, so the OCTET STRING is exactly 20 octets. An empty or
1074
+ // other-length value is malformed renewal metadata and fails closed.
1075
+ function msPreviousCertHash(buf) {
1076
+ var C = ns.prefix + "/bad-ms-previous-cert-hash";
1077
+ var n = decodeTop(buf, C, "CAPrevCertHash");
1078
+ var hash;
1079
+ try { hash = Buffer.concat([asn1.read.octetString(n)]); }
1080
+ catch (e) { throw ns.E(C, "CAPrevCertHash must be an OCTET STRING", e); }
1081
+ if (hash.length !== 20) throw ns.E(C, "CAPrevCertHash must be a 20-octet SHA-1 certificate thumbprint");
1082
+ return hash;
1083
+ }
1084
+
919
1085
  var byOid = {};
1086
+ byOid[O("qcStatements")] = qcStatements;
920
1087
  byOid[O("basicConstraints")] = basicConstraints;
921
1088
  byOid[O("keyUsage")] = keyUsage;
922
1089
  byOid[O("nameConstraints")] = nameConstraints;
@@ -933,6 +1100,11 @@ function certExtensionDecoders(ns) {
933
1100
  byOid[O("precertificatePoison")] = precertPoison;
934
1101
  byOid[O("cRLDistributionPoints")] = crlDistributionPoints;
935
1102
  byOid[O("freshestCRL")] = crlDistributionPoints;
1103
+ byOid[O("msCertificateTemplate")] = msCertificateTemplate;
1104
+ byOid[O("msEnrollCertType")] = msEnrollCertType;
1105
+ byOid[O("msCaVersion")] = msCaVersion;
1106
+ byOid[O("msPreviousCertHash")] = msPreviousCertHash;
1107
+ byOid[O("msApplicationPolicies")] = certificatePolicies; // [MS-WCCE] = RFC 5280 certificatePolicies, reused not re-implemented
936
1108
  return { byOid: byOid };
937
1109
  }
938
1110
 
package/lib/sigstore.js CHANGED
@@ -33,6 +33,7 @@ var x509 = require("./schema-x509");
33
33
  var pathValidate = require("./path-validate");
34
34
  var merkle = require("./merkle");
35
35
  var oid = require("./oid");
36
+ var edwardsPoint = require("./edwards-point");
36
37
 
37
38
  var C = constants;
38
39
  var SigstoreError = frameworkError.SigstoreError;
@@ -184,8 +185,18 @@ function _rawVerify(keyObj, data, derSig) {
184
185
  return nodeCrypto.verify(hash, data, { key: keyObj, dsaEncoding: "der" }, derSig);
185
186
  }
186
187
  function _pubFromSpki(spkiDer, label) {
187
- try { return nodeCrypto.createPublicKey({ key: spkiDer, format: "der", type: "spki" }); }
188
+ var key;
189
+ try { key = nodeCrypto.createPublicKey({ key: spkiDer, format: "der", type: "spki" }); }
188
190
  catch (e) { throw _err("sigstore/bad-key", "invalid " + label + " public key", e); }
191
+ // node imports a low-order (e.g. all-zeroes) OKP public key without complaint, and such a key
192
+ // verifies a FORGED EdDSA signature -- route an Ed25519/Ed448 SPKI through the shared full-order,
193
+ // on-curve Edwards-point gate (as the webauthn and path-validation EdDSA paths already do) before it
194
+ // can be handed to verify.
195
+ var t = key.asymmetricKeyType;
196
+ if (t === "ed25519" || t === "ed448") {
197
+ edwardsPoint.validateSpki(spkiDer, t === "ed25519" ? 6 : 7, SigstoreError, "sigstore/bad-key");
198
+ }
199
+ return key;
189
200
  }
190
201
  // Parse a certificate, re-typing a malformed-DER fault to a sigstore/* error so
191
202
  // the caller never sees a raw certificate/* / asn1/* leak from this boundary.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.2.30",
3
+ "version": "0.2.32",
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:fcac8430-97c8-4f4c-a497-dc2f02fb3c6f",
5
+ "serialNumber": "urn:uuid:c1caa29b-a81a-443d-9296-76988e159993",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-16T18:34:40.509Z",
8
+ "timestamp": "2026-07-16T23:57:14.646Z",
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.2.30",
22
+ "bom-ref": "@blamejs/pki@0.2.32",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.30",
25
+ "version": "0.2.32",
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.2.30",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.2.32",
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.2.30",
57
+ "ref": "@blamejs/pki@0.2.32",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]