@blamejs/pki 0.1.14 → 0.1.16
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 +33 -1
- package/README.md +3 -2
- package/index.js +17 -9
- package/lib/asn1-der.js +3 -2
- package/lib/constants.js +4 -0
- package/lib/framework-error.js +8 -0
- package/lib/oid.js +8 -3
- package/lib/path-validate.js +1393 -0
- package/lib/schema-cms.js +332 -15
- package/lib/schema-crl.js +5 -0
- package/lib/schema-engine.js +6 -5
- package/lib/schema-pkix.js +324 -13
- package/lib/webcrypto.js +2 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/schema-pkix.js
CHANGED
|
@@ -129,12 +129,18 @@ function versionReader(ns, accept) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
// opts.implicitTag (optional): read the AlgorithmIdentifier as a [tag] IMPLICIT
|
|
133
|
+
// SEQUENCE (a context-class constructed node whose children are algorithm + parameters),
|
|
134
|
+
// for the RFC 5652 §6.2.4 pwri.keyDerivationAlgorithm [0]. With no opts the shape is a
|
|
135
|
+
// universal SEQUENCE, byte-identical to every existing caller.
|
|
136
|
+
function algorithmIdentifier(ns, opts) {
|
|
137
|
+
opts = opts || {};
|
|
133
138
|
return schema.seq([
|
|
134
139
|
schema.field("algorithm", schema.oidLeaf()),
|
|
135
140
|
schema.optional("parameters", schema.any(), { whenAny: true }),
|
|
136
141
|
], {
|
|
137
|
-
assert:
|
|
142
|
+
assert: opts.implicitTag != null ? "implicit" : "sequence", implicitTag: opts.implicitTag,
|
|
143
|
+
arity: { min: 1 }, code: ns.prefix + "/bad-algorithm-identifier", what: "AlgorithmIdentifier",
|
|
138
144
|
build: function (m, ctx) {
|
|
139
145
|
var dotted = m.fields.algorithm.value;
|
|
140
146
|
return { oid: dotted, name: ctx.oid.name(dotted) || null, parameters: m.fields.parameters.present ? m.fields.parameters.node.bytes : null };
|
|
@@ -218,11 +224,22 @@ function name(ns) {
|
|
|
218
224
|
// OCTET STRING; registeredID [8] is a primitive OBJECT IDENTIFIER — then surfaces the
|
|
219
225
|
// value RAW ({ bytes, tagClass, tagNumber }). `opts.code` is the caller's error code
|
|
220
226
|
// (e.g. ocsp/bad-requestor-name, tsp/bad-tsa). Shared so the two parsers cannot drift.
|
|
227
|
+
// opts.decodeValue (default false): in addition to the raw { bytes, tagClass,
|
|
228
|
+
// tagNumber }, surface the DECODED `value` per arm — IA5 text (string), an
|
|
229
|
+
// iPAddress Buffer, a directoryName { rdns, dn }, a registeredID OID string,
|
|
230
|
+
// or an otherName { typeId, valueBytes }. The path validator's name-constraint
|
|
231
|
+
// matcher needs the decoded value; the tsp/ocsp/attrcert consumers pass no
|
|
232
|
+
// flag and get the byte-identical raw-only shape.
|
|
233
|
+
// opts.subtreeBase (default false): the GeneralSubtree.base form (RFC 5280
|
|
234
|
+
// §4.2.1.10) — an iPAddress base is an address+mask (8 octets IPv4 / 32 IPv6),
|
|
235
|
+
// NOT the 4/16-octet SAN address form. Only the iPAddress size rule changes.
|
|
221
236
|
var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
|
|
222
237
|
var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
|
|
223
238
|
function generalName(ns, opts) {
|
|
224
239
|
opts = opts || {};
|
|
225
240
|
var code = opts.code || (ns.prefix + "/bad-general-name");
|
|
241
|
+
var decodeValue = opts.decodeValue === true;
|
|
242
|
+
var subtreeBase = opts.subtreeBase === true;
|
|
226
243
|
var NAME = name(ns);
|
|
227
244
|
return schema.decode(function (n, ctx) {
|
|
228
245
|
if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
|
|
@@ -230,45 +247,66 @@ function generalName(ns, opts) {
|
|
|
230
247
|
}
|
|
231
248
|
var t = n.tagNumber;
|
|
232
249
|
var constructed = !!n.children;
|
|
250
|
+
var value;
|
|
233
251
|
if (GN_CONSTRUCTED[t]) {
|
|
234
252
|
if (!constructed || n.children.length < 1) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280 §4.2.1.6)");
|
|
235
253
|
if (t === 0) {
|
|
236
254
|
// otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }.
|
|
237
255
|
if (n.children.length !== 2) throw ctx.E(code, "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] }");
|
|
238
|
-
|
|
256
|
+
var typeId;
|
|
257
|
+
try { typeId = asn1.read.oid(n.children[0]); }
|
|
239
258
|
catch (e) { throw ctx.E(code, "GeneralName otherName [0] must lead with a type-id OBJECT IDENTIFIER", e); }
|
|
240
|
-
var
|
|
241
|
-
if (!(
|
|
259
|
+
var ov = n.children[1];
|
|
260
|
+
if (!(ov.tagClass === "context" && ov.tagNumber === 0 && ov.children && ov.children.length === 1)) {
|
|
242
261
|
throw ctx.E(code, "GeneralName otherName [0] value must be a [0] EXPLICIT wrapper carrying exactly one value");
|
|
243
262
|
}
|
|
263
|
+
if (decodeValue) value = { typeId: typeId, valueBytes: ov.children[0].bytes };
|
|
244
264
|
} else if (t === 4) {
|
|
245
265
|
// directoryName [4] EXPLICIT Name — validate the wrapped RDNSequence.
|
|
246
266
|
if (n.children.length !== 1) throw ctx.E(code, "GeneralName directoryName [4] must wrap exactly one Name");
|
|
247
|
-
schema.walk(NAME, n.children[0], ctx);
|
|
267
|
+
var dnMatch = schema.walk(NAME, n.children[0], ctx);
|
|
268
|
+
if (decodeValue) value = dnMatch.result;
|
|
248
269
|
}
|
|
249
270
|
} else {
|
|
250
271
|
if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 §10.2)");
|
|
251
272
|
if (GN_IA5[t]) {
|
|
252
273
|
if (n.content.length === 0) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty IA5String");
|
|
253
274
|
for (var i = 0; i < n.content.length; i++) {
|
|
254
|
-
|
|
275
|
+
// 7-bit IA5, and no C0/DEL control byte — an embedded NUL/control in a
|
|
276
|
+
// dNSName/rfc822Name/URI enables a name-truncation/confusion bypass
|
|
277
|
+
// (CVE-2009-2408 class) downstream, so reject it at decode.
|
|
278
|
+
if (n.content[i] < 0x20 || n.content[i] > 0x7e) throw ctx.E(code, "GeneralName [" + t + "] must be a printable IA5String (no control bytes)");
|
|
255
279
|
}
|
|
280
|
+
if (decodeValue) value = n.content.toString("latin1");
|
|
256
281
|
} else if (t === 7) {
|
|
257
|
-
if (
|
|
282
|
+
if (subtreeBase) {
|
|
283
|
+
if (n.content.length !== 8 && n.content.length !== 32) throw ctx.E(code, "GeneralName iPAddress [7] subtree base must be an 8- or 32-octet address+mask (RFC 5280 §4.2.1.10)");
|
|
284
|
+
} else if (n.content.length !== 4 && n.content.length !== 16) {
|
|
285
|
+
throw ctx.E(code, "GeneralName iPAddress [7] must be a 4- or 16-octet address");
|
|
286
|
+
}
|
|
287
|
+
// n.content is proven non-null here (the primitive-form guard above
|
|
288
|
+
// threw on a constructed node); copy the octets out.
|
|
289
|
+
if (decodeValue) value = Buffer.concat([n.content]);
|
|
258
290
|
} else if (t === 8) {
|
|
259
|
-
|
|
291
|
+
var regId;
|
|
292
|
+
try { regId = asn1.decodeOidContent(n.content); }
|
|
260
293
|
catch (e) { throw ctx.E(code, "GeneralName registeredID [8] must be a valid OBJECT IDENTIFIER", e); }
|
|
294
|
+
if (decodeValue) value = regId;
|
|
261
295
|
}
|
|
262
296
|
}
|
|
263
|
-
|
|
297
|
+
var out = { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
|
|
298
|
+
if (decodeValue) out.value = value === undefined ? null : value;
|
|
299
|
+
return out;
|
|
264
300
|
});
|
|
265
301
|
}
|
|
266
302
|
|
|
267
303
|
// GeneralNames ::= SEQUENCE OF GeneralName (RFC 5280 §4.2.1.6), SIZE (1..MAX). Every
|
|
268
304
|
// element is validated by generalName (its CHOICE alternative's form + content) and
|
|
269
305
|
// surfaced raw, so a caller carrying a GeneralNames field cannot accept a malformed
|
|
270
|
-
// element by treating the whole sequence as opaque bytes. Returns { names
|
|
271
|
-
//
|
|
306
|
+
// element by treating the whole sequence as opaque bytes. Returns { names, bytes }
|
|
307
|
+
// where each `names[i]` is the generalName leaf's return — { bytes, tagClass,
|
|
308
|
+
// tagNumber } and, when opts.decodeValue is set, the decoded `value` — and `bytes`
|
|
309
|
+
// is the raw outer DER.
|
|
272
310
|
// `opts.implicitTag` handles a [tag] IMPLICIT GeneralNames — the context tag REPLACES
|
|
273
311
|
// the universal SEQUENCE tag (RFC 5755 Holder.entityName [1]); otherwise it is a bare
|
|
274
312
|
// universal SEQUENCE OF. `opts.code` is the caller's error code. Shared so the x509 /
|
|
@@ -276,7 +314,9 @@ function generalName(ns, opts) {
|
|
|
276
314
|
function generalNames(ns, opts) {
|
|
277
315
|
opts = opts || {};
|
|
278
316
|
var code = opts.code || (ns.prefix + "/bad-general-names");
|
|
279
|
-
|
|
317
|
+
// decodeValue / subtreeBase thread through to each element (the path
|
|
318
|
+
// validator's constraint matcher passes decodeValue:true).
|
|
319
|
+
var gn = generalName(ns, { code: code, decodeValue: opts.decodeValue === true, subtreeBase: opts.subtreeBase === true });
|
|
280
320
|
function build(m) { return { names: m.items.map(function (it) { return it.value; }), bytes: m.node.bytes }; }
|
|
281
321
|
if (opts.implicitTag != null) {
|
|
282
322
|
return schema.implicitSeqOf(opts.implicitTag, gn, { min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
@@ -284,6 +324,276 @@ function generalNames(ns, opts) {
|
|
|
284
324
|
return schema.seqOf(gn, { assert: "sequence", min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
285
325
|
}
|
|
286
326
|
|
|
327
|
+
// certExtensionDecoders(ns) — the ns-parameterized RFC 5280 §4.2.1 extension
|
|
328
|
+
// VALUE decoders. `x509.parse` surfaces each extension as { oid, name, critical,
|
|
329
|
+
// value } with `value` the raw inner OCTET-STRING content (a Buffer); the path
|
|
330
|
+
// validator and the future complete-extension-set surface need the STRUCTURE
|
|
331
|
+
// inside. Each decoder takes that raw Buffer and returns the decoded value, or
|
|
332
|
+
// throws a typed `<prefix>/bad-*` code, mirroring the CRL's fail-closed
|
|
333
|
+
// `decodeExt` pattern (asn1.decode → read.* / schema.walk, wrapped). Returns
|
|
334
|
+
// { byOid } keyed by dotted OID so a consumer dispatches by ext.oid.
|
|
335
|
+
var _T = asn1.TAGS;
|
|
336
|
+
function certExtensionDecoders(ns) {
|
|
337
|
+
var GN_SUBTREE = generalName(ns, { decodeValue: true, subtreeBase: true, code: ns.prefix + "/bad-name-constraints" });
|
|
338
|
+
|
|
339
|
+
function decodeTop(buf, code, what) {
|
|
340
|
+
var n;
|
|
341
|
+
try { n = asn1.decode(buf); }
|
|
342
|
+
catch (e) { throw ns.E(code, "malformed " + what + " extension value: " + ((e && e.message) || String(e)), e); }
|
|
343
|
+
return n;
|
|
344
|
+
}
|
|
345
|
+
function seqChildren(buf, code, what) {
|
|
346
|
+
var n = decodeTop(buf, code, what);
|
|
347
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.SEQUENCE || !n.children) {
|
|
348
|
+
throw ns.E(code, what + " must be a SEQUENCE (RFC 5280 §4.2.1)");
|
|
349
|
+
}
|
|
350
|
+
return n.children;
|
|
351
|
+
}
|
|
352
|
+
function readInt(node, code, what) {
|
|
353
|
+
try { return asn1.read.integer(node); }
|
|
354
|
+
catch (e) { throw ns.E(code, what + " must be an INTEGER", e); }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// basicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLen INTEGER (0..MAX) OPTIONAL }
|
|
358
|
+
function basicConstraints(buf) {
|
|
359
|
+
var C = ns.prefix + "/bad-basic-constraints";
|
|
360
|
+
var kids = seqChildren(buf, C, "BasicConstraints");
|
|
361
|
+
var i = 0, cA = false, pathLen = null;
|
|
362
|
+
if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.BOOLEAN) {
|
|
363
|
+
var v;
|
|
364
|
+
try { v = asn1.read.boolean(kids[i]); } catch (e) { throw ns.E(C, "BasicConstraints cA must be a BOOLEAN", e); }
|
|
365
|
+
if (v !== true) throw ns.E(C, "BasicConstraints cA DEFAULT FALSE must be omitted, not an explicit FALSE (X.690 §11.5)");
|
|
366
|
+
cA = true; i++;
|
|
367
|
+
}
|
|
368
|
+
if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.INTEGER) {
|
|
369
|
+
if (!cA) throw ns.E(C, "BasicConstraints pathLenConstraint is only permitted when cA is TRUE (RFC 5280 §4.2.1.9)");
|
|
370
|
+
var pl = readInt(kids[i], C, "pathLenConstraint");
|
|
371
|
+
if (pl < 0n) throw ns.E(C, "BasicConstraints pathLenConstraint must be non-negative");
|
|
372
|
+
pathLen = Number(pl); i++;
|
|
373
|
+
}
|
|
374
|
+
if (i !== kids.length) throw ns.E(C, "BasicConstraints has unexpected trailing fields");
|
|
375
|
+
return { cA: cA, pathLenConstraint: pathLen };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// keyUsage ::= BIT STRING (named bits). At least one bit MUST be set.
|
|
379
|
+
var KU_BITS = ["digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment",
|
|
380
|
+
"keyAgreement", "keyCertSign", "cRLSign", "encipherOnly", "decipherOnly"];
|
|
381
|
+
function keyUsage(buf) {
|
|
382
|
+
var C = ns.prefix + "/bad-key-usage";
|
|
383
|
+
var n = decodeTop(buf, C, "KeyUsage");
|
|
384
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.BIT_STRING) throw ns.E(C, "KeyUsage must be a BIT STRING (RFC 5280 §4.2.1.3)");
|
|
385
|
+
var bs;
|
|
386
|
+
try { bs = asn1.read.bitString(n); } catch (e) { throw ns.E(C, "KeyUsage must be a well-formed BIT STRING", e); }
|
|
387
|
+
// At least one bit MUST be set (RFC 5280 §4.2.1.3) — an all-zero value (a
|
|
388
|
+
// non-empty byte run of only zero bits, e.g. 03 02 07 00) is malformed too.
|
|
389
|
+
var anyBit = false;
|
|
390
|
+
for (var z = 0; z < bs.bytes.length; z++) { if (bs.bytes[z] !== 0) { anyBit = true; break; } }
|
|
391
|
+
if (!anyBit) throw ns.E(C, "KeyUsage must assert at least one bit (RFC 5280 §4.2.1.3)");
|
|
392
|
+
// KeyUsage is a NamedBitList: DER (X.690 §11.2.2) strips all trailing zero
|
|
393
|
+
// bits, so the last content octet must be non-zero and the lowest USED bit
|
|
394
|
+
// (at position unusedBits) must be set — one canonical encoding per value.
|
|
395
|
+
var last = bs.bytes[bs.bytes.length - 1];
|
|
396
|
+
if (last === 0 || ((last >> bs.unusedBits) & 1) !== 1) {
|
|
397
|
+
throw ns.E(C, "KeyUsage must use the minimal DER NamedBitList encoding (no trailing zero bits, X.690 §11.2.2)");
|
|
398
|
+
}
|
|
399
|
+
var out = {};
|
|
400
|
+
KU_BITS.forEach(function (nm, bit) {
|
|
401
|
+
var byte = bit >> 3, mask = 0x80 >> (bit & 7);
|
|
402
|
+
out[nm] = byte < bs.bytes.length && (bs.bytes[byte] & mask) !== 0;
|
|
403
|
+
});
|
|
404
|
+
return out;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// GeneralSubtree ::= SEQUENCE { base GeneralName, minimum [0] DEFAULT 0, maximum [1] OPTIONAL }
|
|
408
|
+
function subtreeList(node, C) {
|
|
409
|
+
// node is the [0]/[1] IMPLICIT SEQUENCE OF GeneralSubtree (context-constructed).
|
|
410
|
+
// GeneralSubtrees is SIZE(1..MAX) — an explicitly present but empty subtree
|
|
411
|
+
// list (a0 00) absorbs no constraint and is malformed, not a no-op.
|
|
412
|
+
if (!node.children || node.children.length < 1) throw ns.E(C, "NameConstraints permittedSubtrees/excludedSubtrees must be a non-empty GeneralSubtrees (SIZE 1..MAX, RFC 5280 §4.2.1.10)");
|
|
413
|
+
return node.children.map(function (st) {
|
|
414
|
+
if (st.tagClass !== "universal" || st.tagNumber !== _T.SEQUENCE || !st.children || st.children.length < 1) {
|
|
415
|
+
throw ns.E(C, "GeneralSubtree must be a SEQUENCE { base, minimum?, maximum? }");
|
|
416
|
+
}
|
|
417
|
+
var base = schema.walk(GN_SUBTREE, st.children[0], ns);
|
|
418
|
+
for (var j = 1; j < st.children.length; j++) {
|
|
419
|
+
var f = st.children[j];
|
|
420
|
+
if (f.tagClass !== "context" || (f.tagNumber !== 0 && f.tagNumber !== 1)) throw ns.E(C, "GeneralSubtree has an unexpected field");
|
|
421
|
+
if (f.tagNumber === 0) throw ns.E(C, "GeneralSubtree minimum DEFAULT 0 must be omitted (RFC 5280 §4.2.1.10 requires minimum = 0)");
|
|
422
|
+
if (f.tagNumber === 1) throw ns.E(C, "GeneralSubtree maximum is not permitted in the RFC 5280 profile (§4.2.1.10)");
|
|
423
|
+
}
|
|
424
|
+
return { base: base };
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
function nameConstraints(buf) {
|
|
428
|
+
var C = ns.prefix + "/bad-name-constraints";
|
|
429
|
+
var kids = seqChildren(buf, C, "NameConstraints");
|
|
430
|
+
var permitted = [], excluded = [], sawP = false, sawE = false, ncLastTag = -1;
|
|
431
|
+
kids.forEach(function (k) {
|
|
432
|
+
if (k.tagClass !== "context" || !k.children) throw ns.E(C, "NameConstraints fields are [0] permittedSubtrees / [1] excludedSubtrees");
|
|
433
|
+
if (k.tagNumber <= ncLastTag) throw ns.E(C, "NameConstraints fields must be unique and in ascending order (DER)");
|
|
434
|
+
ncLastTag = k.tagNumber;
|
|
435
|
+
if (k.tagNumber === 0) { if (sawP) throw ns.E(C, "duplicate permittedSubtrees"); sawP = true; permitted = subtreeList(k, C); }
|
|
436
|
+
else if (k.tagNumber === 1) { if (sawE) throw ns.E(C, "duplicate excludedSubtrees"); sawE = true; excluded = subtreeList(k, C); }
|
|
437
|
+
else throw ns.E(C, "NameConstraints has an unexpected field [" + k.tagNumber + "]");
|
|
438
|
+
});
|
|
439
|
+
if (!sawP && !sawE) throw ns.E(C, "NameConstraints must contain permittedSubtrees or excludedSubtrees (RFC 5280 §4.2.1.10)");
|
|
440
|
+
return { permittedSubtrees: permitted, excludedSubtrees: excluded };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// certificatePolicies ::= SEQUENCE SIZE(1..MAX) OF PolicyInformation
|
|
444
|
+
function certificatePolicies(buf) {
|
|
445
|
+
var C = ns.prefix + "/bad-policy";
|
|
446
|
+
var kids = seqChildren(buf, C, "CertificatePolicies");
|
|
447
|
+
if (kids.length < 1) throw ns.E(C, "CertificatePolicies must contain at least one PolicyInformation (RFC 5280 §4.2.1.4)");
|
|
448
|
+
var seen = {};
|
|
449
|
+
return kids.map(function (pi) {
|
|
450
|
+
// PolicyInformation ::= SEQUENCE { policyIdentifier, policyQualifiers
|
|
451
|
+
// SEQUENCE SIZE(1..MAX) OPTIONAL } — exactly one or two fields; a second
|
|
452
|
+
// field, if present, MUST be a SEQUENCE. Extra/mis-typed fields are malformed.
|
|
453
|
+
if (pi.tagClass !== "universal" || pi.tagNumber !== _T.SEQUENCE || !pi.children || pi.children.length < 1 || pi.children.length > 2) {
|
|
454
|
+
throw ns.E(C, "PolicyInformation must be a SEQUENCE { policyIdentifier, policyQualifiers? }");
|
|
455
|
+
}
|
|
456
|
+
var pid;
|
|
457
|
+
try { pid = asn1.read.oid(pi.children[0]); } catch (e) { throw ns.E(C, "PolicyInformation policyIdentifier must be an OBJECT IDENTIFIER", e); }
|
|
458
|
+
if (seen[pid]) throw ns.E(C, "duplicate policy OID " + pid + " (RFC 5280 §4.2.1.4)");
|
|
459
|
+
seen[pid] = true;
|
|
460
|
+
var qualifiers = null;
|
|
461
|
+
if (pi.children.length > 1) {
|
|
462
|
+
var q = pi.children[1];
|
|
463
|
+
if (q.tagClass !== "universal" || q.tagNumber !== _T.SEQUENCE || !q.children || !q.children.length) {
|
|
464
|
+
throw ns.E(C, "PolicyInformation policyQualifiers must be a non-empty SEQUENCE (RFC 5280 §4.2.1.4)");
|
|
465
|
+
}
|
|
466
|
+
// Each element is a PolicyQualifierInfo ::= SEQUENCE { policyQualifierId
|
|
467
|
+
// OID, qualifier ANY DEFINED BY policyQualifierId } — EXACTLY two members
|
|
468
|
+
// (qualifier is not OPTIONAL). A SEQUENCE missing the qualifier or
|
|
469
|
+
// carrying trailing extra fields is malformed; the qualifier body itself
|
|
470
|
+
// stays opaque.
|
|
471
|
+
q.children.forEach(function (pq) {
|
|
472
|
+
if (pq.tagClass !== "universal" || pq.tagNumber !== _T.SEQUENCE || !pq.children || pq.children.length !== 2) {
|
|
473
|
+
throw ns.E(C, "policyQualifiers element must be a PolicyQualifierInfo SEQUENCE { policyQualifierId, qualifier } (RFC 5280 §4.2.1.4)");
|
|
474
|
+
}
|
|
475
|
+
try { asn1.read.oid(pq.children[0]); } catch (e) { throw ns.E(C, "PolicyQualifierInfo must lead with a policyQualifierId OID", e); }
|
|
476
|
+
});
|
|
477
|
+
qualifiers = q.bytes;
|
|
478
|
+
}
|
|
479
|
+
return { policyIdentifier: pid, qualifiersBytes: qualifiers };
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// policyMappings ::= SEQUENCE SIZE(1..MAX) OF SEQUENCE { issuerDomainPolicy, subjectDomainPolicy }
|
|
484
|
+
function policyMappings(buf) {
|
|
485
|
+
var C = ns.prefix + "/bad-policy";
|
|
486
|
+
var kids = seqChildren(buf, C, "PolicyMappings");
|
|
487
|
+
if (kids.length < 1) throw ns.E(C, "PolicyMappings must contain at least one mapping (RFC 5280 §4.2.1.5)");
|
|
488
|
+
return kids.map(function (mp) {
|
|
489
|
+
if (mp.tagClass !== "universal" || mp.tagNumber !== _T.SEQUENCE || !mp.children || mp.children.length !== 2) {
|
|
490
|
+
throw ns.E(C, "policy mapping must be a SEQUENCE { issuerDomainPolicy, subjectDomainPolicy }");
|
|
491
|
+
}
|
|
492
|
+
var idp, sdp;
|
|
493
|
+
try { idp = asn1.read.oid(mp.children[0]); sdp = asn1.read.oid(mp.children[1]); }
|
|
494
|
+
catch (e) { throw ns.E(C, "policy mapping members must be OBJECT IDENTIFIERs", e); }
|
|
495
|
+
return { issuerDomainPolicy: idp, subjectDomainPolicy: sdp };
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// policyConstraints ::= SEQUENCE { requireExplicitPolicy [0] INTEGER OPT, inhibitPolicyMapping [1] INTEGER OPT }
|
|
500
|
+
function policyConstraints(buf) {
|
|
501
|
+
var C = ns.prefix + "/bad-policy";
|
|
502
|
+
var kids = seqChildren(buf, C, "PolicyConstraints");
|
|
503
|
+
if (kids.length < 1) throw ns.E(C, "PolicyConstraints must contain at least one field (RFC 5280 §4.2.1.11)");
|
|
504
|
+
var rep = null, ipm = null, pcLastTag = -1;
|
|
505
|
+
kids.forEach(function (k) {
|
|
506
|
+
if (k.tagClass !== "context") throw ns.E(C, "PolicyConstraints fields are context-tagged [0]/[1]");
|
|
507
|
+
if (k.tagNumber <= pcLastTag) throw ns.E(C, "PolicyConstraints fields must be unique and in ascending order (DER)");
|
|
508
|
+
pcLastTag = k.tagNumber;
|
|
509
|
+
var v;
|
|
510
|
+
try { v = asn1.read.integerImplicit(k, k.tagNumber); } catch (e) { throw ns.E(C, "PolicyConstraints field must be an INTEGER", e); }
|
|
511
|
+
if (v < 0n) throw ns.E(C, "PolicyConstraints skip count must be non-negative");
|
|
512
|
+
if (k.tagNumber === 0) rep = Number(v);
|
|
513
|
+
else if (k.tagNumber === 1) ipm = Number(v);
|
|
514
|
+
else throw ns.E(C, "PolicyConstraints has an unexpected field [" + k.tagNumber + "]");
|
|
515
|
+
});
|
|
516
|
+
return { requireExplicitPolicy: rep, inhibitPolicyMapping: ipm };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// inhibitAnyPolicy ::= INTEGER (0..MAX)
|
|
520
|
+
function inhibitAnyPolicy(buf) {
|
|
521
|
+
var C = ns.prefix + "/bad-policy";
|
|
522
|
+
var n = decodeTop(buf, C, "InhibitAnyPolicy");
|
|
523
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "InhibitAnyPolicy must be an INTEGER (RFC 5280 §4.2.1.14)");
|
|
524
|
+
var v = readInt(n, C, "InhibitAnyPolicy");
|
|
525
|
+
if (v < 0n) throw ns.E(C, "InhibitAnyPolicy skip count must be non-negative");
|
|
526
|
+
return Number(v);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// subjectAltName / issuerAltName ::= GeneralNames — decoded values surfaced.
|
|
530
|
+
function altName(buf) {
|
|
531
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
532
|
+
var n = decodeTop(buf, C, "GeneralNames");
|
|
533
|
+
return schema.walk(generalNames(ns, { decodeValue: true, code: C }), n, ns).result;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// extKeyUsage ::= SEQUENCE SIZE(1..MAX) OF KeyPurposeId (OID)
|
|
537
|
+
function extKeyUsage(buf) {
|
|
538
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
539
|
+
var kids = seqChildren(buf, C, "ExtKeyUsage");
|
|
540
|
+
if (kids.length < 1) throw ns.E(C, "ExtKeyUsage must contain at least one KeyPurposeId (RFC 5280 §4.2.1.12)");
|
|
541
|
+
return kids.map(function (k) {
|
|
542
|
+
try { return asn1.read.oid(k); } catch (e) { throw ns.E(C, "ExtKeyUsage KeyPurposeId must be an OBJECT IDENTIFIER", e); }
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// authorityKeyIdentifier ::= SEQUENCE { keyIdentifier [0] OPT, authorityCertIssuer [1] OPT, authorityCertSerialNumber [2] OPT }
|
|
547
|
+
function authorityKeyIdentifier(buf) {
|
|
548
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
549
|
+
var kids = seqChildren(buf, C, "AuthorityKeyIdentifier");
|
|
550
|
+
var out = { keyIdentifier: null, authorityCertIssuer: null, authorityCertSerialNumber: null };
|
|
551
|
+
var lastTag = -1;
|
|
552
|
+
kids.forEach(function (k) {
|
|
553
|
+
if (k.tagClass !== "context") throw ns.E(C, "AuthorityKeyIdentifier fields are context-tagged");
|
|
554
|
+
// DER: the OPTIONAL fields appear at most once and in ascending tag order.
|
|
555
|
+
if (k.tagNumber <= lastTag) throw ns.E(C, "AuthorityKeyIdentifier fields must be unique and in ascending order (DER)");
|
|
556
|
+
lastTag = k.tagNumber;
|
|
557
|
+
// keyIdentifier [0] IMPLICIT OCTET STRING and authorityCertSerialNumber
|
|
558
|
+
// [2] IMPLICIT INTEGER are primitive leaves; the readers enforce the
|
|
559
|
+
// primitive form fail-closed (a constructed context node is rejected,
|
|
560
|
+
// never dereferenced for its absent content).
|
|
561
|
+
if (k.tagNumber === 0) { try { out.keyIdentifier = Buffer.from(asn1.read.octetStringImplicit(k, 0)); } catch (e) { throw ns.E(C, "AuthorityKeyIdentifier keyIdentifier [0] must be an IMPLICIT OCTET STRING", e); } }
|
|
562
|
+
else if (k.tagNumber === 1) out.authorityCertIssuer = schema.walk(generalNames(ns, { implicitTag: 1, decodeValue: true, code: C }), k, ns).result;
|
|
563
|
+
else if (k.tagNumber === 2) { try { out.authorityCertSerialNumber = asn1.read.integerImplicit(k, 2); } catch (e) { throw ns.E(C, "authorityCertSerialNumber must be an INTEGER", e); } }
|
|
564
|
+
else throw ns.E(C, "AuthorityKeyIdentifier has an unexpected field [" + k.tagNumber + "]");
|
|
565
|
+
});
|
|
566
|
+
if ((out.authorityCertIssuer === null) !== (out.authorityCertSerialNumber === null)) {
|
|
567
|
+
throw ns.E(C, "AuthorityKeyIdentifier authorityCertIssuer and authorityCertSerialNumber must both be present or both absent (RFC 5280 §4.2.1.1)");
|
|
568
|
+
}
|
|
569
|
+
return out;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// subjectKeyIdentifier ::= OCTET STRING
|
|
573
|
+
function subjectKeyIdentifier(buf) {
|
|
574
|
+
var C = ns.prefix + "/bad-extension-value";
|
|
575
|
+
var n = decodeTop(buf, C, "SubjectKeyIdentifier");
|
|
576
|
+
try { return Buffer.concat([asn1.read.octetString(n)]); }
|
|
577
|
+
catch (e) { throw ns.E(C, "SubjectKeyIdentifier must be an OCTET STRING (RFC 5280 §4.2.1.2)", e); }
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function O(nm) { return ns.oid.byName(nm); }
|
|
581
|
+
var byOid = {};
|
|
582
|
+
byOid[O("basicConstraints")] = basicConstraints;
|
|
583
|
+
byOid[O("keyUsage")] = keyUsage;
|
|
584
|
+
byOid[O("nameConstraints")] = nameConstraints;
|
|
585
|
+
byOid[O("certificatePolicies")] = certificatePolicies;
|
|
586
|
+
byOid[O("policyMappings")] = policyMappings;
|
|
587
|
+
byOid[O("policyConstraints")] = policyConstraints;
|
|
588
|
+
byOid[O("inhibitAnyPolicy")] = inhibitAnyPolicy;
|
|
589
|
+
byOid[O("subjectAltName")] = altName;
|
|
590
|
+
byOid[O("issuerAltName")] = altName;
|
|
591
|
+
byOid[O("extKeyUsage")] = extKeyUsage;
|
|
592
|
+
byOid[O("authorityKeyIdentifier")] = authorityKeyIdentifier;
|
|
593
|
+
byOid[O("subjectKeyIdentifier")] = subjectKeyIdentifier;
|
|
594
|
+
return { byOid: byOid };
|
|
595
|
+
}
|
|
596
|
+
|
|
287
597
|
// Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
|
|
288
598
|
// extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
|
|
289
599
|
// (not context-tagged), so the per-extension decode handles the 2-vs-3-child
|
|
@@ -440,6 +750,7 @@ module.exports = {
|
|
|
440
750
|
name: name,
|
|
441
751
|
generalName: generalName,
|
|
442
752
|
generalNames: generalNames,
|
|
753
|
+
certExtensionDecoders: certExtensionDecoders,
|
|
443
754
|
attribute: attribute,
|
|
444
755
|
extension: extension,
|
|
445
756
|
extensions: extensions,
|
package/lib/webcrypto.js
CHANGED
|
@@ -531,6 +531,7 @@ function _curveFromKey(ko) {
|
|
|
531
531
|
* point). Throws unless the key was created `extractable`.
|
|
532
532
|
*
|
|
533
533
|
* @example
|
|
534
|
+
* var keyPair = await pki.webcrypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
|
|
534
535
|
* var spki = await pki.webcrypto.subtle.exportKey("spki", keyPair.publicKey);
|
|
535
536
|
*/
|
|
536
537
|
SubtleCrypto.prototype.exportKey = async function exportKey(format, key) {
|
|
@@ -566,7 +567,7 @@ function _rawPublic(key) {
|
|
|
566
567
|
*
|
|
567
568
|
* A ready `Crypto` instance (the shape of `globalThis.crypto`) exposing
|
|
568
569
|
* `getRandomValues`, `randomUUID`, and `subtle`. Construct additional
|
|
569
|
-
* instances with `new pki.
|
|
570
|
+
* instances with `new pki.webcrypto.Crypto()`.
|
|
570
571
|
*
|
|
571
572
|
* @example
|
|
572
573
|
* var iv = pki.webcrypto.getRandomValues(new Uint8Array(12));
|
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:69692b51-d096-4124-9a0c-886b9a5ad78e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-06T19:48:47.311Z",
|
|
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.1.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.16",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.16",
|
|
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.1.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.1.16",
|
|
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.1.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.16",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|