@blamejs/pki 0.2.22 → 0.2.24
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 +20 -1
- package/README.md +2 -2
- package/lib/byte-writer.js +75 -0
- package/lib/cms-decrypt.js +539 -0
- package/lib/cms-encrypt.js +498 -0
- package/lib/cms-verify.js +63 -1
- package/lib/constants.js +6 -0
- package/lib/ct.js +193 -8
- package/lib/oid.js +18 -1
- package/lib/schema-cms.js +9 -0
- package/lib/webcrypto.js +63 -8
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/ct.js
CHANGED
|
@@ -38,11 +38,13 @@
|
|
|
38
38
|
* bounded TLS-struct decode, fail-closed.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
|
+
var nodeCrypto = require("crypto");
|
|
41
42
|
var asn1 = require("./asn1-der.js");
|
|
42
43
|
var constants = require("./constants.js");
|
|
43
44
|
var frameworkError = require("./framework-error.js");
|
|
44
45
|
var guard = require("./guard-all.js");
|
|
45
46
|
var ByteReader = require("./byte-reader.js");
|
|
47
|
+
var ByteWriter = require("./byte-writer.js");
|
|
46
48
|
var oid = require("./oid.js");
|
|
47
49
|
var webcrypto = require("./webcrypto.js");
|
|
48
50
|
var validator = require("./validator-all.js");
|
|
@@ -136,15 +138,15 @@ function _parseSct(r, sctLen) {
|
|
|
136
138
|
|
|
137
139
|
/**
|
|
138
140
|
* @primitive pki.ct.parseSctList
|
|
139
|
-
* @signature pki.ct.parseSctList(extValue) -> { scts, unknownScts }
|
|
141
|
+
* @signature pki.ct.parseSctList(extValue) -> { scts, unknownScts, all }
|
|
140
142
|
* @since 0.1.20
|
|
141
143
|
* @status experimental
|
|
142
144
|
* @spec RFC 6962, RFC 5246, RFC 8446
|
|
143
|
-
* @related pki.ct.reconstructSignedData, pki.schema.x509.parse
|
|
145
|
+
* @related pki.ct.reconstructSignedData, pki.ct.encodeSctList, pki.schema.x509.parse
|
|
144
146
|
*
|
|
145
147
|
* Parse the value of an RFC 6962 SCT-list extension (the raw `extnValue`
|
|
146
148
|
* content an `x509.parse` / OCSP extension already surfaces) into
|
|
147
|
-
* `{ scts, unknownScts }`. Each entry of `scts` is a fully decoded v1 SCT:
|
|
149
|
+
* `{ scts, unknownScts, all }`. Each entry of `scts` is a fully decoded v1 SCT:
|
|
148
150
|
* `version` (0), `logId` (32-byte Buffer) + `logIdHex`, `timestamp` (BigInt,
|
|
149
151
|
* exact) + `timestampMs` (Number or `null` above 2^53) + `timestampDate`,
|
|
150
152
|
* `extensions` (raw Buffer), `hashAlg` / `sigAlg` (1-byte code points) + a named
|
|
@@ -152,7 +154,10 @@ function _parseSct(r, sctLen) {
|
|
|
152
154
|
* SerializedSCT body). A SerializedSCT whose version this parser does not define
|
|
153
155
|
* is preserved OPAQUE in `unknownScts` as `{ version, rawSct }` rather than
|
|
154
156
|
* failing the list -- RFC 6962 sec. 3.3 frames each SerializedSCT with its own length
|
|
155
|
-
* so unknown versions are skippable (forward compatibility).
|
|
157
|
+
* so unknown versions are skippable (forward compatibility). `all` lists every
|
|
158
|
+
* SerializedSCT (known and unknown) in the exact wire order, so
|
|
159
|
+
* `encodeSctList(all)` reproduces the list byte-identically even when the two
|
|
160
|
+
* kinds are interleaved.
|
|
156
161
|
*
|
|
157
162
|
* The extension value is a DER `OCTET STRING` wrapping the TLS-encoded list
|
|
158
163
|
* (RFC 6962 sec. 3.3 double wrap); everything below that peel is TLS presentation
|
|
@@ -186,7 +191,7 @@ function parseSctList(extValue) {
|
|
|
186
191
|
if (listLen < 1) {
|
|
187
192
|
throw new CtError("ct/empty-list", "an SCT list must contain at least one SCT (RFC 6962 sec. 3.3)");
|
|
188
193
|
}
|
|
189
|
-
var scts = [], unknownScts = [];
|
|
194
|
+
var scts = [], unknownScts = [], all = [];
|
|
190
195
|
// The shared item counter bounds the TOTAL element count (known + preserved-
|
|
191
196
|
// unknown) before it can drive unbounded per-element work (RFC 6962 sec. 3.3).
|
|
192
197
|
var sctCount = guard.limits.counter(C.LIMITS.SCT_MAX_COUNT, _ctErr, "ct/too-many-scts", "SCT");
|
|
@@ -203,10 +208,12 @@ function parseSctList(extValue) {
|
|
|
203
208
|
}
|
|
204
209
|
sctCount.tick();
|
|
205
210
|
var one = _parseSct(outer.subReader(sctLen, "ct/list-trailing-bytes"), sctLen);
|
|
206
|
-
if (one.unknown)
|
|
207
|
-
else scts.push(one);
|
|
211
|
+
if (one.unknown) { var u = { version: one.version, rawSct: one.rawSct }; unknownScts.push(u); all.push(u); }
|
|
212
|
+
else { scts.push(one); all.push(one); }
|
|
208
213
|
}
|
|
209
|
-
|
|
214
|
+
// `all` preserves the exact wire order across known + unknown entries, so encodeSctList(all)
|
|
215
|
+
// reproduces the list byte-identically even when the two kinds were interleaved.
|
|
216
|
+
return { scts: scts, unknownScts: unknownScts, all: all };
|
|
210
217
|
}
|
|
211
218
|
|
|
212
219
|
function _u24Bytes(n) {
|
|
@@ -409,10 +416,188 @@ async function verifySct(entry, sct, logPublicKey) {
|
|
|
409
416
|
}
|
|
410
417
|
}
|
|
411
418
|
|
|
419
|
+
// The TLS-vector WRITER bound to the ct/* fault domain -- the encode twin of TlsReader,
|
|
420
|
+
// over the shared ByteWriter engine primitive (see lib/byte-writer.js).
|
|
421
|
+
function TlsWriter() { return new ByteWriter(CtError, "ct/bad-input"); }
|
|
422
|
+
|
|
423
|
+
// Encode one SerializedSCT body. A v1 SCT is rebuilt from its fields in the exact _parseSct
|
|
424
|
+
// order (so it round-trips byte-identically); an opaque non-v1 entry re-emits its rawSct
|
|
425
|
+
// verbatim (RFC 6962 sec. 3.3 forward-compat symmetry -- encode preserves what parse preserved).
|
|
426
|
+
function _encodeSctBody(sct) {
|
|
427
|
+
if (!sct || typeof sct !== "object") throw new CtError("ct/bad-input", "each SCT must be an object");
|
|
428
|
+
if (sct.version !== 0) {
|
|
429
|
+
// The SerializedSCT version is a single byte (RFC 6962 sec. 3.2), so a declared version must be a
|
|
430
|
+
// 0..255 integer -- reject a non-byte value rather than masking it (a masked 263 would spoof v7).
|
|
431
|
+
if (typeof sct.version !== "number" || !Number.isInteger(sct.version) || sct.version < 0 || sct.version > 255) {
|
|
432
|
+
throw new CtError("ct/bad-input", "an SCT version must be a byte in 0..255 (RFC 6962 sec. 3.2)");
|
|
433
|
+
}
|
|
434
|
+
// An opaque entry re-emits its rawSct verbatim, but the SerializedSCT's on-wire version is
|
|
435
|
+
// rawSct[0] -- refuse a rawSct whose leading byte disagrees with the declared version, so encode
|
|
436
|
+
// never emits a list whose framing our own parser would reject or re-classify (RFC 6962 sec. 3.3).
|
|
437
|
+
var raw = _toBuffer(sct.rawSct, "sct.rawSct");
|
|
438
|
+
if (raw.length < 1 || raw[0] !== sct.version) {
|
|
439
|
+
throw new CtError("ct/bad-input", "an opaque SCT's rawSct[0] must equal its declared version (RFC 6962 sec. 3.3)");
|
|
440
|
+
}
|
|
441
|
+
return raw;
|
|
442
|
+
}
|
|
443
|
+
var w = new TlsWriter();
|
|
444
|
+
w.u8(0, "ct/bad-input"); // Version -- v1(0)
|
|
445
|
+
var logId = _toBuffer(sct.logId, "sct.logId");
|
|
446
|
+
if (logId.length !== LOGID_BYTES) throw new CtError("ct/bad-input", "an SCT logId must be exactly " + LOGID_BYTES + " bytes (RFC 6962 sec. 3.2)");
|
|
447
|
+
w.bytes(logId);
|
|
448
|
+
w.u64(guard.range.uint64(sct.timestamp, _ctErr, "ct/bad-input", "sct.timestamp"), "ct/bad-input"); // uint64 timestamp
|
|
449
|
+
w.vector(2, 0, 0xffff, _toBuffer(sct.extensions, "sct.extensions"), "ct/bad-input"); // CtExtensions<0..2^16-1>
|
|
450
|
+
w.u8(sct.hashAlg, "ct/bad-input"); // digitally-signed hash
|
|
451
|
+
w.u8(sct.sigAlg, "ct/bad-input"); // digitally-signed signature
|
|
452
|
+
w.vector(2, 0, 0xffff, _toBuffer(sct.signature, "sct.signature"), "ct/bad-input"); // signature<0..2^16-1>
|
|
453
|
+
return w.build();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* @primitive pki.ct.encodeSctList
|
|
458
|
+
* @signature pki.ct.encodeSctList(scts) -> Buffer
|
|
459
|
+
* @since 0.2.24
|
|
460
|
+
* @status experimental
|
|
461
|
+
* @spec RFC 6962, RFC 5246
|
|
462
|
+
* @related pki.ct.parseSctList, pki.ct.signSct
|
|
463
|
+
*
|
|
464
|
+
* Build the value of an RFC 6962 SCT-list extension from an array of SCTs -- the exact
|
|
465
|
+
* inverse of `parseSctList`, such that `parseSctList(encodeSctList(list.all))` round-trips to
|
|
466
|
+
* identical bytes. Each element is either a decoded v1 SCT (the shape `parseSctList().scts[]`
|
|
467
|
+
* or `signSct` returns: `version` 0, 32-byte `logId`, `timestamp` BigInt, raw `extensions`,
|
|
468
|
+
* `hashAlg` / `sigAlg` code points, raw `signature`) -- rebuilt from its fields in the RFC
|
|
469
|
+
* 6962 sec. 3.2 field order -- or an opaque non-v1 entry (`{ version, rawSct }`) whose
|
|
470
|
+
* `rawSct` is re-emitted verbatim (forward compatibility, sec. 3.3). Pass `parseSctList().all`
|
|
471
|
+
* (not `.scts`) to preserve the exact wire order and every unknown-version entry.
|
|
472
|
+
*
|
|
473
|
+
* Returns the DER `OCTET STRING`-wrapped TLS `SignedCertificateTimestampList` (the same
|
|
474
|
+
* `extnValue` content `parseSctList` consumes). The list must be non-empty and stays within
|
|
475
|
+
* the parser's `SCT_MAX_COUNT` element cap and the RFC 6962 sec. 3.3 65535-byte list-body cap so
|
|
476
|
+
* encode cannot emit what parse would reject. Throws a typed `CtError` (`ct/empty-list`,
|
|
477
|
+
* `ct/bad-input`, `ct/too-large`, `ct/too-many-scts`) on malformed input.
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* var list = pki.ct.parseSctList(sctExtValue);
|
|
481
|
+
* var reEncoded = pki.ct.encodeSctList(list.all); // byte-identical to sctExtValue
|
|
482
|
+
*/
|
|
483
|
+
function encodeSctList(scts) {
|
|
484
|
+
if (!Array.isArray(scts)) throw new CtError("ct/bad-input", "encodeSctList expects an array of SCTs");
|
|
485
|
+
if (scts.length < 1) throw new CtError("ct/empty-list", "an SCT list must contain at least one SCT (RFC 6962 sec. 3.3)");
|
|
486
|
+
var sctCount = guard.limits.counter(C.LIMITS.SCT_MAX_COUNT, _ctErr, "ct/too-many-scts", "SCT");
|
|
487
|
+
var elements = [], total = 0;
|
|
488
|
+
for (var i = 0; i < scts.length; i++) {
|
|
489
|
+
sctCount.tick();
|
|
490
|
+
var ew = new TlsWriter();
|
|
491
|
+
ew.vector(2, 1, 0xffff, _encodeSctBody(scts[i]), "ct/bad-input"); // SerializedSCT<1..2^16-1>
|
|
492
|
+
var el = ew.build();
|
|
493
|
+
total += el.length;
|
|
494
|
+
// Enforce the sct_list<1..2^16-1> body cap INCREMENTALLY, so an over-long list fails at the first
|
|
495
|
+
// element that crosses the bound instead of accumulating the whole (potentially large) set first.
|
|
496
|
+
if (total > 0xffff) throw new CtError("ct/too-large", "the SCT list body exceeds the 65535-byte maximum (RFC 6962 sec. 3.3)");
|
|
497
|
+
elements.push(el);
|
|
498
|
+
}
|
|
499
|
+
var lw = new TlsWriter();
|
|
500
|
+
lw.vector(2, 1, 0xffff, Buffer.concat(elements, total), "ct/too-large"); // sct_list<1..2^16-1>
|
|
501
|
+
return asn1.build.octetString(lw.build()); // RFC 6962 sec. 3.3 DER OCTET STRING wrap
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// A CT log private key -> its PKCS#8 DER (for WebCrypto sign) + the SPKI DER (for the LogID +
|
|
505
|
+
// the sec. 2.1.4 algorithm profile). Accepts a PKCS#8 DER Buffer, a PEM string, a node
|
|
506
|
+
// KeyObject, or a { key, format, type } input.
|
|
507
|
+
function _logKeyMaterial(logKey) {
|
|
508
|
+
// Every key operation runs inside the guard: a duck-typed impostor (an object carrying an
|
|
509
|
+
// asymmetricKeyType but not a real KeyObject) fails at export/createPublicKey and surfaces the
|
|
510
|
+
// typed ct/bad-input, never a raw TypeError. The private-key check keeps its own CtError.
|
|
511
|
+
try {
|
|
512
|
+
var keyObj;
|
|
513
|
+
if (logKey && typeof logKey === "object" && logKey.asymmetricKeyType) keyObj = logKey;
|
|
514
|
+
else if (Buffer.isBuffer(logKey)) keyObj = nodeCrypto.createPrivateKey({ key: logKey, format: "der", type: "pkcs8" });
|
|
515
|
+
else keyObj = nodeCrypto.createPrivateKey(logKey);
|
|
516
|
+
if (keyObj.type !== "private") throw new CtError("ct/bad-input", "signSct requires the CT log PRIVATE key");
|
|
517
|
+
return { pkcs8: keyObj.export({ type: "pkcs8", format: "der" }), spki: nodeCrypto.createPublicKey(keyObj).export({ type: "spki", format: "der" }) };
|
|
518
|
+
} catch (e) {
|
|
519
|
+
if (e instanceof CtError) throw e;
|
|
520
|
+
throw new CtError("ct/bad-input", "the CT log private key could not be loaded", e);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* @primitive pki.ct.signSct
|
|
526
|
+
* @signature pki.ct.signSct(entry, logKey, opts?) -> Promise<sct>
|
|
527
|
+
* @since 0.2.24
|
|
528
|
+
* @status experimental
|
|
529
|
+
* @spec RFC 6962
|
|
530
|
+
* @related pki.ct.verifySct, pki.ct.reconstructSignedData, pki.ct.encodeSctList
|
|
531
|
+
*
|
|
532
|
+
* Perform a Certificate Transparency log's signing step (RFC 6962 sec. 3.2): rebuild the exact
|
|
533
|
+
* `digitally-signed` preimage over `entry` (via `reconstructSignedData`, the SAME builder the
|
|
534
|
+
* verifier hashes), sign it with the log's private key, and return a fully-formed v1 SCT that
|
|
535
|
+
* `verifySct` accepts against the log's public key. `entry` is the log entry the SCT covers
|
|
536
|
+
* (`{ entryType: 0, leafCert }` or `{ entryType: 1, tbsCertificate, issuerKeyHash }`, as for
|
|
537
|
+
* `reconstructSignedData`); `logKey` is the log's private key (PKCS#8 DER `Buffer`, PEM string,
|
|
538
|
+
* or a node `KeyObject`).
|
|
539
|
+
*
|
|
540
|
+
* The log-key profile is RFC 6962 sec. 2.1.4: ECDSA NIST P-256 (`sigAlg` 3) or RSA >= 2048
|
|
541
|
+
* (`sigAlg` 1), SHA-256 only -- an unsupported key fails closed `ct/unsupported-algorithm`. The
|
|
542
|
+
* `logId` is derived as SHA-256 of the log SPKI (sec. 3.4); a supplied `opts.logId` must match.
|
|
543
|
+
* The returned SCT is the parseSctList/verifySct shape and composes with `encodeSctList`.
|
|
544
|
+
*
|
|
545
|
+
* @opts timestamp ms since the epoch (finite non-negative integer/BigInt). Default `Date.now()`.
|
|
546
|
+
* @opts extensions raw `CtExtensions` bytes (opaque<0..2^16-1>). Default empty.
|
|
547
|
+
* @opts logId assert the derived LogID equals this 32-byte value (fail closed on mismatch).
|
|
548
|
+
* @example
|
|
549
|
+
* var sct = await pki.ct.signSct({ entryType: 0, leafCert: der }, signerKeyPkcs8);
|
|
550
|
+
* var ext = pki.ct.encodeSctList([sct]);
|
|
551
|
+
*/
|
|
552
|
+
async function signSct(entry, logKey, opts) {
|
|
553
|
+
opts = opts || {};
|
|
554
|
+
var mat = _logKeyMaterial(logKey);
|
|
555
|
+
var alg = _spkiAlg(mat.spki);
|
|
556
|
+
var hashAlg = 4, sigAlg, imp, sign, ecdsaDer = false, coordLen;
|
|
557
|
+
if (alg.algOid === oid.byName("ecPublicKey")) {
|
|
558
|
+
var ec = CT_EC_CURVE[alg.curveOid];
|
|
559
|
+
if (!ec) throw new CtError("ct/unsupported-algorithm", "unsupported SCT log EC curve (RFC 6962 sec. 2.1.4 mandates NIST P-256)");
|
|
560
|
+
sigAlg = 3; imp = { name: "ECDSA", namedCurve: ec.curve }; sign = { name: "ECDSA", hash: "SHA-256" }; ecdsaDer = true; coordLen = ec.coordLen;
|
|
561
|
+
} else if (alg.algOid === oid.byName("rsaEncryption")) {
|
|
562
|
+
if (!(alg.rsaBits >= 2048)) throw new CtError("ct/unsupported-algorithm", "the SCT log RSA key is below the RFC 6962 sec. 2.1.4 minimum of 2048 bits");
|
|
563
|
+
sigAlg = 1; imp = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }; sign = { name: "RSASSA-PKCS1-v1_5" };
|
|
564
|
+
} else {
|
|
565
|
+
throw new CtError("ct/unsupported-algorithm", "unsupported SCT log key algorithm (RFC 6962 sec. 2.1.4 supports ecdsa P-256 / rsa)");
|
|
566
|
+
}
|
|
567
|
+
var timestamp;
|
|
568
|
+
if (opts.timestamp == null) timestamp = BigInt(Date.now());
|
|
569
|
+
else if (typeof opts.timestamp === "bigint") timestamp = opts.timestamp;
|
|
570
|
+
else if (typeof opts.timestamp === "number" && Number.isSafeInteger(opts.timestamp) && opts.timestamp >= 0) timestamp = BigInt(opts.timestamp);
|
|
571
|
+
else throw new CtError("ct/bad-input", "timestamp must be a finite non-negative integer or BigInt (RFC 6962 sec. 3.2)");
|
|
572
|
+
var extensions = opts.extensions == null ? Buffer.alloc(0) : _toBuffer(opts.extensions, "opts.extensions");
|
|
573
|
+
var logId = Buffer.from(await subtle.digest("SHA-256", mat.spki));
|
|
574
|
+
if (opts.logId != null && !_toBuffer(opts.logId, "opts.logId").equals(logId)) {
|
|
575
|
+
throw new CtError("ct/bad-input", "opts.logId does not match SHA-256 of the log key (RFC 6962 sec. 3.4)");
|
|
576
|
+
}
|
|
577
|
+
// Reuse the ONE preimage builder the verifier uses -- sign and verify cannot diverge.
|
|
578
|
+
var preimage = reconstructSignedData(entry, { version: 0, timestamp: timestamp, extensions: extensions });
|
|
579
|
+
var priv = await subtle.importKey("pkcs8", mat.pkcs8, imp, false, ["sign"]);
|
|
580
|
+
var sigRaw = Buffer.from(await subtle.sign(sign, priv, preimage));
|
|
581
|
+
var signature = ecdsaDer ? validator.sig.rawToEcdsaDer(sigRaw, coordLen) : sigRaw;
|
|
582
|
+
return {
|
|
583
|
+
version: 0,
|
|
584
|
+
logId: logId, logIdHex: logId.toString("hex"),
|
|
585
|
+
timestamp: timestamp,
|
|
586
|
+
extensions: extensions,
|
|
587
|
+
hashAlg: hashAlg, sigAlg: sigAlg,
|
|
588
|
+
// The `|| null` fallbacks mirror parseSctList's shape; unreachable here since signSct only ever
|
|
589
|
+
// emits the in-map sha256(4) + ecdsa(3)/rsa(1) code points (coverage residual).
|
|
590
|
+
signatureAlgorithm: { hash: hashAlg, hashName: HASH_ALGORITHMS[hashAlg] || null, signature: sigAlg, signatureName: SIGNATURE_ALGORITHMS[sigAlg] || null },
|
|
591
|
+
signature: signature,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
412
595
|
module.exports = {
|
|
413
596
|
parseSctList: parseSctList,
|
|
414
597
|
reconstructSignedData: reconstructSignedData,
|
|
415
598
|
verifySct: verifySct,
|
|
599
|
+
encodeSctList: encodeSctList,
|
|
600
|
+
signSct: signSct,
|
|
416
601
|
HASH_ALGORITHMS: HASH_ALGORITHMS,
|
|
417
602
|
SIGNATURE_ALGORITHMS: SIGNATURE_ALGORITHMS,
|
|
418
603
|
};
|
package/lib/oid.js
CHANGED
|
@@ -147,7 +147,7 @@ var FAMILIES = {
|
|
|
147
147
|
|
|
148
148
|
// PKCS#1 RSA public-key + RSASSA signature algorithms.
|
|
149
149
|
rsa: { base: [1, 2, 840, 113549, 1, 1], of: {
|
|
150
|
-
rsaEncryption: 1, rsaesOaep: 7, mgf1: 8, rsassaPss: 10,
|
|
150
|
+
rsaEncryption: 1, rsaesOaep: 7, mgf1: 8, pSpecified: 9, rsassaPss: 10,
|
|
151
151
|
sha256WithRSAEncryption: 11,
|
|
152
152
|
sha384WithRSAEncryption: 12, sha512WithRSAEncryption: 13 } },
|
|
153
153
|
|
|
@@ -215,9 +215,26 @@ var FAMILIES = {
|
|
|
215
215
|
// CEK-HKDF content-encryption wrapper (RFC 9709) a KEMRecipientInfo names, plus
|
|
216
216
|
// the RSA-KEM SPKI algorithm (RFC 9690). Parameters are absent for the HKDFs.
|
|
217
217
|
smimeAlg: { base: [1, 2, 840, 113549, 1, 9, 16, 3], of: {
|
|
218
|
+
"id-alg-PWRI-KEK": 9,
|
|
218
219
|
"id-rsa-kem": 14, "id-alg-hss-lms-hashsig": 17,
|
|
220
|
+
// RFC 8418 X25519/X448 key-agreement schemes (HKDF-based).
|
|
221
|
+
"dhSinglePass-stdDH-hkdf-sha256-scheme": 19, "dhSinglePass-stdDH-hkdf-sha384-scheme": 20,
|
|
222
|
+
"dhSinglePass-stdDH-hkdf-sha512-scheme": 21,
|
|
219
223
|
hkdfWithSha256: 28, hkdfWithSha384: 29, hkdfWithSha512: 30, cekHkdfSha256: 31 } },
|
|
220
224
|
|
|
225
|
+
// RFC 5753 ephemeral-static ECDH key-agreement schemes -- the keyEncryptionAlgorithm OID of a
|
|
226
|
+
// kari, whose PARAMETER is the KeyWrapAlgorithm (so these are NOT params-absent). The X9.63 KDF
|
|
227
|
+
// variants: stdDH (SECG arc 1.3.132.1.11) + cofactorDH (1.3.132.1.14) for SHA-224/256/384/512,
|
|
228
|
+
// and the SHA-1 KDF pair on the ANSI-X9.63 arc (the OpenSSL default).
|
|
229
|
+
secgStdDH: { base: [1, 3, 132, 1, 11], of: {
|
|
230
|
+
"dhSinglePass-stdDH-sha224kdf-scheme": 0, "dhSinglePass-stdDH-sha256kdf-scheme": 1,
|
|
231
|
+
"dhSinglePass-stdDH-sha384kdf-scheme": 2, "dhSinglePass-stdDH-sha512kdf-scheme": 3 } },
|
|
232
|
+
secgCofactorDH: { base: [1, 3, 132, 1, 14], of: {
|
|
233
|
+
"dhSinglePass-cofactorDH-sha224kdf-scheme": 0, "dhSinglePass-cofactorDH-sha256kdf-scheme": 1,
|
|
234
|
+
"dhSinglePass-cofactorDH-sha384kdf-scheme": 2, "dhSinglePass-cofactorDH-sha512kdf-scheme": 3 } },
|
|
235
|
+
x963Schemes: { base: [1, 3, 133, 16, 840, 63, 0], of: {
|
|
236
|
+
"dhSinglePass-stdDH-sha1kdf-scheme": 2, "dhSinglePass-cofactorDH-sha1kdf-scheme": 3 } },
|
|
237
|
+
|
|
221
238
|
// PKIX algorithms arc -- the stateful hash-based signature algorithm
|
|
222
239
|
// identifiers (RFC 9802 sec. 4). HSS/LMS additionally has the SMIME
|
|
223
240
|
// id-alg-hss-lms-hashsig OID above (RFC 9708 / RFC 9802 share it).
|
package/lib/schema-cms.js
CHANGED
|
@@ -1203,4 +1203,13 @@ module.exports = {
|
|
|
1203
1203
|
walkSignedData: walkSignedData,
|
|
1204
1204
|
walkEncryptedData: walkEncryptedData,
|
|
1205
1205
|
assertAttachedCiphertext: assertAttachedCiphertext,
|
|
1206
|
+
// The structure's own algorithm tables, exported (like the walk* helpers) as the single source
|
|
1207
|
+
// of truth the crypto layer (cms-encrypt / cms-decrypt) shares -- so the wrap<->KEK-length, the
|
|
1208
|
+
// ML-KEM ciphertext-length, and the AEAD-mode/ICV-length rules the parser enforces cannot drift
|
|
1209
|
+
// from what the producer/consumer emit and accept.
|
|
1210
|
+
WRAP_KEK_LENGTHS: WRAP_KEK_LENGTHS,
|
|
1211
|
+
KEM_CT_LENGTHS: KEM_CT_LENGTHS,
|
|
1212
|
+
AEAD_ALGS: AEAD_ALGS,
|
|
1213
|
+
AEAD_GCM_ICVLENS: AEAD_GCM_ICVLENS,
|
|
1214
|
+
AEAD_CCM_ICVLENS: AEAD_CCM_ICVLENS,
|
|
1206
1215
|
};
|
package/lib/webcrypto.js
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
* still runs on -- RSASSA-PKCS1-v1_5, RSA-PSS, RSA-OAEP, ECDSA, ECDH,
|
|
23
23
|
* Ed25519 / Ed448, AES-GCM / CBC / KW, HMAC, HKDF, PBKDF2, and the SHA
|
|
24
24
|
* family (including legacy SHA-1 for old certificates and signatures).
|
|
25
|
-
* FIPS 203 ML-KEM key generation, encoding,
|
|
26
|
-
*
|
|
27
|
-
*
|
|
25
|
+
* FIPS 203 ML-KEM key generation, encoding, certificate/PKCS#8 import,
|
|
26
|
+
* and encapsulation/decapsulation (encapsulateBits / decapsulateBits over
|
|
27
|
+
* Node's crypto.encapsulate/decapsulate) are all available.
|
|
28
28
|
* Because it is OpenSSL-backed, every key and signature it emits is
|
|
29
29
|
* interoperable with OpenSSL, NSS, and other PKI implementations.
|
|
30
30
|
*
|
|
@@ -83,6 +83,7 @@ function _normalizeAlg(algorithm, who) {
|
|
|
83
83
|
// backwards compatibility with legacy certificates and signatures.
|
|
84
84
|
var HASH_NODE = {
|
|
85
85
|
"SHA-1": "sha1",
|
|
86
|
+
"SHA-224": "sha224",
|
|
86
87
|
"SHA-256": "sha256",
|
|
87
88
|
"SHA-384": "sha384",
|
|
88
89
|
"SHA-512": "sha512",
|
|
@@ -130,9 +131,8 @@ var ML_KEM_NODE = { "ML-KEM-512": "ml-kem-512", "ML-KEM-768": "ml-kem-768", "ML-
|
|
|
130
131
|
|
|
131
132
|
// FIPS 205 SLH-DSA -- stateless hash-based signatures. All twelve
|
|
132
133
|
// parameter sets Node exposes; signing is one-shot (null algorithm), the
|
|
133
|
-
// same shape as ML-DSA / EdDSA. ML-KEM
|
|
134
|
-
// import
|
|
135
|
-
// to the CMS KEM-decrypt surface, not blocked by the engine.
|
|
134
|
+
// same shape as ML-DSA / EdDSA. ML-KEM covers key-generation / encoding /
|
|
135
|
+
// import plus encapsulateBits / decapsulateBits over Node's KEM primitive.
|
|
136
136
|
var SLH_DSA_NODE = {};
|
|
137
137
|
["sha2-128s", "sha2-128f", "sha2-192s", "sha2-192f", "sha2-256s", "sha2-256f",
|
|
138
138
|
"shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
|
|
@@ -147,13 +147,13 @@ var SIGN_VERIFY_NAMES = {};
|
|
|
147
147
|
.concat(Object.keys(ML_DSA_NODE), Object.keys(SLH_DSA_NODE))
|
|
148
148
|
.forEach(function (n) { SIGN_VERIFY_NAMES[n] = true; });
|
|
149
149
|
var ENCRYPT_DECRYPT_NAMES = { "RSA-OAEP": true, "AES-GCM": true, "AES-CBC": true, "AES-CTR": true };
|
|
150
|
-
var DERIVE_NAMES = { "ECDH": true, "X25519": true, "X448": true, "HKDF": true, "PBKDF2": true };
|
|
150
|
+
var DERIVE_NAMES = { "ECDH": true, "X25519": true, "X448": true, "HKDF": true, "PBKDF2": true, "X963KDF": true };
|
|
151
151
|
// The secret-key / KDF algorithms whose key material is raw octets (imported via "raw" or a
|
|
152
152
|
// JWK "oct"), NEVER an SPKI / PKCS#8 asymmetric-key structure. importKey("spki"|"pkcs8", ...)
|
|
153
153
|
// under one of these names is unsupported (W3C: NotSupportedError) -- and, without this gate, it
|
|
154
154
|
// would mint a mislabeled CryptoKey wrapping an asymmetric handle AND dodge the algorithm-keyed
|
|
155
155
|
// pkcs8 pre-validation (e.g. the RFC 9935 ML-KEM CHOICE guard).
|
|
156
|
-
var SECRET_KEY_NAMES = { "AES-GCM": true, "AES-CBC": true, "AES-CTR": true, "AES-KW": true, "HMAC": true, "HKDF": true, "PBKDF2": true };
|
|
156
|
+
var SECRET_KEY_NAMES = { "AES-GCM": true, "AES-CBC": true, "AES-CTR": true, "AES-KW": true, "HMAC": true, "HKDF": true, "PBKDF2": true, "X963KDF": true };
|
|
157
157
|
|
|
158
158
|
// ---- CryptoKey -------------------------------------------------------
|
|
159
159
|
|
|
@@ -533,9 +533,29 @@ function _deriveBitsRaw(alg, key, length) {
|
|
|
533
533
|
var out = nodeCrypto.pbkdf2Sync(_secretBytes(key), _toBuf(alg.salt, "PBKDF2 salt"), alg.iterations, length / 8, _hashNode(alg.hash, "PBKDF2"));
|
|
534
534
|
return _toArrayBuffer(out);
|
|
535
535
|
}
|
|
536
|
+
if (name === "X963KDF") {
|
|
537
|
+
// ANSI-X9.63 / SEC1 sec. 3.6.1 single-step KDF: K = H(Z || INT32(counter) || SharedInfo)
|
|
538
|
+
// concatenated over counter = 1, 2, ... The RFC 5753 kari KEK derivation; the base key holds
|
|
539
|
+
// the ECDH shared secret Z, alg.info holds the DER ECC-CMS-SharedInfo.
|
|
540
|
+
_requireDeriveLength(length, "X963KDF");
|
|
541
|
+
return _toArrayBuffer(_x963Kdf(_hashNode(alg.hash, "X963KDF"), _secretBytes(key), _toBuf(alg.info || Buffer.alloc(0), "X963KDF SharedInfo"), length / 8));
|
|
542
|
+
}
|
|
536
543
|
throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + JSON.stringify(name));
|
|
537
544
|
}
|
|
538
545
|
|
|
546
|
+
// The X9.63 single-step KDF counter loop. Bounded by the 2^32 counter range; every input is a
|
|
547
|
+
// public/agreed value, so a plain (non-constant-time) hash concat is correct.
|
|
548
|
+
function _x963Kdf(hashNode, z, sharedInfo, lenBytes) {
|
|
549
|
+
var blocks = [], counter = 1, got = 0;
|
|
550
|
+
while (got < lenBytes) {
|
|
551
|
+
if (counter > 0xffffffff) throw new WebCryptoError("webcrypto/operation", "X963KDF: requested output exceeds the counter range");
|
|
552
|
+
var ctr = Buffer.alloc(4); ctr.writeUInt32BE(counter, 0);
|
|
553
|
+
var h = nodeCrypto.createHash(hashNode).update(z).update(ctr).update(sharedInfo).digest();
|
|
554
|
+
blocks.push(h); got += h.length; counter += 1;
|
|
555
|
+
}
|
|
556
|
+
return Buffer.concat(blocks).subarray(0, lenBytes);
|
|
557
|
+
}
|
|
558
|
+
|
|
539
559
|
SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, length) {
|
|
540
560
|
var alg = _normalizeAlg(algorithm, "deriveBits");
|
|
541
561
|
_requireUsage(key, "deriveBits");
|
|
@@ -579,6 +599,41 @@ SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey,
|
|
|
579
599
|
return this.importKey("raw", raw, dk, extractable, keyUsages);
|
|
580
600
|
};
|
|
581
601
|
|
|
602
|
+
// ML-KEM (FIPS 203) key encapsulation over Node's crypto.encapsulate/decapsulate.
|
|
603
|
+
// encapsulateBits takes the recipient's PUBLIC (encapsulation) key and returns a fresh
|
|
604
|
+
// { sharedKey, ciphertext }; decapsulateBits takes the PRIVATE (decapsulation) key + a
|
|
605
|
+
// ciphertext and recovers the shared key. ML-KEM's Fujisaki-Okamoto transform gives IMPLICIT
|
|
606
|
+
// rejection: a tampered but correctly-sized ciphertext decapsulates to a pseudo-random shared
|
|
607
|
+
// key rather than failing, so decapsulateBits only throws (typed) on a malformed / wrong-length
|
|
608
|
+
// ciphertext -- the "wrong key" case is indistinguishable by design, which the CMS uniform
|
|
609
|
+
// decrypt-failure verdict relies on. The usage split (encapsulateBits -> public, decapsulateBits
|
|
610
|
+
// -> private) already lives in the generateKey usage filters; the explicit type check hardens a
|
|
611
|
+
// key imported with custom usages.
|
|
612
|
+
SubtleCrypto.prototype.encapsulateBits = async function encapsulateBits(algorithm, encapsulationKey) {
|
|
613
|
+
var alg = _normalizeAlg(algorithm, "encapsulateBits");
|
|
614
|
+
_requireUsage(encapsulationKey, "encapsulateBits");
|
|
615
|
+
if (!ML_KEM_NODE[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "encapsulateBits: unsupported algorithm " + JSON.stringify(alg.name));
|
|
616
|
+
_requireAlgMatch(alg, encapsulationKey, "encapsulateBits");
|
|
617
|
+
if (encapsulationKey.type !== "public") throw new WebCryptoError("webcrypto/invalid-access", "encapsulateBits requires a public (encapsulation) key, got " + JSON.stringify(encapsulationKey.type));
|
|
618
|
+
var r;
|
|
619
|
+
try { r = nodeCrypto.encapsulate(encapsulationKey._handle); }
|
|
620
|
+
catch (e) { throw new WebCryptoError("webcrypto/operation", "encapsulateBits: ML-KEM encapsulation failed", e); }
|
|
621
|
+
return { sharedKey: _toArrayBuffer(Buffer.from(r.sharedKey)), ciphertext: _toArrayBuffer(Buffer.from(r.ciphertext)) };
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
SubtleCrypto.prototype.decapsulateBits = async function decapsulateBits(algorithm, decapsulationKey, ciphertext) {
|
|
625
|
+
var alg = _normalizeAlg(algorithm, "decapsulateBits");
|
|
626
|
+
_requireUsage(decapsulationKey, "decapsulateBits");
|
|
627
|
+
if (!ML_KEM_NODE[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "decapsulateBits: unsupported algorithm " + JSON.stringify(alg.name));
|
|
628
|
+
_requireAlgMatch(alg, decapsulationKey, "decapsulateBits");
|
|
629
|
+
if (decapsulationKey.type !== "private") throw new WebCryptoError("webcrypto/invalid-access", "decapsulateBits requires a private (decapsulation) key, got " + JSON.stringify(decapsulationKey.type));
|
|
630
|
+
var ct = _toBuf(ciphertext, "decapsulateBits ciphertext");
|
|
631
|
+
var ss;
|
|
632
|
+
try { ss = nodeCrypto.decapsulate(decapsulationKey._handle, ct); }
|
|
633
|
+
catch (e) { throw new WebCryptoError("webcrypto/operation", "decapsulateBits: ML-KEM decapsulation failed (malformed or wrong-length ciphertext)", e); }
|
|
634
|
+
return _toArrayBuffer(Buffer.from(ss));
|
|
635
|
+
};
|
|
636
|
+
|
|
582
637
|
SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey, wrapAlgorithm) {
|
|
583
638
|
var exported = await this.exportKey(format, key);
|
|
584
639
|
var bytes = (format === "jwk") ? Buffer.from(JSON.stringify(exported)) : Buffer.from(exported);
|
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:778bbc49-b521-4695-b1a1-915cda8b8012",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-16T09:47:36.237Z",
|
|
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.2.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.24",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.24",
|
|
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.2.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.24",
|
|
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.2.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.24",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|