@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 +13 -0
- package/README.md +27 -0
- package/lib/cms-sign.js +16 -6
- package/lib/cms-verify.js +2 -1
- package/lib/csr-sign.js +2 -1
- package/lib/ocsp.js +18 -7
- package/lib/pki-build.js +1 -1
- package/lib/sign-scheme.js +117 -5
- package/lib/x509-sign.js +7 -2
- 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.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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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) {
|
package/lib/sign-scheme.js
CHANGED
|
@@ -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
|
|
162
|
-
throw E("bad-input", "the signer
|
|
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
|
|
165
|
-
if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer
|
|
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)
|
|
370
|
-
* (`string`)
|
|
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
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:67dbc768-79f1-44d5-921c-53ce9accac3e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.8.5",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.8.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.8.5",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|