@blamejs/pki 0.3.28 → 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.
@@ -70,25 +70,34 @@ function httpDateMs(s, refMs) {
70
70
  // gates absence with its own code) is a delay-seconds integer -> retryAfterSeconds, or an HTTP-date ->
71
71
  // retryAfterDate (epoch ms) plus, when opts.now (epoch ms) is given, a bounded retryAfterSeconds. Either
72
72
  // form beyond the one-year ceiling, or a value that is neither, fails closed via opts.E(opts.code, ...).
73
- // Never slept on here -- the value is SURFACED for the caller to decide.
73
+ // opts.cap (seconds) clamps a value above the cap to the cap instead of rejecting it; opts.lenient surfaces
74
+ // an otherwise-rejected value as a null retryAfterSeconds -- both for a caller (e.g. an ARI poll cadence)
75
+ // for whom the value is advisory and must not discard the response. Never slept on here -- the value is
76
+ // SURFACED for the caller to decide.
74
77
  function parse(value, opts) {
75
78
  opts = opts || {};
76
79
  var raStr = String(value).trim();
77
80
  var out = { retryAfterSeconds: null, retryAfterDate: null };
78
81
  if (/^\d+$/.test(raStr)) {
79
82
  var n = parseInt(raStr, 10);
80
- if (!Number.isSafeInteger(n) || n > MAX_RETRY_AFTER_SECONDS) throw _fail(opts, "the Retry-After delay is out of the supported range (0.." + MAX_RETRY_AFTER_SECONDS + " seconds)");
83
+ // opts.cap (seconds): a caller that will clamp anyway (e.g. an ARI poll cadence) wants a delay ABOVE its
84
+ // ceiling reduced to the ceiling, not rejected -- so a valid-but-huge value never discards the response.
85
+ if (typeof opts.cap === "number" && n > opts.cap) { out.retryAfterSeconds = opts.cap; return out; }
86
+ // opts.lenient: a caller for whom the value is purely advisory wants an UNPARSEABLE one surfaced as
87
+ // null (retryAfterSeconds stays null) rather than a hard reject that would discard the whole response.
88
+ if (!Number.isSafeInteger(n) || n > MAX_RETRY_AFTER_SECONDS) { if (opts.lenient) return out; throw _fail(opts, "the Retry-After delay is out of the supported range (0.." + MAX_RETRY_AFTER_SECONDS + " seconds)"); }
81
89
  out.retryAfterSeconds = n;
82
90
  return out;
83
91
  }
84
92
  var when = httpDateMs(raStr, opts.now);
85
- if (isNaN(when)) throw _fail(opts, "a Retry-After must be delay-seconds or a valid HTTP-date (RFC 7231 sec. 7.1.1.1/7.1.3), got " + JSON.stringify(raStr));
93
+ if (isNaN(when)) { if (opts.lenient) return out; throw _fail(opts, "a Retry-After must be delay-seconds or a valid HTTP-date (RFC 7231 sec. 7.1.1.1/7.1.3), got " + JSON.stringify(raStr)); }
86
94
  out.retryAfterDate = when;
87
95
  if (typeof opts.now === "number" && isFinite(opts.now)) {
88
96
  // Round the remaining whole-second delay UP (a sub-second date must not retry before the requested
89
97
  // time), clamping a past date to 0.
90
98
  var d = Math.max(0, Math.ceil((when - opts.now) / constants.TIME.seconds(1)));
91
- if (d > MAX_RETRY_AFTER_SECONDS) throw _fail(opts, "the Retry-After date is beyond the supported horizon (" + MAX_RETRY_AFTER_SECONDS + " seconds)");
99
+ if (typeof opts.cap === "number" && d > opts.cap) { out.retryAfterSeconds = opts.cap; return out; }
100
+ if (d > MAX_RETRY_AFTER_SECONDS) { if (opts.lenient) return out; throw _fail(opts, "the Retry-After date is beyond the supported horizon (" + MAX_RETRY_AFTER_SECONDS + " seconds)"); }
92
101
  out.retryAfterSeconds = d;
93
102
  }
94
103
  return out;
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
@@ -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
- 6: _name("localityName"),
65
- 7: _name("stateOrProvinceName"),
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("keyUsage"),
77
- 10: _name("authorityKeyIdentifier"),
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 (!node || node.majorType !== 0) throw _err("c509/bad-validity", label + " must be an unwrapped CBOR epoch integer (~time)");
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
- // An int extension value is a Defined CBOR item; v1 reconstructs only a byte-string value (a
205
- // non-byte-string value surfaces as null and fails closed at the type-3 reconstruction).
206
- valContent = valNode.content;
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 -- the canonical int per name (the lossy forward map is resolved to ONE
490
- // choice: EXT_BY_INT maps both 2 and 7 to keyUsage, so keyUsage encodes to the canonical draft int 2).
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 = { commonName: 1, surname: 2, serialNumber: 3, countryName: 4, localityName: 6, stateOrProvinceName: 7, organizationName: 8, organizationalUnitName: 9, title: 10 };
494
- var EXT_TO_INT = { subjectKeyIdentifier: 1, keyUsage: 2, subjectAltName: 3, basicConstraints: 4, authorityKeyIdentifier: 10 };
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
- if (ei !== undefined) {
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
- // a registered-int extension carries its extnValue DER bytes as a bare byte string.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.28",
3
+ "version": "0.3.31",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:ea40d20f-ad92-403e-8de1-cfd14bead6ba",
5
+ "serialNumber": "urn:uuid:1d3e8266-55f8-442b-877c-abe084a62726",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-01T18:47:21.356Z",
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.28",
22
+ "bom-ref": "@blamejs/pki@0.3.31",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.28",
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.28",
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.28",
57
+ "ref": "@blamejs/pki@0.3.31",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]