@blamejs/pki 0.8.4 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ 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.8.5 — 2026-09-25
8
+
9
+ A key this process cannot export can sign, and every verb that signs proves its signature first.
10
+
11
+ ### Added
12
+
13
+ - A signer `{ algorithm, publicKey, sign }` is accepted wherever a signing verb takes a private key: `pki.x509.sign`, `pki.crl.sign`, `pki.csr.sign`, `pki.cms.sign` and its countersignatures, `pki.attrcert.sign`, `pki.cmp.build`, `pki.crmf.build`, `pki.ocsp.buildRequest`, `pki.ocsp.sign` and `pki.tsp.sign`. `sign(bytes)` is handed the bytes WebCrypto would be handed and returns the bytes WebCrypto would return, or a promise of them; for ECDSA that is the fixed-width `r || s` of RFC 9053 sec. 2.1, and a DER `SEQUENCE` is refused by name rather than encoded a second time. The private half never reaches the toolkit.
14
+ - A signer's declared `algorithm` is held to the algorithm of the key the artifact certifies, and its declared `publicKey` to the key the signature is verified against, both before the callback is asked to sign. The caller's object is read once, in the synchronous prologue, so an accessor cannot answer one way to the check and another to the use. For an ECDSA key, whose curve is named but whose digest is chosen per signature, the digest the signer declares is checked on its own, before any remote signing request. The bytes the callback is handed are a copy in an allocation of its own, so a callback that rewrites them rewrites only its own copy and cannot reach the bytes the artifact carries. The bytes it returns are copied before use. Hold a client or any mutable state in the callback's closure: a signing verb snapshots its options, so the signer object the callback sees is a copy of the one you passed. A composite ML-DSA signature is refused by name, since it is produced from both component private keys and one callback cannot do that.
15
+
16
+ ### Fixed
17
+
18
+ - `pki.ocsp.sign`, `pki.ocsp.buildRequest` and `pki.cms.sign`'s countersignature path verify the signature they produced against the public key the artifact declares, which the other signing verbs already did. A signer holding a key that does not match the certificate it signs under produced an OCSP response, an OCSP request or a countersignature that no relying party could validate, and the emitting side reported success.
19
+
7
20
  ## v0.8.4 — 2026-09-25
8
21
 
9
22
  A decode cap bounds every structure a parse reads, and a refusal caused by a cap is no longer reported as malformed input.
package/README.md CHANGED
@@ -190,6 +190,33 @@ var ok = await subtle.verify({ name: "ML-DSA-65" }, kp.publicKey, sig, data); /
190
190
  // same call shape, and every key it exports is OpenSSL/NSS-interoperable.
191
191
  ```
192
192
 
193
+ ### Sign with a key this process cannot export
194
+
195
+ Every signing verb takes a signer object wherever it takes a private key, for a
196
+ key held in a hardware security module, a cloud key management service, a PKCS#11
197
+ token, a PIV slot or a Trusted Platform Module. The private half never reaches
198
+ the toolkit.
199
+
200
+ ```js
201
+ var signer = {
202
+ algorithm: { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },
203
+ publicKey: spkiDer, // the SPKI DER of the public half
204
+ sign: function (bytes) { return kms.sign(keyId, bytes); }, // bytes or a promise of them
205
+ };
206
+
207
+ var cert = await pki.x509.sign(spec, { key: signer, cert: caCert });
208
+ ```
209
+
210
+ `sign(bytes)` is handed the bytes WebCrypto would be handed and returns the bytes
211
+ WebCrypto would return: for ECDSA the fixed-width `r || s` of RFC 9053 §2.1, not a
212
+ DER `SEQUENCE`. `algorithm` is held to the certificate's key algorithm, `publicKey`
213
+ is held to the key the signature is verified against, and the signature is
214
+ verified before the artifact is returned. The bytes handed to the callback are a
215
+ copy, so rewriting them rewrites nothing the artifact carries. Hold the client and
216
+ any mutable state in the callback's closure, as the example does: a signing verb
217
+ snapshots its options, so the signer object the callback sees is a copy of the one
218
+ you passed.
219
+
193
220
  ## What ships today
194
221
 
195
222
  Everything below is callable now; nothing is a stub. The whole documented
package/lib/cms-sign.js CHANGED
@@ -214,7 +214,10 @@ function _keyOnlyCertStandIn(spkiDer) {
214
214
  if (e && typeof e.code === "string" && e.code.indexOf("cms/") === 0) throw e;
215
215
  throw _err("cms/bad-input", "a key-only signer's spki is not a SubjectPublicKeyInfo", e);
216
216
  }
217
- return { subjectPublicKeyInfo: { algorithm: alg } };
217
+ /** @internal The SPKI bytes travel with the stand-in, because the key this signature is verified
218
+ * against is what a signer's declared publicKey is held to, and this form names that key by SPKI
219
+ * rather than by certificate. */
220
+ return { subjectPublicKeyInfo: { bytes: spkiDer, algorithm: alg } };
218
221
  }
219
222
 
220
223
  function _keyOnlyKeyId(so) {
@@ -432,11 +435,18 @@ function _buildCountersignature(targetSigOctets, countersigner, opts) {
432
435
  return _buildSignedAttrs(pairs.concat(extra));
433
436
  });
434
437
  }).then(function (attrs) {
435
- return signScheme.signOverTbs(scheme, soKey, attrs ? attrs.setOf : targetSigOctets, _signE).then(function (sig) {
436
- var fields = [b.integer(BigInt(sidv.version)), sidv.sid, scheme.digestAlgId];
437
- if (attrs) fields.push(attrs.wire);
438
- fields.push(scheme.sigAlgId, b.octetString(sig));
439
- return { value: b.sequence(fields), certDer: certDer };
438
+ var preimage = attrs ? attrs.setOf : targetSigOctets;
439
+ return signScheme.signOverTbs(scheme, soKey, preimage, _signE).then(function (sig) {
440
+ /** @internal The countersignature is held to the same proof the signature is: it verifies
441
+ * against the public key the countersigner's certificate declares. */
442
+ return guard.async.deferred(function () {
443
+ return _b.assertSignatureVerifies(preimage, sig, cert.subjectPublicKeyInfo.bytes, scheme);
444
+ }).then(function () {
445
+ var fields = [b.integer(BigInt(sidv.version)), sidv.sid, scheme.digestAlgId];
446
+ if (attrs) fields.push(attrs.wire);
447
+ fields.push(scheme.sigAlgId, b.octetString(sig));
448
+ return { value: b.sequence(fields), certDer: certDer };
449
+ });
440
450
  });
441
451
  });
442
452
  }
package/lib/cms-verify.js CHANGED
@@ -721,7 +721,8 @@ function _addCert(out, der) {
721
721
  * S/MIME signed mail, RFC 3161 timestamp tokens, and code signing rest on, and exactly what
722
722
  * `pki.cms.verify` consumes and OpenSSL `cms -verify` validates. Each `signers[i]` is
723
723
  * `{ cert, key, digestAlgorithm?, pss? }`: `cert` the signer certificate (PEM or DER), `key`
724
- * its private key (a WebCrypto `CryptoKey` or a PKCS#8 DER `Buffer` / PEM string; for a composite
724
+ * its private key (a WebCrypto `CryptoKey`, a PKCS#8 DER `Buffer` / PEM string, or a signer
725
+ * `{ algorithm, publicKey, sign }` for a key held outside this process; for a composite
725
726
  * ML-DSA signer, the two component keys `{ mldsa, trad }`, each PKCS#8). The
726
727
  * signature covers the RFC 5652 sec. 5.4 preimage: with signed attributes (the default) the
727
728
  * message-digest attribute is bound to the content digest and the signature is over the
package/lib/csr-sign.js CHANGED
@@ -77,7 +77,8 @@ function _challengePassword(pw) {
77
77
  * `challengePassword`. An extension the issuing CA assigns (authorityKeyIdentifier, the
78
78
  * certificate-transparency pair, the CA version and previous-certificate hash, ocspNoCheck) is not a
79
79
  * request and is refused by name. `key` (or
80
- * `{ key }`) is the subject's own PKCS#8 private key / WebCrypto CryptoKey, so the request is self-signed
80
+ * `{ key }`) is the subject's own PKCS#8 private key, WebCrypto CryptoKey, or signer
81
+ * `{ algorithm, publicKey, sign }`, so the request is self-signed
81
82
  * to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the
82
83
  * request is returned. The signature algorithm is resolved from the subject key (RSA PKCS#1 v1.5 or PSS,
83
84
  * ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm). Returns DER, or a PEM `CERTIFICATE REQUEST` with
package/lib/ocsp.js CHANGED
@@ -45,6 +45,9 @@ var b = asn1.build;
45
45
  function O(name) { return oid.byName(name); }
46
46
  function _err(code, message, cause) { return new OcspError(code, message, cause); }
47
47
  function _signE(kind, message, cause) { return new OcspError("ocsp/" + kind, message, cause); }
48
+ /** @internal Only the signature self-check is taken from the shared builder here; the name and
49
+ * SPKI schemas it also serves belong to the certificate verbs, and this module reads neither. */
50
+ var _b = pkiBuild.makeBuilder({ ErrorClass: OcspError, prefix: "ocsp", O: O, NS: null, NAME_SCHEMA: null, SPKI_SCHEMA: null, EXT_DECODERS: {} });
48
51
 
49
52
  var HASH_WC = Object.assign(Object.create(null), { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" });
50
53
  var REASON_CODE = Object.create(null);
@@ -214,8 +217,12 @@ function _buildRequest(query, opts) {
214
217
  if (signerCert === null) return _emitReq(b.sequence([tbsRequest]), opts);
215
218
  var scheme = signScheme.resolveSignScheme(signerCert, { combinedRsaSig: true }, true, _signE);
216
219
  return signScheme.signOverTbs(scheme, opts.signer.key, tbsRequest, _signE).then(function (sig) {
217
- var optionalSignature = b.sequence([scheme.sigAlgId, b.bitString(sig, 0), b.explicit(0, b.sequence([b.raw(signerCertDer)]))]);
218
- return _emitReq(b.sequence([tbsRequest, b.explicit(0, optionalSignature)]), opts);
220
+ /** @internal The request is held to the same proof a certificate is: the signature verifies
221
+ * against the public key the certificate it carries declares. */
222
+ return Promise.resolve(_b.assertSignatureVerifies(tbsRequest, sig, signerCert.subjectPublicKeyInfo.bytes, scheme)).then(function () {
223
+ var optionalSignature = b.sequence([scheme.sigAlgId, b.bitString(sig, 0), b.explicit(0, b.sequence([b.raw(signerCertDer)]))]);
224
+ return _emitReq(b.sequence([tbsRequest, b.explicit(0, optionalSignature)]), opts);
225
+ });
219
226
  });
220
227
  });
221
228
  }
@@ -349,11 +356,15 @@ function _signResponse(responseData, responder, respCert, respCertDer, responder
349
356
  var responseDataDer = b.sequence(rdChildren);
350
357
  var scheme = signScheme.resolveSignScheme(respCert, { combinedRsaSig: true }, true, _signE);
351
358
  return signScheme.signOverTbs(scheme, responderKey, responseDataDer, _signE).then(function (sig) {
352
- var basicChildren = [responseDataDer, scheme.sigAlgId, b.bitString(sig, 0)];
353
- if (opts.embedCert !== false) basicChildren.push(b.explicit(0, b.sequence([b.raw(respCertDer)])));
354
- var responseBytes = b.sequence([b.oid(OID_OCSP_BASIC), b.octetString(b.sequence(basicChildren))]);
355
- var der = b.sequence([b.enumerated(0n), b.explicit(0, responseBytes)]);
356
- return opts.pem ? ocspSchema.pemEncode(der, "OCSP RESPONSE") : der;
359
+ /** @internal The response is held to the same proof a certificate is: the signature verifies
360
+ * against the public key the responder certificate declares. */
361
+ return Promise.resolve(_b.assertSignatureVerifies(responseDataDer, sig, respCert.subjectPublicKeyInfo.bytes, scheme)).then(function () {
362
+ var basicChildren = [responseDataDer, scheme.sigAlgId, b.bitString(sig, 0)];
363
+ if (opts.embedCert !== false) basicChildren.push(b.explicit(0, b.sequence([b.raw(respCertDer)])));
364
+ var responseBytes = b.sequence([b.oid(OID_OCSP_BASIC), b.octetString(b.sequence(basicChildren))]);
365
+ var der = b.sequence([b.enumerated(0n), b.explicit(0, responseBytes)]);
366
+ return opts.pem ? ocspSchema.pemEncode(der, "OCSP RESPONSE") : der;
367
+ });
357
368
  });
358
369
  });
359
370
  });
package/lib/pki-build.js CHANGED
@@ -1290,7 +1290,7 @@ function makeBuilder(ctx) {
1290
1290
  var keyOid;
1291
1291
  try { keyOid = asn1.read.oid(alg.children[0]); }
1292
1292
  catch (e) { throw E("bad-input", "the signing key SPKI algorithm is not an OID", e); }
1293
- return { subjectPublicKeyInfo: { algorithm: { oid: keyOid, parameters: alg.children.length > 1 ? alg.children[1].bytes : undefined } } };
1293
+ return { subjectPublicKeyInfo: { bytes: spkiDer, algorithm: { oid: keyOid, parameters: alg.children.length > 1 ? alg.children[1].bytes : undefined } } };
1294
1294
  }
1295
1295
 
1296
1296
  function assertSignatureVerifies(preimage, sig, spki, scheme) {
@@ -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.4",
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:ca3c695a-64b2-4346-9529-6a6c719eb6bb",
5
+ "serialNumber": "urn:uuid:67dbc768-79f1-44d5-921c-53ce9accac3e",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-20T06:26:19.862Z",
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.4",
22
+ "bom-ref": "@blamejs/pki@0.8.5",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.4",
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.4",
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.4",
57
+ "ref": "@blamejs/pki@0.8.5",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]