@blamejs/pki 0.8.9 → 0.8.11
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 +32 -0
- package/README.md +5 -5
- package/lib/acme.js +9 -5
- package/lib/attrcert-sign.js +14 -15
- package/lib/constants.js +5 -0
- package/lib/guard-parsed.js +1 -1
- package/lib/inspect.js +59 -0
- package/lib/lint.js +3 -3
- package/lib/schema-all.js +10 -5
- package/lib/schema-attrcert.js +108 -13
- package/lib/schema-crl.js +136 -38
- package/lib/schema-crmf.js +91 -6
- package/lib/schema-csr.js +116 -5
- package/lib/schema-pkix.js +494 -4
- package/lib/schema-x509.js +97 -3
- 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/lib/schema-x509.js
CHANGED
|
@@ -35,6 +35,7 @@ var schema = require("./schema-engine");
|
|
|
35
35
|
var oid = require("./oid");
|
|
36
36
|
var frameworkError = require("./framework-error");
|
|
37
37
|
var pkix = require("./schema-pkix");
|
|
38
|
+
var intrinsic = require("./guard-intrinsic");
|
|
38
39
|
|
|
39
40
|
var CertificateError = frameworkError.CertificateError;
|
|
40
41
|
var PemError = frameworkError.PemError;
|
|
@@ -124,7 +125,7 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
|
|
|
124
125
|
build: function (e, ctx) {
|
|
125
126
|
var tbs = e.tbsMatch;
|
|
126
127
|
|
|
127
|
-
if (!e.outerSignatureAlgorithmBytes
|
|
128
|
+
if (!intrinsic.bufferEquals(e.outerSignatureAlgorithmBytes, tbs.fields.signature.node.bytes)) {
|
|
128
129
|
throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 sec. 4.1.1.2)");
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -148,7 +149,7 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
|
|
|
148
149
|
return {
|
|
149
150
|
version: version,
|
|
150
151
|
serialNumber: tbs.fields.serialNumber.value,
|
|
151
|
-
serialNumberHex: serialNode.content
|
|
152
|
+
serialNumberHex: intrinsic.bufToString(serialNode.content, "hex"),
|
|
152
153
|
signatureAlgorithm: e.signatureAlgorithm,
|
|
153
154
|
tbsSignatureAlgorithm: tbs.fields.signature.value.result,
|
|
154
155
|
issuer: issuer,
|
|
@@ -213,14 +214,107 @@ function matches(root) {
|
|
|
213
214
|
var validity = kids[i + 3];
|
|
214
215
|
return schema.isUniversal(validity, TAGS.SEQUENCE) &&
|
|
215
216
|
!!validity.children && validity.children.length === 2 &&
|
|
216
|
-
validity.children
|
|
217
|
+
intrinsic.every(validity.children, function (t) {
|
|
217
218
|
return schema.isUniversalOneOf(t, [TAGS.UTC_TIME, TAGS.GENERALIZED_TIME]);
|
|
218
219
|
});
|
|
219
220
|
}
|
|
220
221
|
|
|
222
|
+
/** @internal The certificate scope, as the shared door reads it: RFC 5280 sec. 4.2's extension
|
|
223
|
+
* table and the criticality that profile fixes. One scope, so the single-extension verb takes no
|
|
224
|
+
* `opts.scope`. */
|
|
225
|
+
var CERT_SCOPES = intrinsic.assign(intrinsic.create(null), {
|
|
226
|
+
certificate: {
|
|
227
|
+
decoders: function (ctx) { return pkix.certExtensionDecoders(ctx).byOid; },
|
|
228
|
+
criticality: pkix.certFixedCriticality(oid),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
var DOOR = pkix.extensionDoor({
|
|
232
|
+
ns: NS, ErrorClass: CertificateError, key: "x509", kind: "certificate", what: "the certificate",
|
|
233
|
+
parse: parse, scopes: CERT_SCOPES, defaultScope: "certificate",
|
|
234
|
+
segments: function (cert) { return [{ list: cert.extensions || [], scope: "certificate" }]; },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @primitive pki.schema.x509.decodeExtensions
|
|
239
|
+
* @signature pki.schema.x509.decodeExtensions(certOrBytes, opts?) -> rows
|
|
240
|
+
* @since 0.8.11
|
|
241
|
+
* @status stable
|
|
242
|
+
* @spec RFC 5280 sec. 4.2
|
|
243
|
+
* @related pki.schema.x509.parse, pki.lint.certificate, pki.inspect.certificate
|
|
244
|
+
*
|
|
245
|
+
* Read a certificate's extensions as decoded records. `parse` hands back each extension's raw
|
|
246
|
+
* `extnValue` octets, because what an extension means is a question the reader asks rather than
|
|
247
|
+
* one the parse answers; this is the verb that asks it. The argument is the object `parse`
|
|
248
|
+
* returned or the bytes `parse` takes, and a parse result is read rather than re-derived.
|
|
249
|
+
*
|
|
250
|
+
* Each row is `{ oid, name, critical, value, scope, index, state, decoded, code, profile }`, with
|
|
251
|
+
* every field always present. `state` is the discriminator: `"decoded"` when the value read as the
|
|
252
|
+
* structure its identifier names, `"unrecognized"` when this toolkit registers no decoder for that
|
|
253
|
+
* OID, and `"undecodable"` when it does and the value did not read. Read `state` rather than
|
|
254
|
+
* testing `decoded`, because a valid `ocspNoCheck` decodes to `null`.
|
|
255
|
+
*
|
|
256
|
+
* `value` carries the raw octets in every arm, byte-identical to the certificate's, for a caller
|
|
257
|
+
* recomputing a hash or comparing an encoding. `code` is the typed error code in the undecodable
|
|
258
|
+
* arm and `null` elsewhere. `profile` reports the criticality the RFC fixes for that OID, as
|
|
259
|
+
* `{ criticalExpected, citation, deviates, violations }`, and is `null` for an extension whose
|
|
260
|
+
* criticality the issuer chooses.
|
|
261
|
+
*
|
|
262
|
+
* Nothing here is a verdict. An unrecognized critical extension is reported rather than refused,
|
|
263
|
+
* whatever `opts.strict` says, because RFC 5280 sec. 4.2's rule about one is the caller's to
|
|
264
|
+
* apply and refusing it would make a certificate carrying a private critical extension
|
|
265
|
+
* unreadable. The two facts that rule needs, `critical` and `state`, are on the row.
|
|
266
|
+
*
|
|
267
|
+
* @opts
|
|
268
|
+
* - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
|
|
269
|
+
* undecodable value. It does not turn an unrecognized OID into a throw.
|
|
270
|
+
* - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call. Each may only
|
|
271
|
+
* tighten the built-in ceiling, and each bounds the decoding this call performs.
|
|
272
|
+
* @example
|
|
273
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
274
|
+
* var certDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
275
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z"),
|
|
276
|
+
* extensions: { basicConstraints: { cA: false }, subjectAltName: [{ dNSName: "leaf.example" }] } },
|
|
277
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
278
|
+
* var rows = pki.schema.x509.decodeExtensions(certDer);
|
|
279
|
+
* rows.filter(function (r) { return r.critical && r.state !== "decoded"; }).length; // -> 0
|
|
280
|
+
* // The filter above is the RFC 5280 sec. 4.2 rule: a relying party rejects a certificate whose
|
|
281
|
+
* // critical extensions it cannot read.
|
|
282
|
+
*/
|
|
283
|
+
var decodeExtensions = DOOR.decodeExtensions;
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @primitive pki.schema.x509.decodeExtension
|
|
287
|
+
* @signature pki.schema.x509.decodeExtension(ext, opts?) -> row
|
|
288
|
+
* @since 0.8.11
|
|
289
|
+
* @status stable
|
|
290
|
+
* @spec RFC 5280 sec. 4.2
|
|
291
|
+
* @related pki.schema.x509.decodeExtensions
|
|
292
|
+
*
|
|
293
|
+
* Read one certificate extension record as a decoded row. The argument is one element of a
|
|
294
|
+
* `pki.schema.x509.parse` result's `extensions`, as `{ oid, name, critical, value }`, and the row
|
|
295
|
+
* is the one `decodeExtensions` would have produced for it, with `index` 0.
|
|
296
|
+
*
|
|
297
|
+
* @opts
|
|
298
|
+
* - `strict` (boolean) -- throw the decoder's own typed error instead of reporting an
|
|
299
|
+
* undecodable value.
|
|
300
|
+
* - `maxBytes` / `maxDepth` / `maxItems` (integer) -- decode caps for this call. Each may only
|
|
301
|
+
* tighten the built-in ceiling.
|
|
302
|
+
* @example
|
|
303
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
304
|
+
* var certDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
305
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2027-01-01T00:00:00Z"),
|
|
306
|
+
* extensions: { basicConstraints: { cA: false } } },
|
|
307
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
308
|
+
* var parsed = pki.schema.x509.parse(certDer);
|
|
309
|
+
* pki.schema.x509.decodeExtension(parsed.extensions[0]).state; // -> "decoded"
|
|
310
|
+
*/
|
|
311
|
+
var decodeExtension = DOOR.decodeExtension;
|
|
312
|
+
|
|
221
313
|
module.exports = {
|
|
222
314
|
parse: parse,
|
|
223
315
|
pemDecode: pemDecode,
|
|
224
316
|
pemEncode: pemEncode,
|
|
225
317
|
matches: matches,
|
|
318
|
+
decodeExtensions: decodeExtensions,
|
|
319
|
+
decodeExtension: decodeExtension,
|
|
226
320
|
};
|
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:41170205-6ef3-4f77-9577-7ba33e5632bc",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
8
|
+
"timestamp": "2026-09-23T22:37:26.125Z",
|
|
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.11",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.11",
|
|
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.11",
|
|
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.11",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|