@blamejs/pki 0.8.3 → 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 +32 -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/guard-parsed.js +2 -2
- package/lib/ocsp.js +18 -7
- package/lib/pki-build.js +1 -1
- package/lib/schema-attrcert.js +36 -26
- package/lib/schema-c509.js +72 -42
- package/lib/schema-cmc.js +25 -22
- package/lib/schema-cmp.js +8 -8
- package/lib/schema-cms.js +39 -28
- package/lib/schema-crl.js +11 -11
- package/lib/schema-crmf.js +11 -6
- package/lib/schema-csrattrs.js +8 -6
- package/lib/schema-engine.js +65 -2
- package/lib/schema-ocsp.js +17 -16
- package/lib/schema-pkcs12.js +15 -11
- package/lib/schema-pkcs8.js +9 -2
- package/lib/schema-pkix.js +44 -17
- package/lib/schema-tsp.js +16 -3
- 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/lib/schema-pkcs8.js
CHANGED
|
@@ -193,8 +193,15 @@ function matches(root) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
/** @internal `ctx` is the walk context of the parse that reached this key, when another format
|
|
197
|
+
* carries it. Only its caps and budget cross over: the key is still read in this namespace, so a
|
|
198
|
+
* fault in it keeps the PKCS#8 error class and code. */
|
|
199
|
+
function walkPrivateKeyInfo(node, ctx) {
|
|
200
|
+
return schema.walk(PRIVATE_KEY_INFO, node, schema.withResources(NS, ctx)).result;
|
|
201
|
+
}
|
|
202
|
+
function walkEncryptedPrivateKeyInfo(node, ctx) {
|
|
203
|
+
return schema.walk(ENCRYPTED_PRIVATE_KEY_INFO, node, schema.withResources(NS, ctx)).result;
|
|
204
|
+
}
|
|
198
205
|
|
|
199
206
|
module.exports = {
|
|
200
207
|
parse: parse,
|
package/lib/schema-pkix.js
CHANGED
|
@@ -222,17 +222,16 @@ function _isPemArmor(buf) {
|
|
|
222
222
|
/** @internal The codec's resource refusals, as distinct from its malformed-input refusals. A
|
|
223
223
|
* caller who tightened a cap has to be able to tell "your bytes are wrong" from "your cap is
|
|
224
224
|
* too small", and only the second is answered by raising the cap. */
|
|
225
|
-
var
|
|
226
|
-
"asn1/too-large": 1, "asn1/too-deep": 1, "asn1/too-many-items": 1,
|
|
227
|
-
});
|
|
225
|
+
var _isResourceRefusal = schema.isResourceRefusal;
|
|
228
226
|
|
|
229
227
|
function decodeRoot(der, opts, caps, budget) {
|
|
228
|
+
// allow:bare-decode the root decode a door performs, which is where the caller's caps enter.
|
|
230
229
|
try { return asn1.decode(der, _decodeOptions(opts, caps)); }
|
|
231
230
|
catch (e) {
|
|
232
231
|
/** @internal `rawDecodeErrors` keeps the codec's own code for a malformed body, which some
|
|
233
232
|
* callers pin. A resource refusal is still translated, that code being new. */
|
|
234
|
-
if (opts.rawDecodeErrors && !(e
|
|
235
|
-
if (e
|
|
233
|
+
if (opts.rawDecodeErrors && !_isResourceRefusal(e)) throw e;
|
|
234
|
+
if (_isResourceRefusal(e)) {
|
|
236
235
|
if (budget) {
|
|
237
236
|
try {
|
|
238
237
|
budget.trip(opts.what || "the input");
|
|
@@ -338,12 +337,18 @@ var _emptyOptions = intrinsic.freeze(intrinsic.create(null));
|
|
|
338
337
|
function runParseWith(input, opts, callerOpts, build) {
|
|
339
338
|
var caps = parseCaps(callerOpts, opts);
|
|
340
339
|
var budget = guard.limits.budget({ maxBytes: caps.maxBytes });
|
|
341
|
-
var ns = _nsWithBudget(opts.ns, budget);
|
|
340
|
+
var ns = _nsWithBudget(opts.ns, budget, caps);
|
|
342
341
|
var result;
|
|
343
342
|
try {
|
|
344
343
|
result = build(decodeRoot(coerceToDer(input, opts), opts, caps, budget), ns, caps, budget);
|
|
345
344
|
} catch (e) {
|
|
346
345
|
if (guard.limits.isBudgetExceeded(e)) throw _budgetRefusal(opts, e);
|
|
346
|
+
/** @internal A refusal that is already this door's own resource verdict has been translated
|
|
347
|
+
* once and carries the codec's refusal as its cause; wrapping it again would put a copy of
|
|
348
|
+
* the verdict where a caller looks for the cap that was exceeded. */
|
|
349
|
+
if (budget.exhausted() && !(e && e.code === opts.prefix + "/too-large")) {
|
|
350
|
+
throw _budgetRefusal(opts, e);
|
|
351
|
+
}
|
|
347
352
|
throw e;
|
|
348
353
|
}
|
|
349
354
|
if (budget.exhausted()) throw _budgetRefusal(opts, null);
|
|
@@ -357,17 +362,31 @@ function runParse(input, opts, callerOpts) {
|
|
|
357
362
|
}
|
|
358
363
|
|
|
359
364
|
/** @internal The re-read at the door is what makes the latch worth having: a catch anywhere
|
|
360
|
-
* under the walk may absorb the exception, and this still
|
|
365
|
+
* under the walk may absorb the exception or rewrap it as a domain error, and this still
|
|
366
|
+
* refuses. The rewrapped error is carried as the cause. */
|
|
361
367
|
function _budgetRefusal(opts, cause) {
|
|
362
368
|
return new opts.ErrorClass(opts.prefix + "/too-large",
|
|
363
369
|
(opts.what || "the input") + " exceeds the decode budget for this parse", cause || undefined);
|
|
364
370
|
}
|
|
365
371
|
|
|
366
|
-
function _nsWithBudget(ns, budget) {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
372
|
+
function _nsWithBudget(ns, budget, caps) {
|
|
373
|
+
return schema.withResources(ns, { budget: budget, caps: caps });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** @internal The walk context a hand-rolled door builds for the structures it reads out of its
|
|
377
|
+
* own input, so the caller's caps reach them the way `runParseWith` makes them reach a factory
|
|
378
|
+
* parser's. The door re-reads `budget.exhausted()` before it returns a result. */
|
|
379
|
+
function nestedContext(ns, caps) {
|
|
380
|
+
var budget = guard.limits.budget({ maxBytes: caps.maxBytes });
|
|
381
|
+
return { ns: _nsWithBudget(ns, budget, caps), budget: budget };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** @internal The decode a format performs on a value it has already read out of its input: an
|
|
385
|
+
* embedded OCTET STRING payload, an encapsulated protocol body. The engine owns it, because
|
|
386
|
+
* `schema.embeddedDer` performs the same decode from inside a walk and the two must apply the
|
|
387
|
+
* caller's caps and trip the parse budget the same way. This is the name format modules call. */
|
|
388
|
+
function decodeNested(bytes, ctx, label, opts) {
|
|
389
|
+
return schema.decodeNested(bytes, ctx, label, opts);
|
|
371
390
|
}
|
|
372
391
|
|
|
373
392
|
var DN_SHORT = constants.NAMES.DN_SHORT;
|
|
@@ -489,6 +508,7 @@ function attrValueToString(ns) {
|
|
|
489
508
|
throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must be a non-empty even run of hex digits (RFC 4514 sec. 2.4)");
|
|
490
509
|
}
|
|
491
510
|
var raw = guard.encoding.hex(hex, null, ns.E, ns.prefix + "/bad-atv", "a #hex attribute value");
|
|
511
|
+
// allow:bare-decode the encoder side, checking bytes a caller wrote by hand, outside any parse.
|
|
492
512
|
try { asn1.decode(raw); }
|
|
493
513
|
catch (e) { throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must encode exactly one DER TLV", e); }
|
|
494
514
|
return raw;
|
|
@@ -826,7 +846,7 @@ function decodeIssuingDistributionPointValue(ns, bytes, code) {
|
|
|
826
846
|
indirect: false, onlyAttr: false, fields: null };
|
|
827
847
|
var m;
|
|
828
848
|
try {
|
|
829
|
-
m = schema.walk(issuingDistributionPoint(code),
|
|
849
|
+
m = schema.walk(issuingDistributionPoint(code), schema.decodeNested(bytes, ns, "an IssuingDistributionPoint value"), ns);
|
|
830
850
|
if (m.fields.distributionPoint.present) {
|
|
831
851
|
var wrap = m.fields.distributionPoint.node;
|
|
832
852
|
if (!wrap.children || wrap.children.length !== 1) {
|
|
@@ -928,15 +948,20 @@ function userNoticeTexts(qualifier) {
|
|
|
928
948
|
return i === kids.length ? out : [];
|
|
929
949
|
}
|
|
930
950
|
|
|
951
|
+
/** @internal The readers an extension or attribute value is decoded through. `ctx` is the walk
|
|
952
|
+
* context of the parse that reached the value, when one did: the bytes here are an OCTET STRING
|
|
953
|
+
* payload the root decode stopped at, so this decode restarts depth and item counting and needs
|
|
954
|
+
* the caller's caps handed to it. A caller outside a parse passes none and takes the built-in
|
|
955
|
+
* ceilings, which is what `ns` carries. */
|
|
931
956
|
function _decodeHelpers(ns) {
|
|
932
|
-
function decodeTop(buf, code, what) {
|
|
957
|
+
function decodeTop(buf, code, what, ctx) {
|
|
933
958
|
var n;
|
|
934
|
-
try { n =
|
|
959
|
+
try { n = schema.decodeNested(buf, ctx || ns, what); }
|
|
935
960
|
catch (e) { throw ns.E(code, "malformed " + what + " value: " + ((e && e.message) || _String(e)), e); }
|
|
936
961
|
return n;
|
|
937
962
|
}
|
|
938
|
-
function seqChildren(buf, code, what) {
|
|
939
|
-
var n = decodeTop(buf, code, what);
|
|
963
|
+
function seqChildren(buf, code, what, ctx) {
|
|
964
|
+
var n = decodeTop(buf, code, what, ctx);
|
|
940
965
|
if (n.tagClass !== "universal" || n.tagNumber !== _T.SEQUENCE || !n.children) {
|
|
941
966
|
throw ns.E(code, what + " must be a SEQUENCE (RFC 5280 sec. 4.2.1)");
|
|
942
967
|
}
|
|
@@ -1576,6 +1601,8 @@ module.exports = {
|
|
|
1576
1601
|
isCborResourceRefusal: isCborResourceRefusal,
|
|
1577
1602
|
cborParseOpts: cborParseOpts,
|
|
1578
1603
|
parseOpts: parseOpts,
|
|
1604
|
+
decodeNested: decodeNested,
|
|
1605
|
+
nestedContext: nestedContext,
|
|
1579
1606
|
makeNS: makeNS,
|
|
1580
1607
|
versionReader: versionReader,
|
|
1581
1608
|
DN_SHORT: DN_SHORT,
|
package/lib/schema-tsp.js
CHANGED
|
@@ -204,7 +204,7 @@ var TIME_STAMP_RESP = schema.seq([
|
|
|
204
204
|
schema.optional("timeStampToken", schema.any(), { whenUniversal: [TAGS.SEQUENCE] }),
|
|
205
205
|
], {
|
|
206
206
|
assert: "sequence", arity: { min: 1 }, code: "tsp/bad-response", what: "TimeStampResp",
|
|
207
|
-
build: function (m) {
|
|
207
|
+
build: function (m, ctx) {
|
|
208
208
|
var status = m.fields.status.value.result;
|
|
209
209
|
var present = m.fields.timeStampToken.present;
|
|
210
210
|
var granted = status.status === 0 || status.status === 1;
|
|
@@ -215,7 +215,9 @@ var TIME_STAMP_RESP = schema.seq([
|
|
|
215
215
|
status: status.status,
|
|
216
216
|
statusString: status.statusString,
|
|
217
217
|
failInfo: status.failInfo,
|
|
218
|
-
|
|
218
|
+
/** @internal The token is a second parse of bytes carried inside this one, so it takes the
|
|
219
|
+
* caps this door was given rather than the built-in ceilings. */
|
|
220
|
+
timeStampToken: present ? parseToken(m.fields.timeStampToken.value.bytes, ctx ? ctx.caps : undefined) : null,
|
|
219
221
|
};
|
|
220
222
|
},
|
|
221
223
|
});
|
|
@@ -424,12 +426,23 @@ function parseToken(input, callerOpts) {
|
|
|
424
426
|
if (!hasSigningCert) {
|
|
425
427
|
throw new TspError("tsp/missing-signing-certificate", "a TimeStampToken signerInfo must carry a SigningCertificate or SigningCertificateV2 signed attribute (RFC 3161 sec. 2.4.2, RFC 5816)");
|
|
426
428
|
}
|
|
429
|
+
/** @internal The TSTInfo is decoded out of the eContent the CMS layer handed back, so it is a
|
|
430
|
+
* second decode and takes the caller's caps the way the carrier did. The latch is re-read
|
|
431
|
+
* below, because the catch here answers in this domain and would otherwise report a cap that
|
|
432
|
+
* was too small as a malformed TSTInfo. */
|
|
433
|
+
var nested = pkix.nestedContext(NS, caps);
|
|
427
434
|
var tstInfo;
|
|
428
|
-
try { tstInfo = schema.embeddedDer(TST_INFO, encap.eContent,
|
|
435
|
+
try { tstInfo = schema.embeddedDer(TST_INFO, encap.eContent, nested.ns, { code: "tsp/bad-der", what: "the encapsulated TSTInfo" }); }
|
|
429
436
|
catch (e) {
|
|
437
|
+
if (nested.budget.exhausted()) {
|
|
438
|
+
throw new TspError("tsp/too-large", "the timeStampToken exceeds the decode budget for this parse", e);
|
|
439
|
+
}
|
|
430
440
|
if (e instanceof TspError) throw e;
|
|
431
441
|
throw new TspError("tsp/bad-der", "the encapsulated TSTInfo did not decode: " + ((e && e.message) || String(e)), e);
|
|
432
442
|
}
|
|
443
|
+
if (nested.budget.exhausted()) {
|
|
444
|
+
throw new TspError("tsp/too-large", "the timeStampToken exceeds the decode budget for this parse");
|
|
445
|
+
}
|
|
433
446
|
return { tstInfo: tstInfo.result, eContent: encap.eContent, signerInfo: signed.signerInfos[0], certificates: signed.certificates };
|
|
434
447
|
}
|
|
435
448
|
|
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
|
]
|