@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 +13 -0
- package/README.md +12 -4
- package/lib/sign-scheme.js +82 -3
- package/lib/x509-sign.js +8 -7
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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
|
|
196
|
-
key
|
|
197
|
-
|
|
198
|
-
|
|
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 = {
|
package/lib/sign-scheme.js
CHANGED
|
@@ -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
|
|
222
|
-
throw E("bad-input", "
|
|
223
|
-
|
|
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
|
|
370
|
-
* (`string`), or a signer `{ algorithm, publicKey, sign }` for a key
|
|
371
|
-
* such as one in a hardware security module, a cloud key management
|
|
372
|
-
* PIV slot. `
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:81aab9c8-7ee6-4344-8f73-8b672cade34f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-20T07:
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.6",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.6",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|