@blamejs/pki 0.2.32 → 0.2.33
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 +8 -0
- package/README.md +1 -1
- package/lib/schema-attrcert.js +251 -2
- package/lib/schema-pkix.js +19 -13
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ 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.33 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
Attribute certificates now decode their RFC 5755 attribute values and attribute-certificate extensions, not just the certificate structure.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.attrcert.parse decodes the RFC 5755 section 4.4 attribute values -- role (RoleSyntax), clearance (Clearance, including the classification bit list and security categories), authenticationInfo and accessIdentity (SvceAuthInfo), and group and chargingIdentity (IetfAttrSyntax) -- and the section 4.3 attribute-certificate extensions -- auditIdentity, targetInformation and proxyInfo (Targets, with targetCert reusing the IssuerSerial decoder), noRevAvail, and aaControls. Each is surfaced additively (a decoded field alongside the raw value) through the parse consumer path; an unrecognized attribute type or extension id is preserved opaque, and a malformed recognized value fails closed with a typed error. Every GeneralName inside these structures is validated through the shared name decoder.
|
|
14
|
+
|
|
7
15
|
## v0.2.32 — 2026-07-16
|
|
8
16
|
|
|
9
17
|
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.
|
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ is callable today; nothing below is a stub.
|
|
|
211
211
|
| `pki.schema.cms` | Parse DER / PEM CMS per RFC 5652 / 5083 / 9629 — SignedData (§5, signer infos + raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds incl. RFC 5753 key-agreement and RFC 9629 KEM recipients with ML-KEM validation), EncryptedData (§8), AuthenticatedData (§9, MAC surface + raw `authAttrsBytes`), and AuthEnvelopedData (RFC 5083, with RFC 5084 AES-GCM/CCM parameter validation); §11 attribute placement enforced, countersignatures validated recursively, certificates / CRLs validated against the closed CHOICE sets and kept raw, every result tagged `contentTypeName`, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
212
212
|
| `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode`, `pemEncode` |
|
|
213
213
|
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp requests, responses, and tokens — the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseRequest`, `parseResponse`, `parseTstInfo`, `parseToken`, `pemDecode`, `pemEncode` |
|
|
214
|
-
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group /
|
|
214
|
+
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes and extensions **decoded to structured values** (role, clearance, service/access identity, group, charging identity; audit identity, target/proxy information, no-rev-avail, AA controls — unknown types preserved opaque), with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
215
215
|
| `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages — the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
216
216
|
| `pki.schema.pkcs12` | Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores — key bags via the PKCS#8 parser, shrouded keys (algorithm surfaced, ciphertext opaque), cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` / `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification; BER accepted exactly where §4.1 requires it, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
217
217
|
| `pki.schema.cmp` | Parse DER / PEM RFC 9810 Certificate Management Protocol messages (PKIMessage) — the header (version, sender / recipient incl. the anonymous NULL-DN, nonces, transaction id, general info), the 27-arm body (certificate requests via the CRMF parser, an encrypted certificate's EnvelopedData via CMS, response / revocation / confirmation / error / support / polling arms structural, the rest raw), and the exact `headerBytes` / `bodyBytes` slices an external verifier reconstructs the protected part from; the CMP-before-OCSP dispatch order is enforced, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
package/lib/schema-attrcert.js
CHANGED
|
@@ -281,7 +281,24 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
|
|
|
281
281
|
// kept stable for consumers keying off `form`.
|
|
282
282
|
var issuer = { form: "v2Form", v2Form: acinfo.fields.issuer.value.result, v1Form: null };
|
|
283
283
|
|
|
284
|
+
// RFC 5755 sec. 4.4 -- decode each attribute's value(s) additively: a recognized attribute type
|
|
285
|
+
// gains `decoded` (parallel to `values`); an unrecognized type falls back to { opaque, bytes } so
|
|
286
|
+
// the parse never fails on an unknown attribute, and a recognized-but-malformed value fails closed.
|
|
287
|
+
var attributes = acinfo.fields.attributes.value.result;
|
|
288
|
+
attributes.forEach(function (a) {
|
|
289
|
+
var dec = _ATTR_VALUE_DECODERS[a.type];
|
|
290
|
+
a.decoded = a.values.map(function (v) { return dec ? dec(v) : { opaque: true, bytes: v }; });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// RFC 5755 sec. 4.3 -- decode each AC extension additively (extensions[i].decoded), same opaque
|
|
294
|
+
// fallback + fail-closed-on-malformed contract as the attributes above.
|
|
284
295
|
var extField = acinfo.fields.extensions;
|
|
296
|
+
var extensions = (extField && extField.present) ? extField.value.result : [];
|
|
297
|
+
extensions.forEach(function (x) {
|
|
298
|
+
var dec = _AC_EXT_DECODERS[x.oid];
|
|
299
|
+
x.decoded = dec ? dec(x.value) : { opaque: true, bytes: x.value };
|
|
300
|
+
});
|
|
301
|
+
|
|
285
302
|
return {
|
|
286
303
|
version: acinfo.fields.version.value, // 2
|
|
287
304
|
holder: acinfo.fields.holder.value.result,
|
|
@@ -291,9 +308,9 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
|
|
|
291
308
|
serialNumber: acinfo.fields.serialNumber.value,
|
|
292
309
|
serialNumberHex: sc.toString("hex"),
|
|
293
310
|
validity: acinfo.fields.attrCertValidityPeriod.value.result,
|
|
294
|
-
attributes:
|
|
311
|
+
attributes: attributes,
|
|
295
312
|
issuerUniqueID: acinfo.fields.issuerUniqueID.present ? acinfo.fields.issuerUniqueID.value : null,
|
|
296
|
-
extensions:
|
|
313
|
+
extensions: extensions,
|
|
297
314
|
tbsBytes: e.tbsBytes,
|
|
298
315
|
signatureValue: e.signatureValue,
|
|
299
316
|
};
|
|
@@ -417,6 +434,238 @@ function matchesV1(root) {
|
|
|
417
434
|
return schema.isUniversal(k[1], TAGS.SEQUENCE);
|
|
418
435
|
}
|
|
419
436
|
|
|
437
|
+
// ---- RFC 5755 sec. 4.4 attribute-value decoders ----------------------
|
|
438
|
+
// A per-attribute-type decoder table (mirroring pkix.certExtensionDecoders): each takes the raw
|
|
439
|
+
// AttributeValue TLV Buffer and returns a structured value, throwing a typed attrcert/bad-* error on
|
|
440
|
+
// any non-conformant shape. Composes pkix's shared imperative helpers + generalName/generalNames.
|
|
441
|
+
// A single GeneralName decodes as the walk OBJECT itself (gn.tagNumber / gn.value); generalNames
|
|
442
|
+
// (plural) returns { names:[...] } on .result.
|
|
443
|
+
var _AV = pkix._decodeHelpers(NS);
|
|
444
|
+
var _decodeTop = _AV.decodeTop, _seqChildren = _AV.seqChildren, _readInt = _AV.readInt, _Oav = _AV.O;
|
|
445
|
+
var _GN = pkix.generalName(NS, { decodeValue: true, code: "attrcert/bad-general-name" });
|
|
446
|
+
var _GNS_0 = pkix.generalNames(NS, { implicitTag: 0, decodeValue: true, code: "attrcert/bad-general-names" });
|
|
447
|
+
function _isCtx(node, n) { return node && node.tagClass === "context" && node.tagNumber === n; }
|
|
448
|
+
function _isU(node, tag) { return node && node.tagClass === "universal" && node.tagNumber === tag; }
|
|
449
|
+
function _oneGN(node, code) {
|
|
450
|
+
if (!_isCtx(node, 0) && !_isCtx(node, 1) && !_isCtx(node, 2) && !_isCtx(node, 3) &&
|
|
451
|
+
!_isCtx(node, 4) && !_isCtx(node, 5) && !_isCtx(node, 6) && !_isCtx(node, 7) && !_isCtx(node, 8)) {
|
|
452
|
+
throw NS.E(code, "a GeneralName must be a context-tagged [0]..[8] CHOICE alternative");
|
|
453
|
+
}
|
|
454
|
+
var gn = schema.walk(_GN, node, NS);
|
|
455
|
+
return { tagNumber: gn.tagNumber, value: gn.value };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// SvceAuthInfo ::= SEQUENCE { service GeneralName, ident GeneralName, authInfo OCTET STRING OPTIONAL }
|
|
459
|
+
// -- authenticationInfo (sec. 4.4.1) allows authInfo; accessIdentity (sec. 4.4.2) MUST omit it.
|
|
460
|
+
function _svceAuthInfo(authInfoAllowed, code) {
|
|
461
|
+
return function (buf) {
|
|
462
|
+
var kids = _seqChildren(buf, code, "SvceAuthInfo");
|
|
463
|
+
if (kids.length < 2 || kids.length > 3) throw NS.E(code, "SvceAuthInfo must be a SEQUENCE { service, ident, authInfo? }");
|
|
464
|
+
var service = _oneGN(kids[0], code);
|
|
465
|
+
var ident = _oneGN(kids[1], code);
|
|
466
|
+
var authInfo = null;
|
|
467
|
+
if (kids.length === 3) {
|
|
468
|
+
if (!authInfoAllowed) throw NS.E(code, "this attribute must not carry authInfo (RFC 5755 sec. 4.4.2)");
|
|
469
|
+
if (!_isU(kids[2], TAGS.OCTET_STRING)) throw NS.E(code, "SvceAuthInfo authInfo must be an OCTET STRING");
|
|
470
|
+
authInfo = Buffer.concat([asn1.read.octetString(kids[2])]);
|
|
471
|
+
}
|
|
472
|
+
return { service: service, ident: ident, authInfo: authInfo };
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// IetfAttrSyntax ::= SEQUENCE { policyAuthority [0] IMPLICIT GeneralNames OPTIONAL,
|
|
477
|
+
// values SEQUENCE OF CHOICE { octets OCTET STRING, oid OBJECT IDENTIFIER, string UTF8String } }
|
|
478
|
+
function _ietfAttrSyntax(buf) {
|
|
479
|
+
var C = "attrcert/bad-ietf-attr";
|
|
480
|
+
var kids = _seqChildren(buf, C, "IetfAttrSyntax");
|
|
481
|
+
if (!kids.length || kids.length > 2) throw NS.E(C, "IetfAttrSyntax must be a SEQUENCE { policyAuthority [0]?, values }");
|
|
482
|
+
var policyAuthority = null, i = 0;
|
|
483
|
+
if (_isCtx(kids[0], 0)) { policyAuthority = schema.walk(_GNS_0, kids[0], NS).result; i = 1; }
|
|
484
|
+
var valuesNode = kids[i];
|
|
485
|
+
if (!valuesNode || !_isU(valuesNode, TAGS.SEQUENCE) || !valuesNode.children || !valuesNode.children.length) {
|
|
486
|
+
throw NS.E(C, "IetfAttrSyntax values must be a non-empty SEQUENCE OF value");
|
|
487
|
+
}
|
|
488
|
+
if (i + 1 !== kids.length) throw NS.E(C, "IetfAttrSyntax has unexpected trailing fields (a [0] must precede values)");
|
|
489
|
+
var kinds = {};
|
|
490
|
+
var values = valuesNode.children.map(function (v) {
|
|
491
|
+
if (_isU(v, TAGS.OCTET_STRING)) { kinds.octets = true; return { kind: "octets", value: Buffer.concat([asn1.read.octetString(v)]) }; }
|
|
492
|
+
if (_isU(v, TAGS.OBJECT_IDENTIFIER)) { kinds.oid = true; return { kind: "oid", value: asn1.read.oid(v) }; }
|
|
493
|
+
if (_isU(v, TAGS.UTF8_STRING)) { kinds.string = true; return { kind: "string", value: asn1.read.string(v) }; }
|
|
494
|
+
throw NS.E(C, "IetfAttrSyntax value must be an OCTET STRING, OBJECT IDENTIFIER, or UTF8String");
|
|
495
|
+
});
|
|
496
|
+
return { policyAuthority: policyAuthority, values: values, homogeneous: Object.keys(kinds).length === 1 };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// RoleSyntax ::= SEQUENCE { roleAuthority [0] IMPLICIT GeneralNames OPTIONAL, roleName [1] EXPLICIT GeneralName }
|
|
500
|
+
function _role(buf) {
|
|
501
|
+
var C = "attrcert/bad-role";
|
|
502
|
+
var kids = _seqChildren(buf, C, "RoleSyntax");
|
|
503
|
+
if (!kids.length || kids.length > 2) throw NS.E(C, "RoleSyntax must be a SEQUENCE { roleAuthority [0]?, roleName [1] }");
|
|
504
|
+
var roleAuthority = null, i = 0;
|
|
505
|
+
if (_isCtx(kids[0], 0)) { roleAuthority = schema.walk(_GNS_0, kids[0], NS).result; i = 1; }
|
|
506
|
+
var rn = kids[i];
|
|
507
|
+
if (!_isCtx(rn, 1) || !rn.children || rn.children.length !== 1) throw NS.E(C, "RoleSyntax roleName must be an EXPLICIT [1] wrapping exactly one GeneralName");
|
|
508
|
+
if (i + 1 !== kids.length) throw NS.E(C, "RoleSyntax has unexpected trailing fields");
|
|
509
|
+
return { roleAuthority: roleAuthority, roleName: _oneGN(rn.children[0], C) };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Clearance ::= SEQUENCE { policyId OBJECT IDENTIFIER, classList ClassList DEFAULT {unclassified},
|
|
513
|
+
// securityCategories SET OF SecurityCategory OPTIONAL }.
|
|
514
|
+
// ClassList ::= BIT STRING { unmarked(0), unclassified(1), restricted(2), confidential(3), secret(4), topSecret(5) }.
|
|
515
|
+
var _CLASSLIST_NAMES = ["unmarked", "unclassified", "restricted", "confidential", "secret", "topSecret"];
|
|
516
|
+
function _decodeClassList(node, C) {
|
|
517
|
+
var bs;
|
|
518
|
+
try { bs = asn1.read.bitString(node); } catch (e) { throw NS.E(C, "Clearance classList must be a BIT STRING", e); }
|
|
519
|
+
schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (m) { throw NS.E(C, m); });
|
|
520
|
+
var names = [], flags = {}, reserved = [];
|
|
521
|
+
var total = bs.bytes.length * 8 - bs.unusedBits;
|
|
522
|
+
for (var bit = 0; bit < total; bit++) {
|
|
523
|
+
if ((bs.bytes[bit >> 3] & (0x80 >> (bit & 7))) === 0) continue;
|
|
524
|
+
if (bit < _CLASSLIST_NAMES.length) { names.push(_CLASSLIST_NAMES[bit]); flags[_CLASSLIST_NAMES[bit]] = true; }
|
|
525
|
+
else reserved.push(bit);
|
|
526
|
+
}
|
|
527
|
+
return { names: names, flags: flags, reservedBits: reserved };
|
|
528
|
+
}
|
|
529
|
+
// SecurityCategory ::= SEQUENCE { type [0] IMPLICIT OBJECT IDENTIFIER, value [1] EXPLICIT ANY }.
|
|
530
|
+
function _securityCategory(node, C) {
|
|
531
|
+
if (!_isU(node, TAGS.SEQUENCE) || !node.children || node.children.length !== 2) throw NS.E(C, "a SecurityCategory must be a SEQUENCE of a type and a value");
|
|
532
|
+
var typeNode = node.children[0], valueNode = node.children[1];
|
|
533
|
+
if (!_isCtx(typeNode, 0) || typeNode.children) throw NS.E(C, "SecurityCategory type must be a primitive [0] IMPLICIT OBJECT IDENTIFIER");
|
|
534
|
+
var type; try { type = asn1.decodeOidContent(typeNode.content); } catch (e) { throw NS.E(C, "SecurityCategory type is not a valid OBJECT IDENTIFIER", e); }
|
|
535
|
+
if (!_isCtx(valueNode, 1) || !valueNode.children || valueNode.children.length !== 1) throw NS.E(C, "SecurityCategory value must be an EXPLICIT [1] wrapping one element");
|
|
536
|
+
return { type: type, valueBytes: Buffer.concat([valueNode.children[0].bytes]) };
|
|
537
|
+
}
|
|
538
|
+
function _clearance(buf) {
|
|
539
|
+
var C = "attrcert/bad-clearance";
|
|
540
|
+
var kids = _seqChildren(buf, C, "Clearance");
|
|
541
|
+
if (!kids.length || kids.length > 3) throw NS.E(C, "Clearance must be a SEQUENCE { policyId, classList?, securityCategories? }");
|
|
542
|
+
if (!_isU(kids[0], TAGS.OBJECT_IDENTIFIER)) throw NS.E(C, "Clearance policyId must be an OBJECT IDENTIFIER");
|
|
543
|
+
var policyId; try { policyId = asn1.read.oid(kids[0]); } catch (e) { throw NS.E(C, "Clearance policyId must be an OBJECT IDENTIFIER", e); }
|
|
544
|
+
var classList = { names: ["unclassified"], flags: { unclassified: true }, reservedBits: [] }, i = 1; // DEFAULT {unclassified}
|
|
545
|
+
if (kids[i] && _isU(kids[i], TAGS.BIT_STRING)) {
|
|
546
|
+
classList = _decodeClassList(kids[i], C);
|
|
547
|
+
if (classList.names.length === 1 && classList.flags.unclassified && !classList.reservedBits.length) {
|
|
548
|
+
throw NS.E(C, "Clearance classList equal to the DEFAULT {unclassified} must be omitted (non-canonical DER)");
|
|
549
|
+
}
|
|
550
|
+
i++;
|
|
551
|
+
}
|
|
552
|
+
var securityCategories = null;
|
|
553
|
+
if (kids[i]) {
|
|
554
|
+
var setNode = kids[i];
|
|
555
|
+
if (!_isU(setNode, TAGS.SET) || !setNode.children || !setNode.children.length) throw NS.E(C, "Clearance securityCategories must be a non-empty SET OF SecurityCategory");
|
|
556
|
+
securityCategories = setNode.children.map(function (categoryNode) { return _securityCategory(categoryNode, C); });
|
|
557
|
+
i++;
|
|
558
|
+
}
|
|
559
|
+
if (i !== kids.length) throw NS.E(C, "Clearance has fields out of order or unexpected trailing fields");
|
|
560
|
+
return { policyId: policyId, classList: classList, securityCategories: securityCategories };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
var _ATTR_VALUE_DECODERS = {};
|
|
564
|
+
_ATTR_VALUE_DECODERS[_Oav("role")] = _role;
|
|
565
|
+
_ATTR_VALUE_DECODERS[_Oav("clearance")] = _clearance;
|
|
566
|
+
// RFC 5755 sec. 4.4.6 requires decoding the legacy RFC 3281 id-at-clearance too (the X.501
|
|
567
|
+
// selected-attribute-types arc, an alias of "clearance" -- built from arcs, never a dotted literal).
|
|
568
|
+
_ATTR_VALUE_DECODERS[oid.fromArcs([2, 5, 1, 5, 55])] = _clearance;
|
|
569
|
+
_ATTR_VALUE_DECODERS[_Oav("authenticationInfo")] = _svceAuthInfo(true, "attrcert/bad-svce-auth-info");
|
|
570
|
+
_ATTR_VALUE_DECODERS[_Oav("accessIdentity")] = _svceAuthInfo(false, "attrcert/bad-access-identity");
|
|
571
|
+
_ATTR_VALUE_DECODERS[_Oav("chargingIdentity")] = _ietfAttrSyntax;
|
|
572
|
+
_ATTR_VALUE_DECODERS[_Oav("group")] = _ietfAttrSyntax;
|
|
573
|
+
|
|
574
|
+
// ---- RFC 5755 sec. 4.3 AC-extension decoders -------------------------
|
|
575
|
+
// A per-extension-OID decoder table for the AC-specific extensions (kept in the AC domain, not the
|
|
576
|
+
// shared cert-extension table, because Targets/TargetCert reuse the local IssuerSerial). Each takes
|
|
577
|
+
// the raw extnValue content Buffer and returns a structured value, fail-closed with a typed error.
|
|
578
|
+
|
|
579
|
+
// AuditIdentity ::= OCTET STRING (sec. 4.3.1) -- an opaque audit tag, unconstrained (no SIZE), so an
|
|
580
|
+
// empty OCTET STRING (04 00) is valid; only the OCTET STRING type is enforced.
|
|
581
|
+
function _acAuditIdentity(buf) {
|
|
582
|
+
var C = "attrcert/bad-audit-identity";
|
|
583
|
+
var n = _decodeTop(buf, C, "AuditIdentity");
|
|
584
|
+
if (!_isU(n, TAGS.OCTET_STRING)) throw NS.E(C, "AuditIdentity must be an OCTET STRING");
|
|
585
|
+
return Buffer.concat([asn1.read.octetString(n)]);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// NoRevAvail syntax is NULL (sec. 4.3.6: '0500'H is the DER encoding) -- the extnValue OCTET STRING
|
|
589
|
+
// contains a DER NULL. An empty or any-other-shape value is malformed and fails closed.
|
|
590
|
+
function _noRevAvail(buf) {
|
|
591
|
+
var C = "attrcert/bad-no-rev-avail";
|
|
592
|
+
var n = _decodeTop(buf, C, "NoRevAvail");
|
|
593
|
+
try { asn1.read.nullValue(n); } catch (e) { throw NS.E(C, "NoRevAvail must be a DER NULL (RFC 5755 sec. 4.3.6)", e); }
|
|
594
|
+
return { noRevAvail: true };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Target ::= CHOICE { targetName [0] GeneralName, targetGroup [1] GeneralName, targetCert [2] TargetCert }.
|
|
598
|
+
// [0]/[1] wrap a GeneralName CHOICE (EXPLICIT); [2] is an IMPLICIT TargetCert SEQUENCE.
|
|
599
|
+
function _acTargetCert(node, C) {
|
|
600
|
+
if (!node.children || !node.children.length) throw NS.E(C, "a targetCert [2] must be a non-empty TargetCert SEQUENCE");
|
|
601
|
+
var out = { kind: "targetCert", targetCertificate: null, targetName: null, certDigestInfo: null }, i = 0;
|
|
602
|
+
out.targetCertificate = schema.walk(ISSUER_SERIAL, node.children[i++], NS).result;
|
|
603
|
+
if (node.children[i] && node.children[i].tagClass === "context") out.targetName = _oneGN(node.children[i++], C);
|
|
604
|
+
if (node.children[i] && _isU(node.children[i], TAGS.SEQUENCE)) out.certDigestInfo = schema.walk(OBJECT_DIGEST_INFO, node.children[i++], NS).result;
|
|
605
|
+
if (i !== node.children.length) throw NS.E(C, "a targetCert has unexpected trailing fields");
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
608
|
+
function _acTarget(node, C, allowTargetCert) {
|
|
609
|
+
if (_isCtx(node, 0) || _isCtx(node, 1)) {
|
|
610
|
+
if (!node.children || node.children.length !== 1) throw NS.E(C, "a Target name must be an EXPLICIT [0]/[1] wrapping one GeneralName");
|
|
611
|
+
return { kind: node.tagNumber === 0 ? "targetName" : "targetGroup", name: _oneGN(node.children[0], C) };
|
|
612
|
+
}
|
|
613
|
+
if (_isCtx(node, 2)) {
|
|
614
|
+
if (!allowTargetCert) throw NS.E(C, "the targetCert [2] CHOICE MUST NOT be used in proxying information (RFC 5755 sec. 7.4)");
|
|
615
|
+
return _acTargetCert(node, C);
|
|
616
|
+
}
|
|
617
|
+
throw NS.E(C, "a Target must be [0] targetName, [1] targetGroup, or [2] targetCert");
|
|
618
|
+
}
|
|
619
|
+
// Targets ::= SEQUENCE OF Target -- decode a Targets SEQUENCE node into an array of Target.
|
|
620
|
+
function _acDecodeTargets(node, C, allowTargetCert) {
|
|
621
|
+
if (!_isU(node, TAGS.SEQUENCE) || !node.children || !node.children.length) throw NS.E(C, "Targets must be a non-empty SEQUENCE OF Target");
|
|
622
|
+
return node.children.map(function (t) { return _acTarget(t, C, allowTargetCert); });
|
|
623
|
+
}
|
|
624
|
+
// Both targetInformation (sec. 4.3.2) and ProxyInfo (sec. 7.4) are SEQUENCE OF Targets -- an outer
|
|
625
|
+
// SEQUENCE whose every element is itself a Targets (SEQUENCE OF Target). A conforming issuer emits one
|
|
626
|
+
// Targets, but users MUST accept several. proxying additionally forbids the targetCert [2] alternative.
|
|
627
|
+
function _seqOfTargets(buf, C, allowTargetCert) {
|
|
628
|
+
var kids = _seqChildren(buf, C, "SEQUENCE OF Targets");
|
|
629
|
+
if (!kids.length) throw NS.E(C, "a SEQUENCE OF Targets must be non-empty");
|
|
630
|
+
return kids.map(function (targetsNode) { return _acDecodeTargets(targetsNode, C, allowTargetCert); });
|
|
631
|
+
}
|
|
632
|
+
function _targetInformation(buf) { return _seqOfTargets(buf, "attrcert/bad-targets", true); }
|
|
633
|
+
function _acProxying(buf) { return _seqOfTargets(buf, "attrcert/bad-proxy-info", false); }
|
|
634
|
+
|
|
635
|
+
// AAControls ::= SEQUENCE { pathLenConstraint INTEGER (0..MAX) OPTIONAL, permittedAttrs [0] AttrSpec
|
|
636
|
+
// OPTIONAL, excludedAttrs [1] AttrSpec OPTIONAL, permitUnSpecified BOOLEAN DEFAULT TRUE }.
|
|
637
|
+
// AttrSpec ::= SEQUENCE OF OBJECT IDENTIFIER (the [0]/[1] IMPLICIT tag replaces the SEQUENCE tag).
|
|
638
|
+
function _acAttrSpec(node, C) {
|
|
639
|
+
if (!node.children) throw NS.E(C, "an AttrSpec must be a SEQUENCE OF OBJECT IDENTIFIER");
|
|
640
|
+
return node.children.map(function (o) {
|
|
641
|
+
if (!_isU(o, TAGS.OBJECT_IDENTIFIER)) throw NS.E(C, "an AttrSpec element must be an OBJECT IDENTIFIER");
|
|
642
|
+
try { return asn1.read.oid(o); } catch (e) { throw NS.E(C, "an AttrSpec element must be an OBJECT IDENTIFIER", e); }
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function _aaControls(buf) {
|
|
646
|
+
var C = "attrcert/bad-aa-controls";
|
|
647
|
+
var kids = _seqChildren(buf, C, "AAControls");
|
|
648
|
+
if (kids.length > 4) throw NS.E(C, "AAControls has too many fields");
|
|
649
|
+
var out = { pathLenConstraint: null, permittedAttrs: null, excludedAttrs: null, permitUnSpecified: true }, i = 0;
|
|
650
|
+
if (kids[i] && _isU(kids[i], TAGS.INTEGER)) { out.pathLenConstraint = guard.range.uint31(_readInt(kids[i], C, "pathLenConstraint"), NS.E, C, "AAControls pathLenConstraint"); i++; }
|
|
651
|
+
if (kids[i] && _isCtx(kids[i], 0)) { out.permittedAttrs = _acAttrSpec(kids[i], C); i++; }
|
|
652
|
+
if (kids[i] && _isCtx(kids[i], 1)) { out.excludedAttrs = _acAttrSpec(kids[i], C); i++; }
|
|
653
|
+
if (kids[i] && _isU(kids[i], TAGS.BOOLEAN)) {
|
|
654
|
+
var v = asn1.read.boolean(kids[i]);
|
|
655
|
+
if (v === true) throw NS.E(C, "AAControls permitUnSpecified TRUE equals the DEFAULT and must be omitted (non-canonical DER)");
|
|
656
|
+
out.permitUnSpecified = v; i++;
|
|
657
|
+
}
|
|
658
|
+
if (i !== kids.length) throw NS.E(C, "AAControls has fields out of order or unexpected trailing fields");
|
|
659
|
+
return out;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
var _AC_EXT_DECODERS = {};
|
|
663
|
+
_AC_EXT_DECODERS[_Oav("acAuditIdentity")] = _acAuditIdentity;
|
|
664
|
+
_AC_EXT_DECODERS[_Oav("targetInformation")] = _targetInformation;
|
|
665
|
+
_AC_EXT_DECODERS[_Oav("noRevAvail")] = _noRevAvail;
|
|
666
|
+
_AC_EXT_DECODERS[_Oav("aaControls")] = _aaControls;
|
|
667
|
+
_AC_EXT_DECODERS[_Oav("acProxying")] = _acProxying;
|
|
668
|
+
|
|
420
669
|
module.exports = {
|
|
421
670
|
parse: parse,
|
|
422
671
|
parseV1: parseV1,
|
package/lib/schema-pkix.js
CHANGED
|
@@ -598,13 +598,14 @@ function assertPolicyQualifiers(qNode, fail) {
|
|
|
598
598
|
});
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
// Shared imperative decode helpers for the certExtensionDecoders + attrValueDecoders factories. Both
|
|
602
|
+
// take the caller's ns, so the helpers close over ns.E / ns.oid; extracted once so the two factories
|
|
603
|
+
// share one copy (a per-OID value-decoder body is the identical idiom to a per-OID extension body).
|
|
604
|
+
function _decodeHelpers(ns) {
|
|
604
605
|
function decodeTop(buf, code, what) {
|
|
605
606
|
var n;
|
|
606
607
|
try { n = asn1.decode(buf); }
|
|
607
|
-
catch (e) { throw ns.E(code, "malformed " + what + "
|
|
608
|
+
catch (e) { throw ns.E(code, "malformed " + what + " value: " + ((e && e.message) || String(e)), e); }
|
|
608
609
|
return n;
|
|
609
610
|
}
|
|
610
611
|
function seqChildren(buf, code, what) {
|
|
@@ -618,6 +619,19 @@ function certExtensionDecoders(ns) {
|
|
|
618
619
|
try { return asn1.read.integer(node); }
|
|
619
620
|
catch (e) { throw ns.E(code, what + " must be an INTEGER", e); }
|
|
620
621
|
}
|
|
622
|
+
// Resolve a registered OID name -> dotted string, throwing at module load on a typo (a decoder keyed
|
|
623
|
+
// by an unresolved name would silently never match, treating a real extension/attribute as opaque).
|
|
624
|
+
function O(nm) {
|
|
625
|
+
var d = ns.oid.byName(nm);
|
|
626
|
+
if (typeof d !== "string") throw new TypeError("pki.schema.pkix: " + JSON.stringify(nm) + " is not a registered OID name");
|
|
627
|
+
return d;
|
|
628
|
+
}
|
|
629
|
+
return { decodeTop: decodeTop, seqChildren: seqChildren, readInt: readInt, O: O };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function certExtensionDecoders(ns) {
|
|
633
|
+
var GN_SUBTREE = generalName(ns, { decodeValue: true, subtreeBase: true, code: ns.prefix + "/bad-name-constraints" });
|
|
634
|
+
var _h = _decodeHelpers(ns), decodeTop = _h.decodeTop, seqChildren = _h.seqChildren, readInt = _h.readInt, O = _h.O;
|
|
621
635
|
|
|
622
636
|
// basicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLen INTEGER (0..MAX) OPTIONAL }
|
|
623
637
|
function basicConstraints(buf) {
|
|
@@ -910,15 +924,6 @@ function certExtensionDecoders(ns) {
|
|
|
910
924
|
});
|
|
911
925
|
}
|
|
912
926
|
|
|
913
|
-
// byName returns undefined for an unregistered name; keying a decoder row
|
|
914
|
-
// under "undefined" would silently drop that extension from dispatch (the
|
|
915
|
-
// consumer would treat the extension as structurally unvalidated / absent),
|
|
916
|
-
// so a typo'd key-list entry must fail here, at module load.
|
|
917
|
-
function O(nm) {
|
|
918
|
-
var d = ns.oid.byName(nm);
|
|
919
|
-
if (typeof d !== "string") throw new TypeError("certExtensionDecoders: " + JSON.stringify(nm) + " is not a registered OID name");
|
|
920
|
-
return d;
|
|
921
|
-
}
|
|
922
927
|
// qcStatements ::= SEQUENCE OF QCStatement (RFC 3739 sec. 3.2.6). QCStatement ::= SEQUENCE {
|
|
923
928
|
// statementId OBJECT IDENTIFIER, statementInfo ANY DEFINED BY statementId OPTIONAL }. Known statementIds
|
|
924
929
|
// (RFC 3739 id-qcs + the ETSI EN 319 412-5 esi4 catalog) decode their statementInfo; an unknown statementId
|
|
@@ -1319,6 +1324,7 @@ module.exports = {
|
|
|
1319
1324
|
rawNonEmptySequence: rawNonEmptySequence,
|
|
1320
1325
|
CRL_REASON_NAMES: CRL_REASON_NAMES,
|
|
1321
1326
|
certExtensionDecoders: certExtensionDecoders,
|
|
1327
|
+
_decodeHelpers: _decodeHelpers,
|
|
1322
1328
|
attribute: attribute,
|
|
1323
1329
|
extension: extension,
|
|
1324
1330
|
extensions: extensions,
|
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:81b0b6a6-dee2-4e3f-b995-abbe40a0f560",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-17T00:29:40.078Z",
|
|
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.33",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.33",
|
|
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.33",
|
|
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.33",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|