@blamejs/pki 0.2.30 → 0.2.31
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 +13 -0
- package/README.md +1 -1
- package/lib/inspect.js +18 -0
- package/lib/lint.js +11 -1
- package/lib/oid.js +14 -1
- package/lib/path-validate.js +6 -0
- package/lib/schema-pkix.js +117 -1
- package/lib/sigstore.js +12 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.31 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
- 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.
|
|
19
|
+
|
|
7
20
|
## v0.2.30 — 2026-07-16
|
|
8
21
|
|
|
9
22
|
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), 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,24 @@ 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
|
+
},
|
|
320
338
|
subjectAltName: _renderAltName,
|
|
321
339
|
issuerAltName: _renderAltName,
|
|
322
340
|
certificatePolicies: function (decoded, inner) {
|
package/lib/lint.js
CHANGED
|
@@ -244,7 +244,17 @@ function _subjectCNs(cert) {
|
|
|
244
244
|
// policyMappings, which the table decodes) and correctly scoped to EXTENSION OIDs: an
|
|
245
245
|
// algorithm / EKU-purpose OID the name registry happens to resolve is NOT in the decoder
|
|
246
246
|
// table, so a critical extension carrying such an OID is still flagged.
|
|
247
|
-
|
|
247
|
+
//
|
|
248
|
+
// The one exception: an extension the table DECODES for display but whose CRITICAL semantics
|
|
249
|
+
// the toolkit does not enforce. qcStatements decodes (for pki.inspect / this linter) yet a
|
|
250
|
+
// critical instance asserts qualified-certificate constraints (a reliance limit, a certificate
|
|
251
|
+
// purpose) no consumer here processes -- path-validate rejects a critical qcStatements as
|
|
252
|
+
// unrecognized-critical for exactly that reason. Structural decodability is not validation
|
|
253
|
+
// processing, so a critical qcStatements is still flagged here, keeping lint consistent with
|
|
254
|
+
// certification-path validation.
|
|
255
|
+
var _DECODE_ONLY_UNPROCESSED_CRITICAL = {};
|
|
256
|
+
_DECODE_ONLY_UNPROCESSED_CRITICAL[oid.byName("qcStatements")] = true;
|
|
257
|
+
function _isUnknownExtension(extOid) { return !EXT_DECODERS[extOid] || _DECODE_ONLY_UNPROCESSED_CRITICAL[extOid] === true; }
|
|
248
258
|
|
|
249
259
|
// RSA-modulus / EC-curve helpers for the weak-key lint. Each returns null when the key
|
|
250
260
|
// material cannot be read as expected (a malformed RSA modulus, or EC parameters that are
|
package/lib/oid.js
CHANGED
|
@@ -80,7 +80,20 @@ 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 } },
|
|
84
97
|
|
|
85
98
|
// id-aca -- RFC 5755 attribute-certificate attribute types on the id-pkix 10 arc:
|
|
86
99
|
// authenticationInfo sec. 4.4.1 .. group sec. 4.4.4, plus encAttrs sec. 7.1 (the encrypted-
|
package/lib/path-validate.js
CHANGED
|
@@ -91,6 +91,12 @@ 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].
|
package/lib/schema-pkix.js
CHANGED
|
@@ -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,120 @@ 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
|
+
|
|
919
1034
|
var byOid = {};
|
|
1035
|
+
byOid[O("qcStatements")] = qcStatements;
|
|
920
1036
|
byOid[O("basicConstraints")] = basicConstraints;
|
|
921
1037
|
byOid[O("keyUsage")] = keyUsage;
|
|
922
1038
|
byOid[O("nameConstraints")] = nameConstraints;
|
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
|
-
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:c89087a8-9dba-4496-9704-cd639f3c4038",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-16T21:10:43.618Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.31",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.31",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.31",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.31",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|