@blamejs/pki 0.8.9 → 0.8.10
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/lib/acme.js +9 -5
- package/lib/sigstore.js +35 -21
- package/lib/tsp-sign.js +20 -13
- 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.8.10 — 2026-09-23
|
|
8
|
+
|
|
9
|
+
A certificate's names are read the same way wherever the toolkit reads them.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `pki.tsp.verify` reports `tsp/bad-eku` for an empty extendedKeyUsage. RFC 5280 section 4.2.1.12 has at least one KeyPurposeId, and an empty one decodes as a SEQUENCE, so a reader checking only the tag called it well-formed and reported it as naming the wrong purpose, `tsp/eku-not-exclusive`. Every other consumer of that extension already answered with the malformed-extension code.
|
|
14
|
+
- The subjectAltName a timestamp token names its TSA through is read under the same rules every other consumer reads a name under, so a name this toolkit refuses elsewhere cannot be a source of TSA identity. The match itself is still a comparison of the encoded GeneralName bytes, which is what naming a TSA means: the name in the token and the name in the certificate have to be the same name.
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- `pki.sigstore.verifyBundle` refuses a Fulcio certificate whose subjectAltName carries more than one name, whatever forms those names take. It counted only rfc822Name, dNSName, uniformResourceIdentifier and otherName, and dropped every other form before counting, so a certificate pairing a URI with an iPAddress or a directoryName was accepted as binding one identity. A caller identity policy matched the form the verifier reported and never saw the other, which is the outcome the one-identity rule exists to prevent. The rule now counts every name the extension carries. A certificate carrying a single name in a form with no identity string still reports `identity.san` as `null`, unchanged, so a policy gates on the null rather than on a name nothing read.
|
|
19
|
+
|
|
7
20
|
## v0.8.9 — 2026-09-23
|
|
8
21
|
|
|
9
22
|
The 0.8.9 tarball is the one to install for everything 0.8.8 added.
|
package/lib/acme.js
CHANGED
|
@@ -578,7 +578,11 @@ var OID_AKI = oid.byName("authorityKeyIdentifier");
|
|
|
578
578
|
var OID_CN = oid.byName("commonName");
|
|
579
579
|
var _extNs = pkix.makeNS("acme", AcmeError, oid);
|
|
580
580
|
var _extDecoders = pkix.certExtensionDecoders(_extNs);
|
|
581
|
-
|
|
581
|
+
/** @internal The decoders take the value and nothing else: the error class and the OID registry
|
|
582
|
+
* they raise and resolve through come from the namespace the table was built with, above. Passing
|
|
583
|
+
* a second argument here suggested a per-call context that the table has no parameter for and
|
|
584
|
+
* silently dropped; where a decode genuinely needs resources, the way to supply them is to build
|
|
585
|
+
* the table with `schema.withResources`, as `schema-attrcert.js` does. */
|
|
582
586
|
|
|
583
587
|
/**
|
|
584
588
|
* @primitive pki.acme.tlsAlpn01Extension
|
|
@@ -657,7 +661,7 @@ async function verifyTlsAlpn01(certDer, token, accountJwk, identifier) {
|
|
|
657
661
|
if (!auth.equals(expected)) throw E("acme/bad-tlsalpn", "the acmeIdentifier digest does not match the key authorization");
|
|
658
662
|
var sanExt = exts.filter(function (e) { return e.oid === OID_SAN; })[0];
|
|
659
663
|
if (!sanExt) throw E("acme/bad-tlsalpn", "the validation certificate is missing the SubjectAltName");
|
|
660
|
-
var san = _extDecoders.byOid[OID_SAN](sanExt.value
|
|
664
|
+
var san = _extDecoders.byOid[OID_SAN](sanExt.value);
|
|
661
665
|
if (!san.names || san.names.length !== 1) throw E("acme/bad-tlsalpn", "the SubjectAltName must carry EXACTLY ONE entry (RFC 8737 sec. 3)");
|
|
662
666
|
var entry = san.names[0];
|
|
663
667
|
if (!_isObject(identifier) || !_isString(identifier.type)) throw E("acme/bad-input", "an identifier { type, value } is required");
|
|
@@ -914,7 +918,7 @@ function _csrIdentifierSet(parsedCsr) {
|
|
|
914
918
|
if (a.type !== oid.byName("extensionRequest") || !_isArrayI(a.extensions)) return;
|
|
915
919
|
_forEach(a.extensions, function (e) {
|
|
916
920
|
if (e.oid !== OID_SAN) return;
|
|
917
|
-
var dec = _extDecoders.byOid[OID_SAN](e.value
|
|
921
|
+
var dec = _extDecoders.byOid[OID_SAN](e.value);
|
|
918
922
|
_forEach(dec.names || [], function (n) { _addSanName(set, n, "the finalize CSR"); });
|
|
919
923
|
});
|
|
920
924
|
});
|
|
@@ -981,7 +985,7 @@ function _certIdentifierSet(parsedCert) {
|
|
|
981
985
|
var set = intrinsic.create(null);
|
|
982
986
|
_forEach(parsedCert.extensions || [], function (e) {
|
|
983
987
|
if (e.oid !== OID_SAN) return;
|
|
984
|
-
var dec = _extDecoders.byOid[OID_SAN](e.value
|
|
988
|
+
var dec = _extDecoders.byOid[OID_SAN](e.value);
|
|
985
989
|
_forEach(dec.names || [], function (n) { _addSanName(set, n, "the downloaded certificate"); });
|
|
986
990
|
});
|
|
987
991
|
if (_objectKeys(set).length === 0) {
|
|
@@ -1249,7 +1253,7 @@ function ariCertId(certDer) {
|
|
|
1249
1253
|
var cert = x509.parse(certDer);
|
|
1250
1254
|
var akiExt = (cert.extensions || []).filter(function (e) { return e.oid === OID_AKI; })[0];
|
|
1251
1255
|
if (!akiExt) throw E("acme/bad-certid", "the certificate has no authorityKeyIdentifier extension (RFC 9773 sec. 4.1)");
|
|
1252
|
-
var aki = _extDecoders.byOid[OID_AKI](akiExt.value
|
|
1256
|
+
var aki = _extDecoders.byOid[OID_AKI](akiExt.value);
|
|
1253
1257
|
if (!aki || !Buffer.isBuffer(aki.keyIdentifier)) throw E("acme/bad-certid", "the authorityKeyIdentifier has no keyIdentifier field (RFC 9773 sec. 4.1)");
|
|
1254
1258
|
var serialBytes = Buffer.from(cert.serialNumberHex, "hex");
|
|
1255
1259
|
return jose.base64url.encode(aki.keyIdentifier) + "." + jose.base64url.encode(serialBytes);
|
package/lib/sigstore.js
CHANGED
|
@@ -1055,6 +1055,13 @@ async function _verifyChain(leaf, chainDers, fulcioRoots, timeMs) {
|
|
|
1055
1055
|
|
|
1056
1056
|
|
|
1057
1057
|
var SAN_OID = oid.byName("subjectAltName");
|
|
1058
|
+
/** @internal The shared table's entry, so the SAN this verb reads is read under the rules every
|
|
1059
|
+
* other consumer of a GeneralNames reads it under. It is built on the certificate namespace rather
|
|
1060
|
+
* than this module's, so a malformed name raises a CertificateError that `_identity` re-types to
|
|
1061
|
+
* `sigstore/bad-certificate` with that fault as its cause. That is the code this module already
|
|
1062
|
+
* reports for a certificate it cannot read, so the fix adds no new code to the public surface. */
|
|
1063
|
+
var _sanDecoder = pkix.certExtensionDecoders(
|
|
1064
|
+
pkix.makeNS("x509", frameworkError.CertificateError, oid)).byOid[SAN_OID];
|
|
1058
1065
|
var FULCIO_PREFIX = _join(_arraySlice(_split(oid.byName("otherName"), "."), 0, -1), ".") + ".";
|
|
1059
1066
|
function _identity(leaf) {
|
|
1060
1067
|
var out = { san: null, extensions: _create(null) };
|
|
@@ -1072,29 +1079,36 @@ function _identity(leaf) {
|
|
|
1072
1079
|
return out;
|
|
1073
1080
|
}
|
|
1074
1081
|
var GN_TYPE = _assign(_create(null), { 1: "rfc822Name", 2: "dNSName", 6: "uri" });
|
|
1082
|
+
/** @internal The GeneralNames come from the shared extension decoder, which is where the RFC 5280
|
|
1083
|
+
* sec. 4.2.1.6 rules live: at least one name, a non-empty IA5String for the text forms, and no
|
|
1084
|
+
* control byte inside one (CVE-2009-2408). Reading the SEQUENCE here instead left this verb
|
|
1085
|
+
* accepting a dNSName carrying a NUL or a CR and handing it on as the signer's identity.
|
|
1086
|
+
*
|
|
1087
|
+
* The one-identity rule counts every name the SAN carries, not the subset this verb turns into an
|
|
1088
|
+
* identity string. Counting only the recognized forms let a certificate pair a URI with an
|
|
1089
|
+
* iPAddress and pass as single-identity, with the second binding invisible to the caller's policy
|
|
1090
|
+
* -- which is the thing the rule exists to stop. A SAN of one name this verb has no string form
|
|
1091
|
+
* for still yields null, so a policy gates on the null rather than on a name nothing read. */
|
|
1075
1092
|
function _sanValue(ext) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
if (val) guard.list.append(names, val);
|
|
1093
|
+
/** @internal The shared decoder is the gate: it applies the sec. 4.2.1.6 rules and throws on a
|
|
1094
|
+
* name that breaks one. What it returns is a GeneralNames view rather than the codec nodes the
|
|
1095
|
+
* mapping below reads, so the count comes from it and the one name is then taken from the raw
|
|
1096
|
+
* decode, which carries the node shape `octetStringImplicit` and the otherName walk need. */
|
|
1097
|
+
var validated = _sanDecoder(ext.value).names || [];
|
|
1098
|
+
if (validated.length > 1) throw _err("sigstore/bad-certificate", "the Fulcio certificate carries multiple SAN identities");
|
|
1099
|
+
var n = (asn1.decode(ext.value).children || [])[0];
|
|
1100
|
+
if (!n || n.tagClass !== "context") return null;
|
|
1101
|
+
if (!n.constructed && GN_TYPE[n.tagNumber]) {
|
|
1102
|
+
return { type: GN_TYPE[n.tagNumber], value: _bufToString(asn1.read.octetStringImplicit(n, n.tagNumber), "utf8") };
|
|
1103
|
+
}
|
|
1104
|
+
if (n.constructed && n.tagNumber === 0 && (n.children || []).length >= 2) {
|
|
1105
|
+
var inner = (n.children[1].children || [])[0] || n.children[1];
|
|
1106
|
+
var v;
|
|
1107
|
+
try { v = asn1.read.string(inner); }
|
|
1108
|
+
catch (_e) { v = null; } // allow:swallow-unverified an otherName body with no string form has no identity string, which the caller's policy then gates on
|
|
1109
|
+
return { type: "otherName", oid: asn1.read.oid(n.children[0]), value: v };
|
|
1095
1110
|
}
|
|
1096
|
-
|
|
1097
|
-
return names[0] || null;
|
|
1111
|
+
return null;
|
|
1098
1112
|
}
|
|
1099
1113
|
function _fulcioExtValue(ext, leafArc) {
|
|
1100
1114
|
if (!_isBuffer(ext.value)) throw _err("sigstore/bad-certificate", "Fulcio extension " + ext.oid + " has no value");
|
package/lib/tsp-sign.js
CHANGED
|
@@ -34,6 +34,9 @@ var pkiBuild = require("./pki-build");
|
|
|
34
34
|
var constants = require("./constants");
|
|
35
35
|
var TspError = frameworkError.TspError;
|
|
36
36
|
var _NS = pkix.makeNS("tsp", TspError, oid);
|
|
37
|
+
/** @internal The shared certificate-extension decoders, so an extension this module reads is read
|
|
38
|
+
* under the rules every other consumer reads it under rather than under a second set written here. */
|
|
39
|
+
var _EXT_DECODERS = pkix.certExtensionDecoders(_NS).byOid;
|
|
37
40
|
var b = asn1.build;
|
|
38
41
|
function _err(code, message, cause) { return new TspError(code, message, cause); }
|
|
39
42
|
|
|
@@ -487,12 +490,13 @@ function _checkTsaCertUsage(tsaCertDer) {
|
|
|
487
490
|
var exts = (cert.extensions || []).filter(function (e) { return e.oid === O("extKeyUsage"); });
|
|
488
491
|
if (exts.length !== 1) return "tsp/bad-eku";
|
|
489
492
|
if (!exts[0].critical) return "tsp/eku-not-critical";
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
493
|
+
/** @internal Read through the shared decoder, which is where the sec. 4.2.1.12 rules live: an
|
|
494
|
+
* ExtKeyUsage carries at least one KeyPurposeId. Reading the SEQUENCE here instead called an
|
|
495
|
+
* empty one well-formed and then reported it as the wrong PURPOSE, so the same class of fault
|
|
496
|
+
* answered with a different code here than at every other consumer of this extension. */
|
|
497
|
+
var purposes;
|
|
498
|
+
try { purposes = _EXT_DECODERS[O("extKeyUsage")](exts[0].value); }
|
|
499
|
+
catch (_e) { return "tsp/bad-eku"; } // allow:swallow-unverified this verb answers with a code rather than throwing, and every malformed shape is the one code
|
|
496
500
|
if (purposes.length !== 1 || purposes[0] !== O("timeStamping")) return "tsp/eku-not-exclusive";
|
|
497
501
|
var ku;
|
|
498
502
|
try { ku = pkix.keyUsageOf(_NS, cert, _err, "tsp/bad-key-usage", "TSA certificate"); }
|
|
@@ -508,13 +512,16 @@ function _tsaMatchesCert(tsaBytes, signerCert) {
|
|
|
508
512
|
if (gn.tagNumber === 4 && gn.value && guard.name.dnEqual(gn.value.rdns, signerCert.subject.rdns, _err, "tsp/tsa-mismatch", "the TSA subject")) return true;
|
|
509
513
|
var sanExt = (signerCert.extensions || []).filter(function (e) { return e.oid === O("subjectAltName"); })[0];
|
|
510
514
|
if (sanExt) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
515
|
+
/** @internal Gated by the shared decoder before the comparison, so a SAN this toolkit refuses
|
|
516
|
+
* everywhere else cannot be a source of identity here. The match itself stays a comparison of
|
|
517
|
+
* the encoded GeneralName bytes, which is what "the token names this TSA" means: the name in
|
|
518
|
+
* the token and the name in the certificate have to be the same name, not merely names that
|
|
519
|
+
* decode alike. */
|
|
520
|
+
var names;
|
|
521
|
+
try { names = _EXT_DECODERS[O("subjectAltName")](sanExt.value).names || []; }
|
|
522
|
+
catch (_e) { return false; } // allow:swallow-unverified a SAN that does not decode names no TSA, which is what this verb answers
|
|
523
|
+
for (var i = 0; i < names.length; i++) {
|
|
524
|
+
if (names[i].bytes && names[i].bytes.equals(tsaBytes)) return true;
|
|
518
525
|
}
|
|
519
526
|
}
|
|
520
527
|
return false;
|
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:4cdebd00-c3c0-4ba6-9d9c-daacb902163c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-23T19:
|
|
8
|
+
"timestamp": "2026-09-23T19:58:00.767Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.10",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.10",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.8.10",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.10",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|