@blamejs/pki 0.8.4 → 0.8.6

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,32 @@ 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.6 — 2026-09-25
8
+
9
+ A node:crypto KeyObject signs, without being asked to export its private half.
10
+
11
+ ### Added
12
+
13
+ - A `node:crypto` `KeyObject` is accepted wherever a signing verb takes a private key, and signs through `node:crypto` rather than being exported to PKCS#8 first. RSA with PKCS#1 v1.5 and with PSS padding, ECDSA on P-256, P-384 and P-521, Ed25519, Ed448, ML-DSA and SLH-DSA all sign this way, and the bytes produced are the bytes WebCrypto would have produced: an ECDSA signature is taken in the fixed-width form of RFC 9053 sec. 2.1 and encoded by the toolkit, so the artifact is byte-identical to one signed by the equivalent `CryptoKey`.
14
+ - A `KeyObject` is held to the key the artifact names exactly as a `CryptoKey` is. Its algorithm must match the certificate's key algorithm and, for an EC key, its curve must match the certificate's curve; a public or a secret key is refused by type; and the signature is verified against the declared key before the artifact is returned, so a `KeyObject` for a different key of the same algorithm is refused rather than producing something no relying party can validate.
15
+
16
+ ### Changed
17
+
18
+ - `pki.x509.sign` and the other signing verbs no longer refuse a `node:crypto` `KeyObject` with the message naming it as not a `CryptoKey`. A caller matching on that message to detect the refusal will stop seeing it, because the key now signs.
19
+
20
+ ## v0.8.5 — 2026-09-25
21
+
22
+ A key this process cannot export can sign, and every verb that signs proves its signature first.
23
+
24
+ ### Added
25
+
26
+ - 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.
27
+ - 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.
28
+
29
+ ### Fixed
30
+
31
+ - `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.
32
+
7
33
  ## v0.8.4 — 2026-09-25
8
34
 
9
35
  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,41 @@ 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 `node:crypto` `KeyObject` wherever it takes a private
196
+ key, and signs through `node:crypto`, so a key an OpenSSL engine or provider
197
+ refuses to export still signs.
198
+
199
+ ```js
200
+ var keyObject = crypto.createPrivateKey({ key: pem });
201
+ var cert = await pki.x509.sign(spec, { key: keyObject, cert: caCert });
202
+ ```
203
+
204
+ For a key that is not in this process at all, held in a hardware security module,
205
+ a cloud key management service, a PKCS#11 token, a PIV slot or a Trusted Platform
206
+ Module, every signing verb takes a signer object in the same place.
207
+
208
+ ```js
209
+ var signer = {
210
+ algorithm: { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },
211
+ publicKey: spkiDer, // the SPKI DER of the public half
212
+ sign: function (bytes) { return kms.sign(keyId, bytes); }, // bytes or a promise of them
213
+ };
214
+
215
+ var cert = await pki.x509.sign(spec, { key: signer, cert: caCert });
216
+ ```
217
+
218
+ `sign(bytes)` is handed the bytes WebCrypto would be handed and returns the bytes
219
+ WebCrypto would return: for ECDSA the fixed-width `r || s` of RFC 9053 §2.1, not a
220
+ DER `SEQUENCE`. `algorithm` is held to the certificate's key algorithm, `publicKey`
221
+ is held to the key the signature is verified against, and the signature is
222
+ verified before the artifact is returned. The bytes handed to the callback are a
223
+ copy, so rewriting them rewrites nothing the artifact carries. Hold the client and
224
+ any mutable state in the callback's closure, as the example does: a signing verb
225
+ snapshots its options, so the signer object the callback sees is a copy of the one
226
+ you passed.
227
+
193
228
  ## What ships today
194
229
 
195
230
  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) {
@@ -5,7 +5,10 @@
5
5
 
6
6
  var asn1 = require("./asn1-der");
7
7
  var oid = require("./oid");
8
+ var nodeCrypto = require("crypto");
8
9
  var pkcs8 = require("./schema-pkcs8");
10
+ var pkix = require("./schema-pkix");
11
+ var frameworkError = require("./framework-error");
9
12
  var webcrypto = require("./webcrypto");
10
13
  var subtle = webcrypto.webcrypto.subtle;
11
14
  var validator = require("./validator-all");
@@ -20,9 +23,11 @@ var _String = intrinsic.String;
20
23
  var _filter = intrinsic.filter;
21
24
  var _stringify = intrinsic.stringify;
22
25
  var _toUpperCase = intrinsic.toUpperCase;
26
+ var _toLowerCase = intrinsic.toLowerCase;
23
27
  var _isBuffer = intrinsic.isBuffer;
24
28
  var _bufferFrom = intrinsic.bufferFrom;
25
29
  var _isU8 = intrinsic.types.isUint8Array;
30
+ var _resolve = intrinsic.uncurry(intrinsic.promiseResolve);
26
31
 
27
32
  var HASH = intrinsic.assign(intrinsic.create(null), { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" });
28
33
  var NODE_DIGEST = intrinsic.assign(intrinsic.create(null), { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake128: "shake128", shake256: "shake256" });
@@ -98,7 +103,17 @@ function pssSpkiPinnedHash(cert, E) {
98
103
  return HASH[d];
99
104
  }
100
105
 
106
+ /** @internal The SPKI the signature will be verified against travels on the scheme, because the
107
+ * signer form declares a public key and the door has to hold it to the one the certificate names.
108
+ * Attached here rather than on each return, so a new algorithm branch cannot omit it. */
101
109
  function resolveSignScheme(cert, so, noSignedAttrs, E) {
110
+ var scheme = _resolveSignScheme(cert, so, noSignedAttrs, E);
111
+ var spki = cert && cert.subjectPublicKeyInfo;
112
+ scheme.spki = (spki && spki.bytes) || null;
113
+ return scheme;
114
+ }
115
+
116
+ function _resolveSignScheme(cert, so, noSignedAttrs, E) {
102
117
  so = so || {};
103
118
  var alg = cert.subjectPublicKeyInfo.algorithm;
104
119
  var keyOid = alg.oid;
@@ -154,15 +169,16 @@ function resolveSignScheme(cert, so, noSignedAttrs, E) {
154
169
  throw E("unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
155
170
  }
156
171
 
157
- function _assertKeyMatchesScheme(key, imp, E) {
172
+ function _assertKeyMatchesScheme(key, imp, E, what) {
158
173
  var ka = key.algorithm || {};
174
+ var subject = what || "CryptoKey";
159
175
  if (_toUpperCase(_String(ka.name)) !== _toUpperCase(_String(imp.name))) {
160
176
  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);
177
+ ? "; this " + subject + " is bound to RSASSA-PSS -- pass opts.pss to sign with RSASSA-PSS" : "";
178
+ throw E("bad-input", "the signer " + subject + " algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")" + hint);
163
179
  }
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 + ")");
180
+ 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 + ")");
181
+ 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
182
  }
167
183
  /** @internal `owned`, when given, collects the copies this makes of the caller's key so the caller can
168
184
  * wipe them: a Buffer is viewed and stays the caller's, while a Uint8Array or a PEM string produces a
@@ -204,9 +220,10 @@ function _importKey(key, imp, E) {
204
220
  try { der = pkcs8.pemDecode(key); }
205
221
  catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
206
222
  owned = true;
207
- } else if (intrinsic.types.isKeyObject(key)) {
208
- throw E("bad-input", "the signing key is a node:crypto KeyObject, not a WebCrypto CryptoKey; pass a CryptoKey (e.g. from pki.key.generate) or export it to a PKCS#8 DER Buffer or PEM string");
209
- } else throw E("bad-input", "a signer key must be a WebCrypto CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
223
+ } else {
224
+ throw E("bad-input", "a signer key must be a WebCrypto CryptoKey, a node:crypto KeyObject, a PKCS#8 DER Buffer, " +
225
+ "a PKCS#8 PEM string, or a signer { algorithm, publicKey, sign }");
226
+ }
210
227
  var imported = subtle.importKey("pkcs8", der, imp, false, ["sign"]);
211
228
  if (!owned) return imported;
212
229
  return imported.then(function (k) {
@@ -218,7 +235,181 @@ function _importKey(key, imp, E) {
218
235
  });
219
236
  }
220
237
 
238
+ /** @internal The signer form, read ONCE. A caller's object is not asked the same question twice:
239
+ * an accessor that answers differently on a second read would let the algorithm the door checked
240
+ * differ from the one that signs, and the callback the door validated differ from the one it
241
+ * calls. `sign` doubles as the discriminator, so the read that classifies is the read that runs. */
242
+ function _snapshotSigner(key) {
243
+ 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
244
+ var sign = key.sign;
245
+ if (typeof sign !== "function") return null;
246
+ var algorithm = key.algorithm;
247
+ if (typeof algorithm === "string") algorithm = { name: algorithm };
248
+ var flat = null;
249
+ if (algorithm != null && typeof algorithm === "object") {
250
+ var hash = algorithm.hash;
251
+ var hashName = typeof hash === "string" ? hash : (hash == null ? undefined : hash.name);
252
+ flat = {
253
+ name: algorithm.name,
254
+ hash: hashName === undefined ? undefined : { name: hashName },
255
+ namedCurve: algorithm.namedCurve,
256
+ };
257
+ }
258
+ /** @internal `receiver` is the object this door was handed, so `sign` is called on it and plain
259
+ * configuration read off `this` answers. A signing verb snapshots its options, so a client or a
260
+ * counter a signer holds on itself is a copy by the time it arrives; a callback keeps those in
261
+ * its closure instead, which is where the documented examples put them. */
262
+ return { sign: sign, receiver: key, algorithm: flat, publicKey: key.publicKey };
263
+ }
264
+
265
+ /** @internal An SPKI a signer declares, from the shapes a public key arrives in. */
266
+ function _signerSpki(publicKey, E) {
267
+ if (typeof publicKey === "string") {
268
+ try { return pkix.pemDecode(publicKey, "PUBLIC KEY", frameworkError.PemError); }
269
+ catch (e) { throw E("bad-input", "the signer publicKey PEM could not be decoded", e); }
270
+ }
271
+ return guard.bytes.snapshotSource(publicKey, E, "bad-input", "the signer publicKey");
272
+ }
273
+
274
+ /** @internal The signature a signer returned, held to the contract the callback is documented
275
+ * against: the bytes `subtle.sign` would return for this scheme. An ECDSA signature is the
276
+ * fixed-width r||s of RFC 9053 sec. 2.1, and the DER SEQUENCE a token usually emits is named
277
+ * rather than re-derived, because guessing which encoding arrived is how one gets converted twice. */
278
+ function _assertSignerSignature(sig, scheme, E) {
279
+ if (!scheme.ecdsaDer) return sig;
280
+ var want = scheme.coordLen * 2;
281
+ if (sig.length === want) return sig;
282
+ if (sig.length > 1 && sig[0] === 0x30) {
283
+ throw E("bad-input", "the signer returned a DER SEQUENCE for an ECDSA signature; sign() returns the " +
284
+ want + "-byte r||s a WebCrypto signature carries (RFC 9053 sec. 2.1), and this toolkit encodes it");
285
+ }
286
+ throw E("bad-input", "the signer returned " + sig.length + " byte(s) for an ECDSA signature on this curve; " +
287
+ "sign() returns the " + want + "-byte r||s a WebCrypto signature carries (RFC 9053 sec. 2.1)");
288
+ }
289
+
290
+ function _signExternally(signer, scheme, signedBytes, E) {
291
+ if (scheme.composite) {
292
+ throw E("bad-input", "a composite " + scheme.composite.name + " signature is produced from both component private keys, " +
293
+ "which one sign() callback cannot do; pass { mldsa, trad } PKCS#8 keys");
294
+ }
295
+ if (signer.algorithm == null) throw E("bad-input", "a signer must declare the algorithm it signs with: { algorithm, publicKey, sign }");
296
+ 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");
297
+ _assertKeyMatchesScheme(signer, scheme.imp, E, "signer");
298
+ /** @internal The digest a scheme signs under is not always part of the key's import parameters:
299
+ * an ECDSA key names only its curve, and the digest is chosen per signature. A signer declares
300
+ * what it will do, so it is held to that here rather than after a remote signing request. */
301
+ var wantHash = scheme.sign && scheme.sign.hash;
302
+ if (wantHash && signer.algorithm.hash && signer.algorithm.hash.name !== wantHash) {
303
+ throw E("bad-input", "the signer algorithm hash (" + signer.algorithm.hash.name +
304
+ ") does not match the signing digest (" + wantHash + ")");
305
+ }
306
+ var declared = _signerSpki(signer.publicKey, E);
307
+ 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");
308
+ var named = guard.bytes.snapshotSource(scheme.spki, E, "bad-input", "the public key this signature is verified against");
309
+ if (!guard.crypto.constantTimeEqual(declared, named)) {
310
+ throw E("bad-input", "the signer publicKey is not the key this signature is verified against");
311
+ }
312
+ /** @internal The callback is handed a COPY, in an allocation of its own. It is a caller's
313
+ * function over the bytes an artifact is built from, and a callback that rewrote them in place
314
+ * would rewrite what is verified and what is emitted together, so the self-check would pass on
315
+ * the rewritten artifact. `Buffer.from` is not enough: a small copy comes out of Node's shared
316
+ * pool, whose backing ArrayBuffer the callback could reach the original through. */
317
+ var handed = new intrinsic.Uint8Array(signedBytes.length);
318
+ handed.set(signedBytes);
319
+ var produced;
320
+ try { produced = intrinsic.apply(signer.sign, signer.receiver, [handed]); }
321
+ catch (e) { throw E("bad-input", "the signer sign() callback failed", e); }
322
+ return _resolve(Promise, produced).then(function (raw) {
323
+ var sig = guard.bytes.snapshotSource(raw, E, "bad-input", "the signature the signer returned");
324
+ return _assertSignerSignature(sig, scheme, E);
325
+ }, function (e) {
326
+ throw E("bad-input", "the signer sign() callback failed", e);
327
+ });
328
+ }
329
+
330
+ /** @internal What `node:crypto` calls the key an algorithm the toolkit signs with uses, keyed by
331
+ * the WebCrypto name the scheme resolved to. The two vocabularies differ, and the map is the one
332
+ * place that knows it, so a new algorithm adds a row rather than a branch. */
333
+ var NODE_KEY_TYPE = intrinsic.assign(intrinsic.create(null), {
334
+ "RSASSA-PKCS1-v1_5": "rsa", "RSA-PSS": "rsa-pss", ECDSA: "ec", Ed25519: "ed25519", Ed448: "ed448",
335
+ });
336
+ /** @internal The curve a KeyObject reports, keyed by the WebCrypto curve a scheme names. */
337
+ var NODE_CURVE = intrinsic.assign(intrinsic.create(null), {
338
+ "P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1",
339
+ });
340
+
341
+ /** @internal An RSA key carries no padding in its type, so an RSA-PSS scheme signs with a plain
342
+ * `rsa` key too; the padding is named on the signature, not on the key. */
343
+ function _keyTypeAccepts(reported, wanted) {
344
+ if (reported === wanted) return true;
345
+ return wanted === "rsa-pss" && reported === "rsa";
346
+ }
347
+
348
+ function _assertKeyObjectMatchesScheme(key, scheme, E) {
349
+ var reported = key.asymmetricKeyType;
350
+ var wanted = NODE_KEY_TYPE[scheme.imp.name] || _toLowerCase(_String(scheme.imp.name));
351
+ if (!_keyTypeAccepts(reported, wanted)) {
352
+ throw E("bad-input", "the signer KeyObject algorithm (" + reported +
353
+ ") does not match the certificate's key algorithm (" + scheme.imp.name + ")");
354
+ }
355
+ if (scheme.imp.namedCurve) {
356
+ var details = key.asymmetricKeyDetails;
357
+ var curve = details && details.namedCurve;
358
+ if (curve !== NODE_CURVE[scheme.imp.namedCurve]) {
359
+ throw E("bad-input", "the signer KeyObject curve (" + curve +
360
+ ") does not match the certificate curve (" + scheme.imp.namedCurve + ")");
361
+ }
362
+ }
363
+ }
364
+
365
+ /** @internal The options `node:crypto` signs a scheme under, so the bytes it returns are the bytes
366
+ * WebCrypto would have returned: an ECDSA signature in the fixed-width form of RFC 9053 sec. 2.1,
367
+ * which the caller then encodes, and a PSS signature at the salt length the scheme fixed. */
368
+ function _nodeSignOptions(key, scheme) {
369
+ var name = scheme.sign.name;
370
+ if (name === "ECDSA") return { key: key, dsaEncoding: "ieee-p1363" };
371
+ if (name === "RSA-PSS") {
372
+ return { key: key, padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: scheme.sign.saltLength };
373
+ }
374
+ return key;
375
+ }
376
+
377
+ /** @internal A digest `node:crypto` applies before signing. An algorithm that hashes the message
378
+ * itself (the Edwards curves, ML-DSA, SLH-DSA) takes none, and naming one there is an error. */
379
+ function _nodeDigest(scheme) {
380
+ var name = scheme.sign.name;
381
+ if (name === "ECDSA" || name === "RSA-PSS" || name === "RSASSA-PKCS1-v1_5") return NODE_DIGEST[scheme.digest] || scheme.digest;
382
+ return null;
383
+ }
384
+
385
+ function _signWithKeyObject(key, scheme, signedBytes, E) {
386
+ if (scheme.composite) {
387
+ throw E("bad-input", "a composite " + scheme.composite.name + " signature is produced from both component private keys, " +
388
+ "which one KeyObject cannot hold; pass { mldsa, trad } PKCS#8 keys");
389
+ }
390
+ if (key.type !== "private") {
391
+ throw E("bad-input", "the signing key is a node:crypto KeyObject of type " + _stringify(key.type) +
392
+ "; a signing key must be a private key (type \"private\")");
393
+ }
394
+ _assertKeyObjectMatchesScheme(key, scheme, E);
395
+ try {
396
+ return _bufferFrom(nodeCrypto.sign(_nodeDigest(scheme), signedBytes, _nodeSignOptions(key, scheme)));
397
+ } catch (e) {
398
+ throw E("bad-input", "the signer KeyObject could not produce a " + scheme.imp.name + " signature", e);
399
+ }
400
+ }
401
+
221
402
  function signOverTbs(scheme, key, signedBytes, E) {
403
+ if (intrinsic.types.isKeyObject(key)) {
404
+ var koSig = _signWithKeyObject(key, scheme, signedBytes, E);
405
+ return _resolve(Promise, scheme.ecdsaDer ? validator.sig.rawToEcdsaDer(koSig, scheme.coordLen) : koSig);
406
+ }
407
+ var signer = _snapshotSigner(key);
408
+ if (signer) {
409
+ return _signExternally(signer, scheme, signedBytes, E).then(function (sig) {
410
+ return scheme.ecdsaDer ? validator.sig.rawToEcdsaDer(sig, scheme.coordLen) : sig;
411
+ });
412
+ }
222
413
  if (scheme.composite) {
223
414
  return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(key, scheme.composite, E), signedBytes).then(function (sig) { return _bufferFrom(sig); });
224
415
  }
package/lib/x509-sign.js CHANGED
@@ -366,8 +366,14 @@ 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 `node:crypto` `KeyObject`, a PKCS#8 private
370
+ * key as DER (`Buffer`) or PEM (`string`), or a signer `{ algorithm, publicKey, sign }` for a key
371
+ * this process cannot export, such as one in a hardware security module, a cloud key management
372
+ * service, a PKCS#11 token or a PIV slot. A `KeyObject` signs through `node:crypto`, so a key that
373
+ * refuses to export its private half still signs. `sign(bytes)` returns the bytes WebCrypto would
374
+ * return for that algorithm, which for ECDSA is the fixed-width `r || s` of RFC 9053 sec. 2.1, and
375
+ * holds its client in its own closure, since this verb snapshots its options. A public or secret
376
+ * key is refused whichever form carries it. A `subjectAltName`
371
377
  * (or any `GeneralName`) entry may be a form object (`{ dNSName: "..." }`) or a bare string classified
372
378
  * fail-closed into its form (`"host.example"`, `"a@b.example"`, `"10.0.0.1"`, `"https://host.example/"`).
373
379
  * 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.6",
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:81aab9c8-7ee6-4344-8f73-8b672cade34f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-20T06:26:19.862Z",
8
+ "timestamp": "2026-09-20T07:41:52.928Z",
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.6",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.4",
25
+ "version": "0.8.6",
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.6",
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.6",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]