@blamejs/pki 0.8.3 → 0.8.5

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.
@@ -193,8 +193,15 @@ function matches(root) {
193
193
  }
194
194
 
195
195
 
196
- function walkPrivateKeyInfo(node) { return schema.walk(PRIVATE_KEY_INFO, node, NS).result; }
197
- function walkEncryptedPrivateKeyInfo(node) { return schema.walk(ENCRYPTED_PRIVATE_KEY_INFO, node, NS).result; }
196
+ /** @internal `ctx` is the walk context of the parse that reached this key, when another format
197
+ * carries it. Only its caps and budget cross over: the key is still read in this namespace, so a
198
+ * fault in it keeps the PKCS#8 error class and code. */
199
+ function walkPrivateKeyInfo(node, ctx) {
200
+ return schema.walk(PRIVATE_KEY_INFO, node, schema.withResources(NS, ctx)).result;
201
+ }
202
+ function walkEncryptedPrivateKeyInfo(node, ctx) {
203
+ return schema.walk(ENCRYPTED_PRIVATE_KEY_INFO, node, schema.withResources(NS, ctx)).result;
204
+ }
198
205
 
199
206
  module.exports = {
200
207
  parse: parse,
@@ -222,17 +222,16 @@ function _isPemArmor(buf) {
222
222
  /** @internal The codec's resource refusals, as distinct from its malformed-input refusals. A
223
223
  * caller who tightened a cap has to be able to tell "your bytes are wrong" from "your cap is
224
224
  * too small", and only the second is answered by raising the cap. */
225
- var RESOURCE_REFUSALS = intrinsic.assign(intrinsic.create(null), {
226
- "asn1/too-large": 1, "asn1/too-deep": 1, "asn1/too-many-items": 1,
227
- });
225
+ var _isResourceRefusal = schema.isResourceRefusal;
228
226
 
229
227
  function decodeRoot(der, opts, caps, budget) {
228
+ // allow:bare-decode the root decode a door performs, which is where the caller's caps enter.
230
229
  try { return asn1.decode(der, _decodeOptions(opts, caps)); }
231
230
  catch (e) {
232
231
  /** @internal `rawDecodeErrors` keeps the codec's own code for a malformed body, which some
233
232
  * callers pin. A resource refusal is still translated, that code being new. */
234
- if (opts.rawDecodeErrors && !(e && RESOURCE_REFUSALS[e.code])) throw e;
235
- if (e && RESOURCE_REFUSALS[e.code]) {
233
+ if (opts.rawDecodeErrors && !_isResourceRefusal(e)) throw e;
234
+ if (_isResourceRefusal(e)) {
236
235
  if (budget) {
237
236
  try {
238
237
  budget.trip(opts.what || "the input");
@@ -338,12 +337,18 @@ var _emptyOptions = intrinsic.freeze(intrinsic.create(null));
338
337
  function runParseWith(input, opts, callerOpts, build) {
339
338
  var caps = parseCaps(callerOpts, opts);
340
339
  var budget = guard.limits.budget({ maxBytes: caps.maxBytes });
341
- var ns = _nsWithBudget(opts.ns, budget);
340
+ var ns = _nsWithBudget(opts.ns, budget, caps);
342
341
  var result;
343
342
  try {
344
343
  result = build(decodeRoot(coerceToDer(input, opts), opts, caps, budget), ns, caps, budget);
345
344
  } catch (e) {
346
345
  if (guard.limits.isBudgetExceeded(e)) throw _budgetRefusal(opts, e);
346
+ /** @internal A refusal that is already this door's own resource verdict has been translated
347
+ * once and carries the codec's refusal as its cause; wrapping it again would put a copy of
348
+ * the verdict where a caller looks for the cap that was exceeded. */
349
+ if (budget.exhausted() && !(e && e.code === opts.prefix + "/too-large")) {
350
+ throw _budgetRefusal(opts, e);
351
+ }
347
352
  throw e;
348
353
  }
349
354
  if (budget.exhausted()) throw _budgetRefusal(opts, null);
@@ -357,17 +362,31 @@ function runParse(input, opts, callerOpts) {
357
362
  }
358
363
 
359
364
  /** @internal The re-read at the door is what makes the latch worth having: a catch anywhere
360
- * under the walk may absorb the exception, and this still refuses. */
365
+ * under the walk may absorb the exception or rewrap it as a domain error, and this still
366
+ * refuses. The rewrapped error is carried as the cause. */
361
367
  function _budgetRefusal(opts, cause) {
362
368
  return new opts.ErrorClass(opts.prefix + "/too-large",
363
369
  (opts.what || "the input") + " exceeds the decode budget for this parse", cause || undefined);
364
370
  }
365
371
 
366
- function _nsWithBudget(ns, budget) {
367
- var out = intrinsic.create(null);
368
- _forEach(intrinsic.keys(ns), function (k) { out[k] = ns[k]; });
369
- out.budget = budget;
370
- return out;
372
+ function _nsWithBudget(ns, budget, caps) {
373
+ return schema.withResources(ns, { budget: budget, caps: caps });
374
+ }
375
+
376
+ /** @internal The walk context a hand-rolled door builds for the structures it reads out of its
377
+ * own input, so the caller's caps reach them the way `runParseWith` makes them reach a factory
378
+ * parser's. The door re-reads `budget.exhausted()` before it returns a result. */
379
+ function nestedContext(ns, caps) {
380
+ var budget = guard.limits.budget({ maxBytes: caps.maxBytes });
381
+ return { ns: _nsWithBudget(ns, budget, caps), budget: budget };
382
+ }
383
+
384
+ /** @internal The decode a format performs on a value it has already read out of its input: an
385
+ * embedded OCTET STRING payload, an encapsulated protocol body. The engine owns it, because
386
+ * `schema.embeddedDer` performs the same decode from inside a walk and the two must apply the
387
+ * caller's caps and trip the parse budget the same way. This is the name format modules call. */
388
+ function decodeNested(bytes, ctx, label, opts) {
389
+ return schema.decodeNested(bytes, ctx, label, opts);
371
390
  }
372
391
 
373
392
  var DN_SHORT = constants.NAMES.DN_SHORT;
@@ -489,6 +508,7 @@ function attrValueToString(ns) {
489
508
  throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must be a non-empty even run of hex digits (RFC 4514 sec. 2.4)");
490
509
  }
491
510
  var raw = guard.encoding.hex(hex, null, ns.E, ns.prefix + "/bad-atv", "a #hex attribute value");
511
+ // allow:bare-decode the encoder side, checking bytes a caller wrote by hand, outside any parse.
492
512
  try { asn1.decode(raw); }
493
513
  catch (e) { throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must encode exactly one DER TLV", e); }
494
514
  return raw;
@@ -826,7 +846,7 @@ function decodeIssuingDistributionPointValue(ns, bytes, code) {
826
846
  indirect: false, onlyAttr: false, fields: null };
827
847
  var m;
828
848
  try {
829
- m = schema.walk(issuingDistributionPoint(code), asn1.decode(bytes), ns);
849
+ m = schema.walk(issuingDistributionPoint(code), schema.decodeNested(bytes, ns, "an IssuingDistributionPoint value"), ns);
830
850
  if (m.fields.distributionPoint.present) {
831
851
  var wrap = m.fields.distributionPoint.node;
832
852
  if (!wrap.children || wrap.children.length !== 1) {
@@ -928,15 +948,20 @@ function userNoticeTexts(qualifier) {
928
948
  return i === kids.length ? out : [];
929
949
  }
930
950
 
951
+ /** @internal The readers an extension or attribute value is decoded through. `ctx` is the walk
952
+ * context of the parse that reached the value, when one did: the bytes here are an OCTET STRING
953
+ * payload the root decode stopped at, so this decode restarts depth and item counting and needs
954
+ * the caller's caps handed to it. A caller outside a parse passes none and takes the built-in
955
+ * ceilings, which is what `ns` carries. */
931
956
  function _decodeHelpers(ns) {
932
- function decodeTop(buf, code, what) {
957
+ function decodeTop(buf, code, what, ctx) {
933
958
  var n;
934
- try { n = asn1.decode(buf); }
959
+ try { n = schema.decodeNested(buf, ctx || ns, what); }
935
960
  catch (e) { throw ns.E(code, "malformed " + what + " value: " + ((e && e.message) || _String(e)), e); }
936
961
  return n;
937
962
  }
938
- function seqChildren(buf, code, what) {
939
- var n = decodeTop(buf, code, what);
963
+ function seqChildren(buf, code, what, ctx) {
964
+ var n = decodeTop(buf, code, what, ctx);
940
965
  if (n.tagClass !== "universal" || n.tagNumber !== _T.SEQUENCE || !n.children) {
941
966
  throw ns.E(code, what + " must be a SEQUENCE (RFC 5280 sec. 4.2.1)");
942
967
  }
@@ -1576,6 +1601,8 @@ module.exports = {
1576
1601
  isCborResourceRefusal: isCborResourceRefusal,
1577
1602
  cborParseOpts: cborParseOpts,
1578
1603
  parseOpts: parseOpts,
1604
+ decodeNested: decodeNested,
1605
+ nestedContext: nestedContext,
1579
1606
  makeNS: makeNS,
1580
1607
  versionReader: versionReader,
1581
1608
  DN_SHORT: DN_SHORT,
package/lib/schema-tsp.js CHANGED
@@ -204,7 +204,7 @@ var TIME_STAMP_RESP = schema.seq([
204
204
  schema.optional("timeStampToken", schema.any(), { whenUniversal: [TAGS.SEQUENCE] }),
205
205
  ], {
206
206
  assert: "sequence", arity: { min: 1 }, code: "tsp/bad-response", what: "TimeStampResp",
207
- build: function (m) {
207
+ build: function (m, ctx) {
208
208
  var status = m.fields.status.value.result;
209
209
  var present = m.fields.timeStampToken.present;
210
210
  var granted = status.status === 0 || status.status === 1;
@@ -215,7 +215,9 @@ var TIME_STAMP_RESP = schema.seq([
215
215
  status: status.status,
216
216
  statusString: status.statusString,
217
217
  failInfo: status.failInfo,
218
- timeStampToken: present ? parseToken(m.fields.timeStampToken.value.bytes) : null,
218
+ /** @internal The token is a second parse of bytes carried inside this one, so it takes the
219
+ * caps this door was given rather than the built-in ceilings. */
220
+ timeStampToken: present ? parseToken(m.fields.timeStampToken.value.bytes, ctx ? ctx.caps : undefined) : null,
219
221
  };
220
222
  },
221
223
  });
@@ -424,12 +426,23 @@ function parseToken(input, callerOpts) {
424
426
  if (!hasSigningCert) {
425
427
  throw new TspError("tsp/missing-signing-certificate", "a TimeStampToken signerInfo must carry a SigningCertificate or SigningCertificateV2 signed attribute (RFC 3161 sec. 2.4.2, RFC 5816)");
426
428
  }
429
+ /** @internal The TSTInfo is decoded out of the eContent the CMS layer handed back, so it is a
430
+ * second decode and takes the caller's caps the way the carrier did. The latch is re-read
431
+ * below, because the catch here answers in this domain and would otherwise report a cap that
432
+ * was too small as a malformed TSTInfo. */
433
+ var nested = pkix.nestedContext(NS, caps);
427
434
  var tstInfo;
428
- try { tstInfo = schema.embeddedDer(TST_INFO, encap.eContent, NS, { code: "tsp/bad-der", what: "the encapsulated TSTInfo" }); }
435
+ try { tstInfo = schema.embeddedDer(TST_INFO, encap.eContent, nested.ns, { code: "tsp/bad-der", what: "the encapsulated TSTInfo" }); }
429
436
  catch (e) {
437
+ if (nested.budget.exhausted()) {
438
+ throw new TspError("tsp/too-large", "the timeStampToken exceeds the decode budget for this parse", e);
439
+ }
430
440
  if (e instanceof TspError) throw e;
431
441
  throw new TspError("tsp/bad-der", "the encapsulated TSTInfo did not decode: " + ((e && e.message) || String(e)), e);
432
442
  }
443
+ if (nested.budget.exhausted()) {
444
+ throw new TspError("tsp/too-large", "the timeStampToken exceeds the decode budget for this parse");
445
+ }
433
446
  return { tstInfo: tstInfo.result, eContent: encap.eContent, signerInfo: signed.signerInfos[0], certificates: signed.certificates };
434
447
  }
435
448
 
@@ -6,6 +6,8 @@
6
6
  var asn1 = require("./asn1-der");
7
7
  var oid = require("./oid");
8
8
  var pkcs8 = require("./schema-pkcs8");
9
+ var pkix = require("./schema-pkix");
10
+ var frameworkError = require("./framework-error");
9
11
  var webcrypto = require("./webcrypto");
10
12
  var subtle = webcrypto.webcrypto.subtle;
11
13
  var validator = require("./validator-all");
@@ -23,6 +25,7 @@ var _toUpperCase = intrinsic.toUpperCase;
23
25
  var _isBuffer = intrinsic.isBuffer;
24
26
  var _bufferFrom = intrinsic.bufferFrom;
25
27
  var _isU8 = intrinsic.types.isUint8Array;
28
+ var _resolve = intrinsic.uncurry(intrinsic.promiseResolve);
26
29
 
27
30
  var HASH = intrinsic.assign(intrinsic.create(null), { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" });
28
31
  var NODE_DIGEST = intrinsic.assign(intrinsic.create(null), { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake128: "shake128", shake256: "shake256" });
@@ -98,7 +101,17 @@ function pssSpkiPinnedHash(cert, E) {
98
101
  return HASH[d];
99
102
  }
100
103
 
104
+ /** @internal The SPKI the signature will be verified against travels on the scheme, because the
105
+ * signer form declares a public key and the door has to hold it to the one the certificate names.
106
+ * Attached here rather than on each return, so a new algorithm branch cannot omit it. */
101
107
  function resolveSignScheme(cert, so, noSignedAttrs, E) {
108
+ var scheme = _resolveSignScheme(cert, so, noSignedAttrs, E);
109
+ var spki = cert && cert.subjectPublicKeyInfo;
110
+ scheme.spki = (spki && spki.bytes) || null;
111
+ return scheme;
112
+ }
113
+
114
+ function _resolveSignScheme(cert, so, noSignedAttrs, E) {
102
115
  so = so || {};
103
116
  var alg = cert.subjectPublicKeyInfo.algorithm;
104
117
  var keyOid = alg.oid;
@@ -154,15 +167,16 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
154
167
  throw E("unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
155
168
  }
156
169
 
157
- function _assertKeyMatchesScheme(key, imp, E) {
170
+ function _assertKeyMatchesScheme(key, imp, E, what) {
158
171
  var ka = key.algorithm || {};
172
+ var subject = what || "CryptoKey";
159
173
  if (_toUpperCase(_String(ka.name)) !== _toUpperCase(_String(imp.name))) {
160
174
  var hint = (_toUpperCase(_String(ka.name)) === "RSA-PSS" && _toUpperCase(_String(imp.name)) === "RSASSA-PKCS1-V1_5")
161
- ? "; this CryptoKey is bound to RSASSA-PSS -- pass opts.pss to sign with RSASSA-PSS" : "";
162
- throw E("bad-input", "the signer CryptoKey algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")" + hint);
175
+ ? "; this " + subject + " is bound to RSASSA-PSS -- pass opts.pss to sign with RSASSA-PSS" : "";
176
+ throw E("bad-input", "the signer " + subject + " algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")" + hint);
163
177
  }
164
- if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw E("bad-input", "the signer CryptoKey hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
165
- if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
178
+ if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw E("bad-input", "the signer " + subject + " hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
179
+ if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer " + subject + " curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
166
180
  }
167
181
  /** @internal `owned`, when given, collects the copies this makes of the caller's key so the caller can
168
182
  * wipe them: a Buffer is viewed and stays the caller's, while a Uint8Array or a PEM string produces a
@@ -218,7 +232,105 @@ function _importKey(key, imp, E) {
218
232
  });
219
233
  }
220
234
 
235
+ /** @internal The signer form, read ONCE. A caller's object is not asked the same question twice:
236
+ * an accessor that answers differently on a second read would let the algorithm the door checked
237
+ * differ from the one that signs, and the callback the door validated differ from the one it
238
+ * calls. `sign` doubles as the discriminator, so the read that classifies is the read that runs. */
239
+ function _snapshotSigner(key) {
240
+ if (!key || typeof key !== "object" || _isBuffer(key) || _isU8(key)) return null; // allow:byte-source-narrow -- signer-form discrimination: a raw-key BufferSource is correctly classed as not a signer and normalized by _importKey
241
+ var sign = key.sign;
242
+ if (typeof sign !== "function") return null;
243
+ var algorithm = key.algorithm;
244
+ if (typeof algorithm === "string") algorithm = { name: algorithm };
245
+ var flat = null;
246
+ if (algorithm != null && typeof algorithm === "object") {
247
+ var hash = algorithm.hash;
248
+ var hashName = typeof hash === "string" ? hash : (hash == null ? undefined : hash.name);
249
+ flat = {
250
+ name: algorithm.name,
251
+ hash: hashName === undefined ? undefined : { name: hashName },
252
+ namedCurve: algorithm.namedCurve,
253
+ };
254
+ }
255
+ /** @internal `receiver` is the object this door was handed, so `sign` is called on it and plain
256
+ * configuration read off `this` answers. A signing verb snapshots its options, so a client or a
257
+ * counter a signer holds on itself is a copy by the time it arrives; a callback keeps those in
258
+ * its closure instead, which is where the documented examples put them. */
259
+ return { sign: sign, receiver: key, algorithm: flat, publicKey: key.publicKey };
260
+ }
261
+
262
+ /** @internal An SPKI a signer declares, from the shapes a public key arrives in. */
263
+ function _signerSpki(publicKey, E) {
264
+ if (typeof publicKey === "string") {
265
+ try { return pkix.pemDecode(publicKey, "PUBLIC KEY", frameworkError.PemError); }
266
+ catch (e) { throw E("bad-input", "the signer publicKey PEM could not be decoded", e); }
267
+ }
268
+ return guard.bytes.snapshotSource(publicKey, E, "bad-input", "the signer publicKey");
269
+ }
270
+
271
+ /** @internal The signature a signer returned, held to the contract the callback is documented
272
+ * against: the bytes `subtle.sign` would return for this scheme. An ECDSA signature is the
273
+ * fixed-width r||s of RFC 9053 sec. 2.1, and the DER SEQUENCE a token usually emits is named
274
+ * rather than re-derived, because guessing which encoding arrived is how one gets converted twice. */
275
+ function _assertSignerSignature(sig, scheme, E) {
276
+ if (!scheme.ecdsaDer) return sig;
277
+ var want = scheme.coordLen * 2;
278
+ if (sig.length === want) return sig;
279
+ if (sig.length > 1 && sig[0] === 0x30) {
280
+ throw E("bad-input", "the signer returned a DER SEQUENCE for an ECDSA signature; sign() returns the " +
281
+ want + "-byte r||s a WebCrypto signature carries (RFC 9053 sec. 2.1), and this toolkit encodes it");
282
+ }
283
+ throw E("bad-input", "the signer returned " + sig.length + " byte(s) for an ECDSA signature on this curve; " +
284
+ "sign() returns the " + want + "-byte r||s a WebCrypto signature carries (RFC 9053 sec. 2.1)");
285
+ }
286
+
287
+ function _signExternally(signer, scheme, signedBytes, E) {
288
+ if (scheme.composite) {
289
+ throw E("bad-input", "a composite " + scheme.composite.name + " signature is produced from both component private keys, " +
290
+ "which one sign() callback cannot do; pass { mldsa, trad } PKCS#8 keys");
291
+ }
292
+ if (signer.algorithm == null) throw E("bad-input", "a signer must declare the algorithm it signs with: { algorithm, publicKey, sign }");
293
+ if (signer.publicKey == null) throw E("bad-input", "a signer must declare its publicKey as SPKI DER, so the door can hold it to the key the signature is verified against");
294
+ _assertKeyMatchesScheme(signer, scheme.imp, E, "signer");
295
+ /** @internal The digest a scheme signs under is not always part of the key's import parameters:
296
+ * an ECDSA key names only its curve, and the digest is chosen per signature. A signer declares
297
+ * what it will do, so it is held to that here rather than after a remote signing request. */
298
+ var wantHash = scheme.sign && scheme.sign.hash;
299
+ if (wantHash && signer.algorithm.hash && signer.algorithm.hash.name !== wantHash) {
300
+ throw E("bad-input", "the signer algorithm hash (" + signer.algorithm.hash.name +
301
+ ") does not match the signing digest (" + wantHash + ")");
302
+ }
303
+ var declared = _signerSpki(signer.publicKey, E);
304
+ if (!scheme.spki) throw E("bad-input", "this signing path does not name the public key a signature is verified against, so a signer's publicKey cannot be held to it");
305
+ var named = guard.bytes.snapshotSource(scheme.spki, E, "bad-input", "the public key this signature is verified against");
306
+ if (!guard.crypto.constantTimeEqual(declared, named)) {
307
+ throw E("bad-input", "the signer publicKey is not the key this signature is verified against");
308
+ }
309
+ /** @internal The callback is handed a COPY, in an allocation of its own. It is a caller's
310
+ * function over the bytes an artifact is built from, and a callback that rewrote them in place
311
+ * would rewrite what is verified and what is emitted together, so the self-check would pass on
312
+ * the rewritten artifact. `Buffer.from` is not enough: a small copy comes out of Node's shared
313
+ * pool, whose backing ArrayBuffer the callback could reach the original through. */
314
+ var handed = new intrinsic.Uint8Array(signedBytes.length);
315
+ handed.set(signedBytes);
316
+ var produced;
317
+ try { produced = intrinsic.apply(signer.sign, signer.receiver, [handed]); }
318
+ catch (e) { throw E("bad-input", "the signer sign() callback failed", e); }
319
+ return _resolve(Promise, produced).then(function (raw) {
320
+ var sig = guard.bytes.snapshotSource(raw, E, "bad-input", "the signature the signer returned");
321
+ return _assertSignerSignature(sig, scheme, E);
322
+ }, function (e) {
323
+ throw E("bad-input", "the signer sign() callback failed", e);
324
+ });
325
+ }
326
+
221
327
  function signOverTbs(scheme, key, signedBytes, E) {
328
+ var signer = _snapshotSigner(key);
329
+ if (signer) {
330
+ return _signExternally(signer, scheme, signedBytes, E).then(function (sig) {
331
+ return scheme.ecdsaDer ? validator.sig.rawToEcdsaDer(sig, scheme.coordLen) : sig;
332
+ });
333
+ }
222
334
  if (scheme.composite) {
223
335
  return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(key, scheme.composite, E), signedBytes).then(function (sig) { return _bufferFrom(sig); });
224
336
  }
package/lib/x509-sign.js CHANGED
@@ -366,8 +366,13 @@ function _hasCriticalSan(extSpec) {
366
366
  * `extensions` object. `issuer` is the signing side: `{ key }` alone issues a self-signed certificate
367
367
  * (issuer = subject, signed with the subject's own key); `{ name, publicKey, key }` or `{ cert, key }`
368
368
  * issues a CA-signed one. The signing key `key` is a WebCrypto `CryptoKey` (a `pki.key.generate`
369
- * private key passed directly, without exporting) or a PKCS#8 private key as DER (`Buffer`) or PEM
370
- * (`string`); a public or secret CryptoKey, or a `node:crypto` KeyObject, is refused. A `subjectAltName`
369
+ * private key passed directly, without exporting), a PKCS#8 private key as DER (`Buffer`) or PEM
370
+ * (`string`), or a signer `{ algorithm, publicKey, sign }` for a key this process cannot export,
371
+ * such as one in a hardware security module, a cloud key management service, a PKCS#11 token or a
372
+ * PIV slot. `sign(bytes)` returns the bytes WebCrypto would return for that algorithm, which for
373
+ * ECDSA is the fixed-width `r || s` of RFC 9053 sec. 2.1, and holds its client in its own closure,
374
+ * since this verb snapshots its options. A public or secret CryptoKey,
375
+ * or a `node:crypto` KeyObject, is refused. A `subjectAltName`
371
376
  * (or any `GeneralName`) entry may be a form object (`{ dNSName: "..." }`) or a bare string classified
372
377
  * fail-closed into its form (`"host.example"`, `"a@b.example"`, `"10.0.0.1"`, `"https://host.example/"`).
373
378
  * The signature algorithm is resolved from the signing key: RSA (PKCS#1 v1.5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
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:9e704e55-ccb2-4ecb-9370-04d6f58aa807",
5
+ "serialNumber": "urn:uuid:67dbc768-79f1-44d5-921c-53ce9accac3e",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-20T04:03:01.349Z",
8
+ "timestamp": "2026-09-20T07:17:13.483Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.8.3",
22
+ "bom-ref": "@blamejs/pki@0.8.5",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.3",
25
+ "version": "0.8.5",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.8.3",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.8.5",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.8.3",
57
+ "ref": "@blamejs/pki@0.8.5",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]