@blamejs/pki 0.3.29 → 0.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/lib/oid.js +14 -2
- package/lib/schema-c509.js +197 -19
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ 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.3.31 — 2026-08-03
|
|
8
|
+
|
|
9
|
+
pki.schema.c509 encodes and decodes the compact per-extension value forms -- a C509 certificate's keyUsage, basicConstraints, extended key usage, key identifiers, and other scalar extensions now ride in their specific draft-20 CBOR shape and interoperate with a conformant C509 implementation, not only this decoder.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.c509 encodes and decodes the compact per-extension value forms defined by draft-ietf-cose-cbor-encoded-cert-20 sec. 3.3 for the common scalar extensions: subjectKeyIdentifier (the bare key id), keyUsage (a network-byte-order integer), basicConstraints (an integer -- -2 for cA=false, -1 for cA=true with no path length, N for the path length), authorityKeyIdentifier (the bare key id, keyId-only form), extendedKeyUsage (an array of registry integers or unwrapped OIDs, the array omitted for a single purpose), inhibitAnyPolicy (an integer), OCSP No Check (CBOR null), and TLS Feature (an array of integers). Each form inverts to the DER extnValue byte-for-byte in both directions, so a C509 certificate carrying these extensions is the specific compact shape a conformant C509 implementation expects (draft-20 sec. 3.7 requires the specific form where one is defined) rather than an opaque DER byte string. The encoder is guarded: it emits the compact form only when it decodes back to the exact DER value, so a non-canonical or unrepresentable extension value falls back to the unwrapped-OID byte-string form instead of encoding lossily; a malformed compact value fails closed with a typed C509Error. The extended-key-usage integer shorthands cover the full draft-20 sec. 8.12 registry -- the RFC 5280 purposes (serverAuth, clientAuth, codeSigning, emailProtection, timeStamping, ocspSigning, anyExtendedKeyUsage) plus the SSH (RFC 6187), Kerberos PKINIT (RFC 4556), CMC (RFC 6402), and Wi-SUN purposes; any purpose outside the registry encodes as an unwrapped OID.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- pki.schema.c509's RDN-attribute (sec. 8.6) and extension (sec. 8.8) integer registries now match draft-ietf-cose-cbor-encoded-cert-20: authorityKeyIdentifier is extension integer 7, extendedKeyUsage 8, inhibitAnyPolicy 30, OCSP No Check 36, and TLS Feature 38; and the RDN attributes localityName, stateOrProvinceName, and streetAddress are integers 5, 6, and 7. A certificate carrying any of these attributes or extensions now encodes to -- and decodes from -- the draft-20 integers and compact value shapes, so it interoperates with a conformant C509 implementation; re-encode any C509 produced by an earlier release, whose integers and per-extension value shapes differ.
|
|
18
|
+
|
|
7
19
|
## v0.3.29 — 2026-08-02
|
|
8
20
|
|
|
9
21
|
The pki.acme client rounds out its RFC 8555 / RFC 9773 surface -- pre-authorize an identifier with client.newAuthz, choose among alternate issuance chains in client.downloadCertificate, and schedule renewal from the CA's ARI window with client.renewalWindow.
|
package/README.md
CHANGED
|
@@ -204,7 +204,7 @@ is callable today; nothing below is a stub.
|
|
|
204
204
|
| `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
|
|
205
205
|
| `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
|
|
206
206
|
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions — including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque) and the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
207
|
-
| `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands and the
|
|
207
|
+
| `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands, the C509 compressions, and the compact draft-20 per-extension value forms (keyUsage, basicConstraints, extended key usage, subject/authority key identifier, and the other scalar extensions); a certificate outside the invertible set throws a typed `C509Error` |
|
|
208
208
|
| `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
209
209
|
| `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
210
210
|
| `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
|
package/lib/oid.js
CHANGED
|
@@ -81,6 +81,8 @@ var FAMILIES = {
|
|
|
81
81
|
pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: {
|
|
82
82
|
authorityInfoAccess: 1, acAuditIdentity: 4, aaControls: 6, acProxying: 10,
|
|
83
83
|
acmeIdentifier: 31,
|
|
84
|
+
// id-pe-tlsfeature (RFC 7633) -- the TLS Feature (formerly "must-staple") extension.
|
|
85
|
+
tlsFeature: 24,
|
|
84
86
|
// id-pe-qcStatements (RFC 3739 sec. 3.2.6) -- the qualified-certificate-statements extension.
|
|
85
87
|
qcStatements: 3 } },
|
|
86
88
|
|
|
@@ -138,9 +140,19 @@ var FAMILIES = {
|
|
|
138
140
|
regInfo: { base: [1, 3, 6, 1, 5, 5, 7, 5, 2], of: { utf8Pairs: 1, certReq: 2 } },
|
|
139
141
|
|
|
140
142
|
// PKIX extended key purposes (id-kp, RFC 5280 sec. 4.2.1.12). timeStamping is required
|
|
141
|
-
// -- critical and sole -- on an RFC 3161 TSA signing certificate (sec. 2.3).
|
|
143
|
+
// -- critical and sole -- on an RFC 3161 TSA signing certificate (sec. 2.3). The SSH
|
|
144
|
+
// (RFC 6187), CMC (RFC 6402), and Bundle Security (RFC 9174) purposes complete the C509
|
|
145
|
+
// Extended Key Usages registry (draft-ietf-cose-cbor-encoded-cert sec. 8.12).
|
|
142
146
|
pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
|
|
143
|
-
serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9
|
|
147
|
+
serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9,
|
|
148
|
+
secureShellClient: 21, secureShellServer: 22, cmcCA: 27, cmcRA: 28, cmcArchive: 29, cmKGA: 32, bundleSecurity: 35 } },
|
|
149
|
+
|
|
150
|
+
// Kerberos PKINIT extended key purposes (id-pkinit, RFC 4556 sec. 3.2.3 / 3.2.4): the client
|
|
151
|
+
// and KDC authentication EKUs (C509 Extended Key Usages registry integers 10 / 11).
|
|
152
|
+
pkinitKp: { base: [1, 3, 6, 1, 5, 2, 3], of: { pkinitClientAuth: 4, pkinitKdc: 5 } },
|
|
153
|
+
|
|
154
|
+
// Wi-SUN FAN device extended key purpose (C509 Extended Key Usages registry integer 20).
|
|
155
|
+
wisun: { base: [1, 3, 6, 1, 4, 1, 45605], of: { fanDevice: 1 } },
|
|
144
156
|
|
|
145
157
|
// Google Certificate Transparency (RFC 6962) on the 1.3.6.1.4.1.11129.2.4 arc:
|
|
146
158
|
// the SCT-list X.509 extension (sec. 3.3), the precertificate poison (sec. 3.1), the
|
package/lib/schema-c509.js
CHANGED
|
@@ -55,27 +55,53 @@ var PK_ALG_BY_INT = {
|
|
|
55
55
|
// EC curve -> field size in bytes (the SEC1 coordinate width), for point-length validation.
|
|
56
56
|
var EC_FIELD_BYTES = { "prime256v1": 32, "secp384r1": 48, "secp521r1": 66 };
|
|
57
57
|
|
|
58
|
-
// sec. 8.6 attribute types (abs(int) -> name; the sign selects the X.509 string type).
|
|
58
|
+
// sec. 8.6 RDN attribute types (abs(int) -> name; the sign selects the X.509 string type). emailAddress
|
|
59
|
+
// (int 0) is an IA5String value not covered by the DirectoryString SpecialText codec below, so it is
|
|
60
|
+
// declined; a DN carrying it encodes via the ~oid attribute form, not this alias table.
|
|
59
61
|
var ATTR_BY_INT = {
|
|
60
62
|
1: _name("commonName"),
|
|
61
63
|
2: _name("surname"),
|
|
62
64
|
3: _name("serialNumber"),
|
|
63
65
|
4: _name("countryName"),
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
5: _name("localityName"),
|
|
67
|
+
6: _name("stateOrProvinceName"),
|
|
68
|
+
7: _name("streetAddress"),
|
|
66
69
|
8: _name("organizationName"),
|
|
67
70
|
9: _name("organizationalUnitName"),
|
|
68
71
|
10: _name("title"),
|
|
69
72
|
};
|
|
70
|
-
// sec. 8.8 extension types (abs(int) -> name; the sign selects criticality).
|
|
73
|
+
// sec. 8.8 extension types (abs(int) -> name; the sign selects criticality). The general-name-bearing
|
|
74
|
+
// extensions (subjectAltName et al) carry an int extensionID only when their compact value is supported;
|
|
75
|
+
// subjectAltName stays registered for the byte-string value form (the compact GeneralNames array awaits
|
|
76
|
+
// the sec. 8.13 general-name codec and fails closed).
|
|
71
77
|
var EXT_BY_INT = {
|
|
72
78
|
1: _name("subjectKeyIdentifier"),
|
|
73
79
|
2: _name("keyUsage"),
|
|
74
80
|
3: _name("subjectAltName"),
|
|
75
81
|
4: _name("basicConstraints"),
|
|
76
|
-
7: _name("
|
|
77
|
-
|
|
82
|
+
7: _name("authorityKeyIdentifier"),
|
|
83
|
+
8: _name("extKeyUsage"),
|
|
84
|
+
30: _name("inhibitAnyPolicy"),
|
|
85
|
+
36: _name("ocspNoCheck"),
|
|
86
|
+
38: _name("tlsFeature"),
|
|
78
87
|
};
|
|
88
|
+
// The extensions whose compact CBOR value form (sec. 3.3) is invertible to/from the DER extnValue.
|
|
89
|
+
// A registered extension NOT in this set carries its value as the ~oid + byte-string form (sec. 3.7).
|
|
90
|
+
var EXT_COMPACT = {
|
|
91
|
+
subjectKeyIdentifier: 1, keyUsage: 1, basicConstraints: 1, authorityKeyIdentifier: 1,
|
|
92
|
+
extKeyUsage: 1, inhibitAnyPolicy: 1, ocspNoCheck: 1, tlsFeature: 1,
|
|
93
|
+
};
|
|
94
|
+
// sec. 8.12 Extended Key Usages registry (C509 int -> registered id-kp purpose name). A KeyPurposeId
|
|
95
|
+
// outside this set encodes as an unwrapped ~oid; a C509 int outside it fails closed on decode.
|
|
96
|
+
var EKU_BY_INT = {
|
|
97
|
+
0: _name("anyExtendedKeyUsage"), 1: _name("serverAuth"), 2: _name("clientAuth"),
|
|
98
|
+
3: _name("codeSigning"), 4: _name("emailProtection"), 8: _name("timeStamping"), 9: _name("ocspSigning"),
|
|
99
|
+
10: _name("pkinitClientAuth"), 11: _name("pkinitKdc"), 12: _name("secureShellClient"), 13: _name("secureShellServer"),
|
|
100
|
+
14: _name("bundleSecurity"), 15: _name("cmcCA"), 16: _name("cmcRA"), 17: _name("cmcArchive"), 18: _name("cmKGA"),
|
|
101
|
+
20: _name("fanDevice"),
|
|
102
|
+
};
|
|
103
|
+
var EKU_TO_INT = {}; // dotted OID -> C509 int (the encode inverse; keyed on the resolved OID)
|
|
104
|
+
Object.keys(EKU_BY_INT).forEach(function (k) { EKU_TO_INT[oid.byName(EKU_BY_INT[k])] = Number(k); });
|
|
79
105
|
|
|
80
106
|
// ---- field readers (the unwrapped ~biguint / ~time / ~oid contracts; draft-20 sec. 3.1) ----
|
|
81
107
|
|
|
@@ -94,7 +120,7 @@ function _biguint(node, code, label) {
|
|
|
94
120
|
// MUST reject. Bound to the Date window; the CBOR simple null (permitted only for notAfter) -> null.
|
|
95
121
|
function _time(node, allowNull, label) {
|
|
96
122
|
if (allowNull && node.majorType === 7 && node.ai === 22) return null; // CBOR simple null (0xF6)
|
|
97
|
-
if (
|
|
123
|
+
if (node.majorType !== 0) throw _err("c509/bad-validity", label + " must be an unwrapped CBOR epoch integer (~time)");
|
|
98
124
|
var secs = node.argument;
|
|
99
125
|
if (secs > MAX_EPOCH_SECONDS) throw _err("c509/bad-validity", label + " is outside the representable Date range");
|
|
100
126
|
return new Date(C.TIME.seconds(Number(secs)));
|
|
@@ -179,6 +205,143 @@ function _name509(node, isSubject) {
|
|
|
179
205
|
}
|
|
180
206
|
function _shortName(n) { return n === "commonName" ? "CN" : n === "countryName" ? "C" : n === "organizationName" ? "O" : n === "organizationalUnitName" ? "OU" : n === "localityName" ? "L" : n === "stateOrProvinceName" ? "ST" : n; }
|
|
181
207
|
|
|
208
|
+
// ---- compact per-extension value codec (draft-20 sec. 3.3) -------------------
|
|
209
|
+
// Each registered extension in EXT_COMPACT has a specific CBOR encoding of its value that is invertible
|
|
210
|
+
// to/from the DER extnValue inner content (the bytes _reconExtensions wraps in an OCTET STRING). Native
|
|
211
|
+
// C509 MUST use the specific form (sec. 3.7); the decode side reconstructs the DER, the encode side emits
|
|
212
|
+
// the compact form only when it round-trips byte-for-byte (else the ~oid + byte-string fallback).
|
|
213
|
+
|
|
214
|
+
// A CBOR unsigned integer node -> its BigInt value (major type 0 guarantees non-negative). Arbitrary
|
|
215
|
+
// precision: a DER INTEGER holds any width, so no cap -- the round-trip guard verifies the reconstruction.
|
|
216
|
+
function _cborUint(node, label) {
|
|
217
|
+
if (node.majorType !== 0) throw _err("c509/bad-extensions", "a " + label + " value must be a CBOR unsigned integer");
|
|
218
|
+
return cbor.read.int(node);
|
|
219
|
+
}
|
|
220
|
+
// A CBOR integer node (either sign) -> its BigInt value (basicConstraints uses -2/-1/pathLen).
|
|
221
|
+
function _cborIntVal(node, label) {
|
|
222
|
+
if (node.majorType !== 0 && node.majorType !== 1) throw _err("c509/bad-extensions", "a " + label + " value must be a CBOR integer");
|
|
223
|
+
return cbor.read.int(node);
|
|
224
|
+
}
|
|
225
|
+
// A C509 KeyPurposeId (int registry alias OR unwrapped ~oid) -> the dotted extended-key-usage OID.
|
|
226
|
+
function _ekuPurposeOid(node) {
|
|
227
|
+
if (node.majorType === 0 || node.majorType === 1) {
|
|
228
|
+
var i = Number(cbor.read.int(node));
|
|
229
|
+
var nm = EKU_BY_INT[i];
|
|
230
|
+
if (nm === undefined) throw _err("c509/bad-extensions", "an extKeyUsage int " + i + " has no C509 registry row");
|
|
231
|
+
return oid.byName(nm);
|
|
232
|
+
}
|
|
233
|
+
// A ~oid KeyPurposeId routes through _oidName, which validates the BER OID content and remaps an oid/*
|
|
234
|
+
// fault to c509/bad-extensions (fault preserved as .cause) -- a non-byte-string node fails closed there too.
|
|
235
|
+
return _oidName(node, "c509/bad-extensions", "an extKeyUsage KeyPurposeId").oid;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Decode a compact extension value (a decoded CBOR node) to the DER extnValue inner content. Fails closed
|
|
239
|
+
// (c509/bad-extensions) on a CBOR shape the named extension does not define.
|
|
240
|
+
function _extValueToDer(name, node) {
|
|
241
|
+
switch (name) {
|
|
242
|
+
case "subjectKeyIdentifier": // KeyIdentifier = bytes -> OCTET STRING(keyid)
|
|
243
|
+
if (node.majorType !== 2) throw _err("c509/bad-extensions", "a subjectKeyIdentifier value must be a CBOR byte string");
|
|
244
|
+
return b.octetString(node.content);
|
|
245
|
+
case "keyUsage": // uint -> KeyUsage BIT STRING (a value past the 9 named bits fails closed)
|
|
246
|
+
return _reconKeyUsageBits(Number(_cborUint(node, "keyUsage")));
|
|
247
|
+
case "basicConstraints": { // int -> SEQUENCE { cA?, pathLen? }
|
|
248
|
+
var iv = _cborIntVal(node, "basicConstraints");
|
|
249
|
+
if (iv === -2n) return b.sequence([]); // cA false (omitted)
|
|
250
|
+
if (iv === -1n) return b.sequence([b.boolean(true)]); // cA true, no pathLen
|
|
251
|
+
if (iv >= 0n) return b.sequence([b.boolean(true), b.integer(iv)]); // cA true, pathLen
|
|
252
|
+
throw _err("c509/bad-extensions", "a basicConstraints int " + iv + " is outside the -2/-1/pathLen range");
|
|
253
|
+
}
|
|
254
|
+
case "authorityKeyIdentifier": // keyId-only bytes -> SEQUENCE { [0] IMPLICIT keyIdentifier }
|
|
255
|
+
if (node.majorType !== 2) throw _err("c509/bad-extensions", "an authorityKeyIdentifier value must be a CBOR byte string (keyId-only form; the 3-tuple form is not supported)");
|
|
256
|
+
return b.sequence([b.contextPrimitive(0, node.content)]);
|
|
257
|
+
case "extKeyUsage": { // [2* KeyPurposeId] / KeyPurposeId -> SEQUENCE OF OID (draft-20 sec. 3.3)
|
|
258
|
+
var items;
|
|
259
|
+
if (node.majorType === 4) {
|
|
260
|
+
items = node.children || [];
|
|
261
|
+
if (items.length < 2) throw _err("c509/bad-extensions", "an extKeyUsage array must hold 2 or more KeyPurposeIds; a single purpose omits the array (draft-20 sec. 3.3)");
|
|
262
|
+
} else {
|
|
263
|
+
items = [node];
|
|
264
|
+
}
|
|
265
|
+
return b.sequence(items.map(function (it) { return b.oid(_ekuPurposeOid(it)); }));
|
|
266
|
+
}
|
|
267
|
+
case "inhibitAnyPolicy": // uint -> INTEGER SkipCerts
|
|
268
|
+
return b.integer(_cborUint(node, "inhibitAnyPolicy"));
|
|
269
|
+
case "ocspNoCheck": // null -> NULL
|
|
270
|
+
if (node.majorType !== 7 || !(Buffer.isBuffer(node.bytes) && node.bytes.length === 1 && node.bytes[0] === 0xf6)) throw _err("c509/bad-extensions", "an ocspNoCheck value must be the CBOR simple value null");
|
|
271
|
+
return b.nullValue();
|
|
272
|
+
case "tlsFeature": { // [uint ...] -> SEQUENCE OF INTEGER
|
|
273
|
+
if (node.majorType !== 4) throw _err("c509/bad-extensions", "a tlsFeature value must be a CBOR array");
|
|
274
|
+
return b.sequence((node.children || []).map(function (f) { return b.integer(_cborUint(f, "tlsFeature feature")); }));
|
|
275
|
+
}
|
|
276
|
+
default:
|
|
277
|
+
throw _err("c509/bad-extensions", "extension " + name + " has no compact value decoder");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Encode a DER extnValue inner content to its compact CBOR value bytes, or null when the DER is not the
|
|
282
|
+
// canonical form the compact encoding covers (the caller then emits the ~oid + byte-string fallback).
|
|
283
|
+
function _extValueFromDer(name, der) {
|
|
284
|
+
var node;
|
|
285
|
+
// A malformed DER extnValue (an ill-formed OID/INTEGER child, a truncated SEQUENCE) is simply not
|
|
286
|
+
// compact-encodable; any decode/read fault surfaces as null so the caller emits the ~oid byte-string form.
|
|
287
|
+
// Wrong-type children throw from asn1.read.* / cbor.build.uint (a negative value) and surface as the null
|
|
288
|
+
// fallback; the encode-side round-trip guard is the final net, so only the structural dispatch is inline.
|
|
289
|
+
try {
|
|
290
|
+
node = asn1.decode(der);
|
|
291
|
+
switch (name) {
|
|
292
|
+
case "subjectKeyIdentifier": // OCTET STRING(keyid) -> the bare key id
|
|
293
|
+
return cbor.build.byteString(asn1.read.octetString(node));
|
|
294
|
+
case "keyUsage": { // BIT STRING -> uint
|
|
295
|
+
var bits = _keyUsageBitsFromDer(der);
|
|
296
|
+
return bits == null ? null : cbor.build.uint(BigInt(bits));
|
|
297
|
+
}
|
|
298
|
+
case "basicConstraints": { // SEQUENCE { cA?, pathLen? } -> int
|
|
299
|
+
var kids = node.children || [];
|
|
300
|
+
if (kids.length === 0) return cbor.build.int(-2n); // cA absent (a non-SEQUENCE falls back via the guard)
|
|
301
|
+
if (asn1.read.boolean(kids[0]) !== true) return null; // explicit cA=false is non-canonical -> fall back
|
|
302
|
+
if (kids.length === 1) return cbor.build.int(-1n);
|
|
303
|
+
if (kids.length !== 2) return null;
|
|
304
|
+
return cbor.build.uint(asn1.read.integer(kids[1])); // pathLen (>= 0; a negative INTEGER throws -> fall back)
|
|
305
|
+
}
|
|
306
|
+
case "authorityKeyIdentifier": { // SEQUENCE { [0] keyId } -> the bare key id
|
|
307
|
+
var akids = node.children || [];
|
|
308
|
+
if (akids.length !== 1 || akids[0].tagNumber !== 0) return null; // 3-tuple / other forms fall back
|
|
309
|
+
return cbor.build.byteString(akids[0].content);
|
|
310
|
+
}
|
|
311
|
+
case "extKeyUsage": { // SEQUENCE OF OID -> [int/~oid ...] / single
|
|
312
|
+
var purposes = node.children || [];
|
|
313
|
+
if (!purposes.length) return null;
|
|
314
|
+
var out = purposes.map(function (p) {
|
|
315
|
+
var dotted = asn1.read.oid(p), pint = EKU_TO_INT[dotted];
|
|
316
|
+
return pint !== undefined ? cbor.build.int(BigInt(pint)) : cbor.build.byteString(asn1.encodeOidContent(dotted));
|
|
317
|
+
});
|
|
318
|
+
return out.length === 1 ? out[0] : cbor.build.array(out);
|
|
319
|
+
}
|
|
320
|
+
case "inhibitAnyPolicy": // INTEGER -> uint
|
|
321
|
+
return cbor.build.uint(asn1.read.integer(node));
|
|
322
|
+
case "ocspNoCheck": // NULL -> null
|
|
323
|
+
asn1.read.nullValue(node);
|
|
324
|
+
return cbor.build.nullValue();
|
|
325
|
+
case "tlsFeature": // SEQUENCE OF INTEGER -> [uint ...]
|
|
326
|
+
return cbor.build.array((node.children || []).map(function (f) { return cbor.build.uint(asn1.read.integer(f)); }));
|
|
327
|
+
default:
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
} catch (_e) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Encode-side guard: emit the compact value only when it decodes back to the EXACT DER extnValue, so a
|
|
336
|
+
// non-canonical or unrepresentable value can never produce a lossy compact form (it falls back to ~oid).
|
|
337
|
+
// _extValueToDer is the exact inverse of _extValueFromDer, so the round-trip decode does not throw here;
|
|
338
|
+
// the guard is the byte comparison (a canonical-equivalent DER differs and falls back).
|
|
339
|
+
function _tryCompactExtValue(name, der) {
|
|
340
|
+
var compact = _extValueFromDer(name, der);
|
|
341
|
+
if (compact == null) return null;
|
|
342
|
+
return _extValueToDer(name, cbor.decode(compact)).equals(der) ? compact : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
182
345
|
// extensions (sec. 3.1.10/sec. 3.3/sec. 8.8): [ * Extension ] | a single keyUsage int-shortcut.
|
|
183
346
|
function _extensions(node) {
|
|
184
347
|
// The keyUsage int-shortcut (sec. 3.1.10): a bare int -> one keyUsage extension, criticality from the
|
|
@@ -201,9 +364,18 @@ function _extensions(node) {
|
|
|
201
364
|
name = EXT_BY_INT[Math.abs(ei)];
|
|
202
365
|
if (name === undefined) throw _err("c509/bad-extensions", "extension type integer " + ei + " has no C509 registry row");
|
|
203
366
|
extOid = oid.byName(name); critical = ei < 0;
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
367
|
+
// A registered extension with a compact value form (sec. 3.3) decodes its specific CBOR value to the
|
|
368
|
+
// DER extnValue inner content. A registered extension WITHOUT one (subjectAltName, until the general-
|
|
369
|
+
// name value codec lands) can only reconstruct a byte-string value (the raw DER extnValue); a
|
|
370
|
+
// non-byte-string value there is an unsupported compact form and MUST fail closed -- a text/array value
|
|
371
|
+
// is NOT raw DER, and copying its bytes would reconstruct a structurally invalid extension.
|
|
372
|
+
if (EXT_COMPACT[name]) {
|
|
373
|
+
valContent = _extValueToDer(name, valNode);
|
|
374
|
+
} else if (valNode.majorType === 2) {
|
|
375
|
+
valContent = valNode.content;
|
|
376
|
+
} else {
|
|
377
|
+
throw _err("c509/bad-extensions", "extension " + name + " has no compact value codec; its int-form value must be a byte string (draft-20 sec. 3.3)");
|
|
378
|
+
}
|
|
207
379
|
} else {
|
|
208
380
|
var r = _oidName(idNode, "c509/bad-extensions", "an extension id");
|
|
209
381
|
name = r.name; extOid = r.oid;
|
|
@@ -486,12 +658,16 @@ function matches(node) {
|
|
|
486
658
|
// FLAGSHIP type-3 forward transform (parse(encode(der)).reconstructedDer == der, so the original signature
|
|
487
659
|
// verifies); a c509.parse result -> re-emit its native array. Signing-free (mirrors ct.encodeSctList).
|
|
488
660
|
|
|
489
|
-
// The registry INVERSE tables
|
|
490
|
-
//
|
|
661
|
+
// The registry INVERSE tables (name -> canonical int). The RDN-attribute and extension inverses are
|
|
662
|
+
// DERIVED from the *_BY_INT decode tables so the two directions cannot drift apart (a hand-kept inverse
|
|
663
|
+
// once carried a stale pre-draft-20 numbering); the extension inverse keeps only the compact-encodable
|
|
664
|
+
// rows, so a registered extension without a value codec (subjectAltName) routes to the ~oid form.
|
|
491
665
|
var SIG_ALG_TO_INT = { ecdsaWithSHA256: 0, ecdsaWithSHA384: 1, ecdsaWithSHA512: 2 };
|
|
492
666
|
var PK_ALG_TO_INT = { rsaEncryption: 0, "ecPublicKey|prime256v1": 1, "ecPublicKey|secp384r1": 2, "ecPublicKey|secp521r1": 3 };
|
|
493
|
-
var ATTR_TO_INT = {
|
|
494
|
-
|
|
667
|
+
var ATTR_TO_INT = {};
|
|
668
|
+
Object.keys(ATTR_BY_INT).forEach(function (k) { ATTR_TO_INT[ATTR_BY_INT[k]] = Number(k); });
|
|
669
|
+
var EXT_TO_INT = {};
|
|
670
|
+
Object.keys(EXT_BY_INT).forEach(function (k) { if (EXT_COMPACT[EXT_BY_INT[k]]) EXT_TO_INT[EXT_BY_INT[k]] = Number(k); });
|
|
495
671
|
|
|
496
672
|
// A non-negative BigInt -> its minimal big-endian ~biguint bytes (the leading 0x00 sign octet omitted).
|
|
497
673
|
function _minBytes(n) {
|
|
@@ -548,13 +724,15 @@ function _encExtensions(exts) {
|
|
|
548
724
|
var items = [];
|
|
549
725
|
exts.forEach(function (ext) {
|
|
550
726
|
var ei = EXT_TO_INT[ext.name];
|
|
551
|
-
|
|
727
|
+
// A registered extension with a compact value form emits int extID + the specific CBOR value, but
|
|
728
|
+
// only when that value inverts to the EXACT DER extnValue (the round-trip guard); otherwise it falls
|
|
729
|
+
// through to the conformant ~oid + byte-string form (sec. 3.7) -- never a lossy compact encoding.
|
|
730
|
+
var compact = (ei !== undefined && Buffer.isBuffer(ext.value)) ? _tryCompactExtValue(ext.name, ext.value) : null;
|
|
731
|
+
if (compact != null) {
|
|
552
732
|
items.push(cbor.build.int(BigInt(ext.critical ? -ei : ei)));
|
|
553
|
-
|
|
554
|
-
if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + ext.name + " has no byte-string value to encode");
|
|
555
|
-
items.push(cbor.build.byteString(ext.value));
|
|
733
|
+
items.push(compact);
|
|
556
734
|
} else {
|
|
557
|
-
items.push(cbor.build.byteString(asn1.encodeOidContent(ext.oid))); // ~oid extension id
|
|
735
|
+
items.push(cbor.build.byteString(asn1.encodeOidContent(ext.oid || oid.byName(ext.name)))); // ~oid extension id
|
|
558
736
|
if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + (ext.oid || ext.name) + " has no byte-string value to encode");
|
|
559
737
|
var bs = cbor.build.byteString(ext.value);
|
|
560
738
|
items.push(ext.critical ? cbor.build.array([bs]) : bs); // critical ~oid value wraps in a 1-element array
|
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:1d3e8266-55f8-442b-877c-abe084a62726",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-08-
|
|
8
|
+
"timestamp": "2026-08-03T23:07:28.101Z",
|
|
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.3.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.31",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.31",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.3.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.31",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.3.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.31",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|