@blamejs/pki 0.8.5 → 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,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.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
+
7
20
  ## v0.8.5 — 2026-09-25
8
21
 
9
22
  A key this process cannot export can sign, and every verb that signs proves its signature first.
package/README.md CHANGED
@@ -192,10 +192,18 @@ var ok = await subtle.verify({ name: "ML-DSA-65" }, kp.publicKey, sig, data); /
192
192
 
193
193
  ### Sign with a key this process cannot export
194
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.
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.
199
207
 
200
208
  ```js
201
209
  var signer = {
@@ -5,6 +5,7 @@
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");
9
10
  var pkix = require("./schema-pkix");
10
11
  var frameworkError = require("./framework-error");
@@ -22,6 +23,7 @@ var _String = intrinsic.String;
22
23
  var _filter = intrinsic.filter;
23
24
  var _stringify = intrinsic.stringify;
24
25
  var _toUpperCase = intrinsic.toUpperCase;
26
+ var _toLowerCase = intrinsic.toLowerCase;
25
27
  var _isBuffer = intrinsic.isBuffer;
26
28
  var _bufferFrom = intrinsic.bufferFrom;
27
29
  var _isU8 = intrinsic.types.isUint8Array;
@@ -218,9 +220,10 @@ function _importKey(key, imp, E) {
218
220
  try { der = pkcs8.pemDecode(key); }
219
221
  catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); }
220
222
  owned = true;
221
- } else if (intrinsic.types.isKeyObject(key)) {
222
- 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");
223
- } 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
+ }
224
227
  var imported = subtle.importKey("pkcs8", der, imp, false, ["sign"]);
225
228
  if (!owned) return imported;
226
229
  return imported.then(function (k) {
@@ -324,7 +327,83 @@ function _signExternally(signer, scheme, signedBytes, E) {
324
327
  });
325
328
  }
326
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
+
327
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
+ }
328
407
  var signer = _snapshotSigner(key);
329
408
  if (signer) {
330
409
  return _signExternally(signer, scheme, signedBytes, E).then(function (sig) {
package/lib/x509-sign.js CHANGED
@@ -366,13 +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), 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`
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`
376
377
  * (or any `GeneralName`) entry may be a form object (`{ dNSName: "..." }`) or a bare string classified
377
378
  * fail-closed into its form (`"host.example"`, `"a@b.example"`, `"10.0.0.1"`, `"https://host.example/"`).
378
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.5",
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:67dbc768-79f1-44d5-921c-53ce9accac3e",
5
+ "serialNumber": "urn:uuid:81aab9c8-7ee6-4344-8f73-8b672cade34f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-09-20T07:17:13.483Z",
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.5",
22
+ "bom-ref": "@blamejs/pki@0.8.6",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.8.5",
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.5",
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.5",
57
+ "ref": "@blamejs/pki@0.8.6",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]