@blamejs/pki 0.6.48 → 0.6.50

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/lib/cmp-build.js CHANGED
@@ -13,12 +13,13 @@
13
13
  * PKCS#10 via `pki.csr.sign`, a `certConf` / `pollReq` / `genm` / `rr`, or a `nested` wrapper of other
14
14
  * PKIMessages an RA forwards), and an optional
15
15
  * `PKIProtection` computed over the message. Protection is a signature under the sender key (any
16
- * registry algorithm) or a PBMAC1 shared-secret MAC. The emitted message round-trips through
16
+ * registry algorithm), a PBMAC1 shared-secret MAC, or a KEM-based MAC keyed by a secret the peer
17
+ * encapsulated to the sender's ML-KEM key. The emitted message round-trips through
17
18
  * `pki.schema.cmp.parse` and its protection verifies. `pki.cmp.transfer` carries a message to a CMP
18
19
  * endpoint (RFC 9811), `pki.cmp.verify` checks an incoming message's protection, and `pki.cmp.session`
19
20
  * drives a full enrollment transaction end to end over these. Parsing lives at `pki.schema.cmp.parse`.
20
21
  * @spec RFC 9810
21
- * @card Build a CMP PKIMessage with signature or PBMAC1 protection.
22
+ * @card Build a CMP PKIMessage with signature, PBMAC1 or KEM-based protection.
22
23
  */
23
24
 
24
25
  var asn1 = require("./asn1-der");
@@ -52,6 +53,7 @@ var signScheme = require("./sign-scheme");
52
53
  var pkix = require("./schema-pkix");
53
54
  var pkiBuild = require("./pki-build");
54
55
  var webcrypto = require("./webcrypto");
56
+ var subtle = webcrypto.webcrypto.subtle;
55
57
  var pbes2 = require("./pbes2");
56
58
  var constants = require("./constants");
57
59
  var guard = require("./guard-all");
@@ -87,9 +89,10 @@ var BODY_TAG = intrinsic.assign(intrinsic.create(null), {
87
89
  });
88
90
  var CRMF_BODY = intrinsic.assign(intrinsic.create(null), { ir: 1, cr: 1, kur: 1, krr: 1, ccr: 1 });
89
91
  var CERT_REP_ARM = intrinsic.assign(intrinsic.create(null), { ip: 1, cp: 1, kup: 1, ccp: 1 });
90
- var KNOWN_OPTS_KEYS = intrinsic.assign(intrinsic.create(null), { key: 1, cert: 1, mac: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 });
92
+ var KNOWN_OPTS_KEYS = intrinsic.assign(intrinsic.create(null), { key: 1, cert: 1, mac: 1, kem: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 });
91
93
  var KNOWN_OPTS_SIG_KEYS = intrinsic.assign(intrinsic.create(null), { key: 1, cert: 1, extraCerts: 1, pem: 1, pss: 1, digestAlgorithm: 1 });
92
94
  var KNOWN_OPTS_MAC_KEYS = intrinsic.assign(intrinsic.create(null), { mac: 1, extraCerts: 1, pem: 1 });
95
+ var KNOWN_OPTS_KEM_KEYS = intrinsic.assign(intrinsic.create(null), { kem: 1, extraCerts: 1, pem: 1 });
93
96
  var KNOWN_MESSAGE_KEYS = intrinsic.assign(intrinsic.create(null), { header: 1, body: 1 });
94
97
  var KNOWN_MAC_KEYS = intrinsic.assign(intrinsic.create(null), { secret: 1, salt: 1, iterationCount: 1, prf: 1, keyLength: 1, algorithm: 1 });
95
98
 
@@ -545,11 +548,164 @@ async function _p10crBody(csrDer, parsedCsr, tag, state) {
545
548
 
546
549
 
547
550
 
548
- function _resolveProtection(opts) {
551
+ /** @internal A byte field of the KEM protection spec, taken through the toolkit's byte door so a
552
+ * caller object cannot answer differently on a second read. */
553
+ function _kemBytes(v, what) {
554
+ if (v == null) throw _err("cmp/bad-input", what + " is required for KEM-based protection");
555
+ if (!guard.bytes.isByteSource(v)) throw _err("cmp/bad-input", what + " must be DER bytes");
556
+ return guard.bytes.snapshotSource(v, CmpError, "cmp/bad-input", what);
557
+ }
558
+
559
+ var KEM_STATIC_STRING = "CMP-KEM";
560
+ var KNOWN_KEM_KEYS = intrinsic.assign(intrinsic.create(null),
561
+ { key: 1, ciphertext: 1, kemAlgorithm: 1, kemContext: 1, transactionID: 1, len: 1 });
562
+ /** @internal The KEM algorithms a ciphertext can be decapsulated under, and the MAC the derived key
563
+ * drives. RFC 9810 sec. 5.1.3.4 leaves both to the profile and points at RFC 9481; the shipped set is
564
+ * the ML-KEM parameter sets this toolkit already establishes secrets with, under HMAC-SHA256. */
565
+ /** @internal Keyed by the DOTTED OID, not by a display name: `pki.oid.register` can re-point a name,
566
+ * and an algorithm this client supports must not become unsupported because a caller renamed it. */
567
+ var KEM_DECAPSULATE = intrinsic.create(null);
568
+ KEM_DECAPSULATE[O("id-ml-kem-512")] = "ML-KEM-512";
569
+ KEM_DECAPSULATE[O("id-ml-kem-768")] = "ML-KEM-768";
570
+ KEM_DECAPSULATE[O("id-ml-kem-1024")] = "ML-KEM-1024";
571
+ var KEM_MAC_KEY_BYTES = 32;
572
+
573
+ /** @internal The context a KEM-derived protection key is bound to (RFC 9810 sec. 5.1.3.4). Written
574
+ * ONCE and read by both the protecting and the verifying half, because two copies of one derivation
575
+ * drift: the static string, the transaction identifier and any kemContext all feed the KDF, so a
576
+ * single byte of disagreement yields a key the peer cannot reproduce. The CMP module is EXPLICIT TAGS,
577
+ * so `[0]` wraps the OCTET STRING. */
578
+ function kemOtherInfo(transactionID, kemContext) {
579
+ var kids = [b.sequence([b.utf8(KEM_STATIC_STRING)]), b.octetString(transactionID)];
580
+ if (kemContext != null) guard.list.append(kids, b.explicit(0, b.octetString(kemContext)));
581
+ return b.sequence(kids);
582
+ }
583
+
584
+ /** @internal The shared secret key: HKDF over the KEM shared secret, with the DER KemOtherInfo as
585
+ * info and `len` as the output size the MAC algorithm needs. */
586
+ async function kemSharedKey(sharedSecret, len, transactionID, kemContext) {
587
+ var info = kemOtherInfo(transactionID, kemContext);
588
+ var ikm = await subtle.importKey("raw", sharedSecret, { name: "HKDF" }, false, ["deriveBits"]);
589
+ var bits = await subtle.deriveBits(
590
+ { name: "HKDF", hash: "SHA-256", salt: intrinsic.bufferAlloc(0), info: info }, ikm, len * 8);
591
+ return intrinsic.bufferFrom(bits);
592
+ }
593
+
594
+ /** @internal The protecting half of RFC 9810 sec. 5.1.3.4: the holder of the KEM private key
595
+ * decapsulates the ciphertext the peer sent, derives the shared secret key, and MACs the message with
596
+ * it. Decapsulation failure terminates the operation with the failInfo the section names, rather than
597
+ * falling through to another flavor. */
598
+ /** @internal The decapsulation key, from the shapes every other key option in this module accepts: a
599
+ * WebCrypto CryptoKey is adopted as it is, which is what `pki.key.generate` hands back, and an encoded
600
+ * key is decoded and imported. Any copy the decode makes is collected for the caller to wipe. */
601
+ async function _kemPrivateKey(rawKey, wcName, ownedKeyCopies) {
602
+ if (rawKey && typeof rawKey === "object" && !guard.bytes.isByteSource(rawKey) &&
603
+ typeof rawKey.type === "string" && rawKey.algorithm != null) {
604
+ if (rawKey.type !== "private") {
605
+ throw _err("cmp/bad-input", "opts.kem.key is a CryptoKey of type " + guard.text.showValue(rawKey.type) +
606
+ "; decapsulation needs the private key (type \"private\")");
607
+ }
608
+ return webcrypto.adoptKey(rawKey, { name: wcName }, ["decapsulateBits"], _signE, "bad-input");
609
+ }
610
+ var keyDer = signScheme.normalizePkcs8(rawKey, "opts.kem.key", _signE, ownedKeyCopies);
611
+ return subtle.importKey("pkcs8", keyDer, { name: wcName }, false, ["decapsulateBits"]);
612
+ }
613
+
614
+ /** @internal The KemBMParameter fields in the order sec. 5.1.3.4 gives them, with the optional
615
+ * kemContext between the KDF and the length. */
616
+ function _kemBmParameterKids(len, kemContext) {
617
+ var kids = [b.sequence([b.oid(oid.byName("hkdfWithSha256"))])];
618
+ if (kemContext != null) guard.list.append(kids, b.explicit(0, b.octetString(kemContext)));
619
+ guard.list.append(kids, b.integer(_bigInt(len)));
620
+ guard.list.append(kids, b.sequence([b.oid(oid.byName("hmacWithSHA256"))]));
621
+ return kids;
622
+ }
623
+
624
+ /** @internal The MAC the derived key drives, computed the same way on both halves. */
625
+ async function kemMac(sharedKey, protectedPartDer) {
626
+ var k = await subtle.importKey("raw", sharedKey, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
627
+ return intrinsic.bufferFrom(await subtle.sign({ name: "HMAC" }, k, protectedPartDer));
628
+ }
629
+
630
+ function _resolveKemProtection(kemSpec, message) {
631
+ if (!kemSpec || typeof kemSpec !== "object" || _isBuffer(kemSpec)) {
632
+ throw _err("cmp/bad-input", "opts.kem must be an object { key, ciphertext, kemAlgorithm, kemContext?, transactionID?, len? }");
633
+ }
634
+ guard.identifier.assertKnownKeys(kemSpec, KNOWN_KEM_KEYS, _err, "cmp/bad-input", "unknown opts.kem field ");
635
+ if (kemSpec.key == null) throw _err("cmp/bad-input", "opts.kem.key is the KEM private key the ciphertext was encapsulated to");
636
+ /** @internal Taken once, and decoded LATER: normalizing a PEM or a Uint8Array allocates a buffer
637
+ * holding the private key, and anything that throws between here and the MAC would leave it behind.
638
+ * The copy is therefore made inside the protected region below, where its cleanup runs on every
639
+ * exit; a caller-owned Buffer is only viewed and is never wiped. */
640
+ var rawKey = kemSpec.key;
641
+ var ct = _kemBytes(kemSpec.ciphertext, "opts.kem.ciphertext (the KEM ciphertext the peer sent)");
642
+ var algName = kemSpec.kemAlgorithm;
643
+ if (typeof algName !== "string") throw _err("cmp/bad-input", "opts.kem.kemAlgorithm names the KEM the ciphertext was produced under");
644
+ var algOid = oid.isDottedDecimal(algName) ? algName : oid.byName(algName);
645
+ var wcName = KEM_DECAPSULATE[guard.text.keyOf(algOid)];
646
+ if (!wcName) {
647
+ throw _err("cmp/unsupported-algorithm",
648
+ "unsupported KEM " + algName + " for KEM-based message protection (ML-KEM-512/768/1024)");
649
+ }
650
+ /** @internal The ASN.1 admits len 1, and a one-byte key is one an observer of a single message can
651
+ * search exhaustively, so the floor is the one this module already applies to a MAC key rather than
652
+ * the syntax's. */
653
+ var len = kemSpec.len == null ? KEM_MAC_KEY_BYTES : kemSpec.len;
654
+ if (typeof len !== "number" || !Number.isInteger(len) ||
655
+ len < constants.LIMITS.CMP_KEM_MIN_KEY_BYTES || len > constants.LIMITS.CMP_KEM_MAX_KEY_BYTES) {
656
+ throw _err("cmp/bad-input", "opts.kem.len is the derived key size in bytes, " +
657
+ constants.LIMITS.CMP_KEM_MIN_KEY_BYTES + ".." + constants.LIMITS.CMP_KEM_MAX_KEY_BYTES +
658
+ " (a shorter key would not carry the strength the MAC is meant to have)");
659
+ }
660
+ var kemContext = kemSpec.kemContext == null ? null
661
+ : _kemBytes(kemSpec.kemContext, "opts.kem.kemContext");
662
+ /** @internal "transactionID MUST be the value from the message containing the ciphertext", which is
663
+ * this message's own id in the ordinary flow and the peer's when it sent the ciphertext first, so
664
+ * the caller can name it. */
665
+ var txid = kemSpec.transactionID != null
666
+ ? _kemBytes(kemSpec.transactionID, "opts.kem.transactionID")
667
+ /** @internal `_build` has already refused a spec with no header, so this reads the header it
668
+ * verified rather than re-testing for one. */
669
+ : _kemBytes(message.header.transactionID,
670
+ "message.header.transactionID (a KEM-derived key is bound to it)");
671
+ return {
672
+ protectionAlgDer: b.sequence([b.oid(oid.byName("kemBasedMac")), b.sequence(_kemBmParameterKids(len, kemContext))]),
673
+ certDer: null,
674
+ computeBits: async function (protectedPartDer) {
675
+ var ss = null, ssk = null, ownedKeyCopies = [];
676
+ try {
677
+ try {
678
+ var priv = await _kemPrivateKey(rawKey, wcName, ownedKeyCopies);
679
+ ss = intrinsic.bufferFrom(await subtle.decapsulateBits({ name: wcName }, priv, ct));
680
+ } catch (e) {
681
+ if (e instanceof CmpError) throw e;
682
+ throw _err("cmp/bad-message-check",
683
+ "the KEM ciphertext did not decapsulate under opts.kem.key, so no protection key can be " +
684
+ "derived and the operation is terminated (RFC 9810 sec. 5.1.3.4 step 2)", e);
685
+ }
686
+ ssk = await kemSharedKey(ss, len, txid, kemContext);
687
+ return b.bitString(await kemMac(ssk, protectedPartDer), 0);
688
+ } finally {
689
+ var owned = [];
690
+ if (ss) guard.list.append(owned, ss);
691
+ if (ssk) guard.list.append(owned, ssk);
692
+ for (var ci = 0; ci < ownedKeyCopies.length; ci++) guard.list.append(owned, ownedKeyCopies[ci]);
693
+ ownedKeyCopies.length = 0;
694
+ if (owned.length) guard.secret.zeroizeAll(owned, CmpError, "cmp/bad-input", "the KEM shared secret");
695
+ }
696
+ },
697
+ };
698
+ }
699
+
700
+ function _resolveProtection(opts, message) {
549
701
  var hasSig = opts.key != null || opts.cert != null;
550
702
  var hasMac = opts.mac != null;
551
- if (hasSig && hasMac) throw _err("cmp/bad-input", "supply exactly one of { key, cert } (signature) or { mac } (PBMAC1), not both");
552
- if (!hasSig && !hasMac) throw _err("cmp/bad-input", "a PKIMessage requires protection: supply { key, cert } for a signature or { mac } for PBMAC1");
703
+ var hasKem = opts.kem != null;
704
+ if ((hasSig ? 1 : 0) + (hasMac ? 1 : 0) + (hasKem ? 1 : 0) > 1) {
705
+ throw _err("cmp/bad-input", "supply exactly one of { key, cert } (signature), { mac } (PBMAC1) or { kem } (KEM-based MAC), not more than one");
706
+ }
707
+ if (hasKem) return _resolveKemProtection(opts.kem, message);
708
+ if (!hasSig && !hasMac) throw _err("cmp/bad-input", "a PKIMessage requires protection: supply { key, cert } for a signature, { mac } for PBMAC1, or { kem } for a KEM-based MAC");
553
709
 
554
710
  if (hasSig) {
555
711
  if (opts.key == null || opts.cert == null) throw _err("cmp/bad-input", "signature protection requires both opts.key (the private key) and opts.cert (the signer certificate)");
@@ -621,8 +777,11 @@ async function _build(message, opts) {
621
777
  return "unknown message field " + JSON.stringify(k) + " (a message carries only header + body)";
622
778
  });
623
779
  var sigForm = opts.key != null || opts.cert != null, macForm = opts.mac != null;
780
+ var kemForm = opts.kem != null;
781
+ var formCount = (sigForm ? 1 : 0) + (macForm ? 1 : 0) + (kemForm ? 1 : 0);
624
782
  guard.identifier.assertKnownKeys(opts,
625
- sigForm === macForm ? KNOWN_OPTS_KEYS : sigForm ? KNOWN_OPTS_SIG_KEYS : KNOWN_OPTS_MAC_KEYS,
783
+ formCount !== 1 ? KNOWN_OPTS_KEYS
784
+ : sigForm ? KNOWN_OPTS_SIG_KEYS : macForm ? KNOWN_OPTS_MAC_KEYS : KNOWN_OPTS_KEM_KEYS,
626
785
  _err, "cmp/bad-input", function (k) {
627
786
  return "unknown opts field " + JSON.stringify(k) +
628
787
  (macForm && (k === "pss" || k === "digestAlgorithm")
@@ -632,7 +791,7 @@ async function _build(message, opts) {
632
791
  if (message.header == null) throw _err("cmp/bad-input", "message.header is required");
633
792
  if (message.body == null) throw _err("cmp/bad-input", "message.body is required");
634
793
 
635
- var prot = _resolveProtection(opts);
794
+ var prot = _resolveProtection(opts, message);
636
795
  var senderKey = opts.key;
637
796
 
638
797
  var bodyResult = await _encodeBody(message.body, senderKey, opts);
@@ -712,6 +871,13 @@ function _collectExtraCerts(opts, protCertDer) {
712
871
  * - `key` (Buffer|CryptoKey) + `cert` (Buffer): signature protection under the sender key; `cert` is
713
872
  * the signer certificate (its SPKI resolves the algorithm) and is placed in `extraCerts`.
714
873
  * - `mac` ({ secret, salt?, iterationCount?, prf?, keyLength? }): PBMAC1 protection from a shared secret.
874
+ * - `kem` ({ key, ciphertext, kemAlgorithm, kemContext?, transactionID?, len? }): KEM-based MAC
875
+ * protection (RFC 9810 sec. 5.1.3.4), for a sender whose key establishes secrets rather than
876
+ * signing. `key` is the KEM private key as a CryptoKey (what `pki.key.generate` returns), a
877
+ * PKCS#8 DER Buffer / Uint8Array, or a PEM string. `ciphertext` is what the peer encapsulated to
878
+ * it, `kemAlgorithm` names the ML-KEM
879
+ * parameter set it was produced under, and the derived key is bound to `transactionID` (this
880
+ * message's, unless the ciphertext arrived under another) and any `kemContext`.
715
881
  * - `extraCerts` (array of Buffer): additional certificates to carry in `extraCerts [1]`.
716
882
  * - `pem` (boolean|string): return a PEM `CMP` block instead of DER.
717
883
  * - `pss` (boolean) / `digestAlgorithm` (string): signature-protection algorithm options.
@@ -875,9 +1041,12 @@ function wellKnownUrl(base, opts) {
875
1041
  var rawBasePath = _beforeQueryOrFragment(_stripSchemeAuthority(String(base)));
876
1042
  if (rawBasePath !== "" && rawBasePath !== "/") throw _err("cmp/bad-url", "an RFC 8615 well-known URI is authority-rooted; the CMP base URL must have no path component (pass a full operation URL to transfer instead): " + JSON.stringify(String(base)));
877
1043
  var segs = [".well-known", "cmp"];
878
- if (opts.label != null) { segs.push("p"); segs.push(_wellKnownSeg(opts.label, "label")); }
879
- if (opts.operation != null) segs.push(_wellKnownSeg(opts.operation, "operation"));
880
- return u.origin + "/" + segs.join("/");
1044
+ if (opts.label != null) {
1045
+ guard.list.append(segs, "p");
1046
+ guard.list.append(segs, _wellKnownSeg(opts.label, "label"));
1047
+ }
1048
+ if (opts.operation != null) guard.list.append(segs, _wellKnownSeg(opts.operation, "operation"));
1049
+ return u.origin + "/" + intrinsic.join(segs, "/");
881
1050
  }
882
1051
 
883
1052
  /**
@@ -962,4 +1131,8 @@ module.exports = {
962
1131
  },
963
1132
  // @internal
964
1133
  decodeIdpDistributionPoint: decodeIdpDistributionPoint,
1134
+ /** @internal The KEM protection derivation, read by pki.cmp.verify so the two halves of one
1135
+ * key agreement cannot drift apart. */
1136
+ kemSharedKey: kemSharedKey,
1137
+ kemMac: kemMac,
965
1138
  };
@@ -77,6 +77,14 @@ var INFO_OPS = Object.assign(Object.create(null), {
77
77
  rootCaCert: { name: "rootCaCert", requestOid: "rootCaCert", responseOid: "rootCaKeyUpdate", value: "cert", read: "readRootCaKeyUpdate" },
78
78
  certReqTemplate: { name: "certReqTemplate", requestOid: "certReqTemplate", responseOid: "certReqTemplate", value: false, read: "readCertReqTemplate" },
79
79
  crlUpdate: { name: "crlUpdate", requestOid: "crlStatusList", responseOid: "crls", value: "crlStatus", read: "readCrls" },
80
+ /** @internal The KEM ciphertext a peer encapsulated to this client's KEM public key, which the
81
+ * client then decapsulates to protect its own messages (RFC 9810 sec. 5.3.19.18 / 5.1.3.4). The
82
+ * request carries no value; the answer carries the algorithm and the ciphertext. */
83
+ kemCiphertext: { name: "kemCiphertext", requestOid: "kemCiphertextInfo", responseOid: "kemCiphertextInfo", value: false, read: "readKemCiphertextInfo" },
84
+ });
85
+ var KEM_CIPHERTEXT_ALGORITHMS = Object.assign(Object.create(null));
86
+ ["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n) {
87
+ KEM_CIPHERTEXT_ALGORITHMS[oid.byName(n)] = n;
80
88
  });
81
89
  var KNOWN_REVOKE_KEYS = Object.assign(Object.create(null), { certificate: 1, certDetails: 1, reason: 1 });
82
90
  var RSA_ARCS = ["rsaEncryption", "rsaSignatureWithripemd160", "id-TA-RSA-v1-5-SHA-256",
@@ -1341,6 +1349,25 @@ function session(opts) {
1341
1349
  }
1342
1350
 
1343
1351
  function _checkInfoValue(op, value, requestValue) {
1352
+ /** @internal A ciphertext is only useful if the client can decapsulate it, so the answer is held
1353
+ * to a KEM this toolkit establishes secrets under and to a non-empty ciphertext; anything else
1354
+ * could not be turned into a protection key (RFC 9810 sec. 5.3.19.18 / 5.1.3.4). */
1355
+ if (op.name === "kemCiphertext") {
1356
+ if (!KEM_CIPHERTEXT_ALGORITHMS[value.kem.oid]) {
1357
+ throw _err("cmp/bad-info-value", "a KEM ciphertext answer names " +
1358
+ (value.kem.name || value.kem.oid) + ", which this client cannot decapsulate under (ML-KEM-512/768/1024)");
1359
+ }
1360
+ /** @internal An ML-KEM ciphertext is a fixed size, so one of any other length could not be
1361
+ * decapsulated under the algorithm the answer names. Refusing it here reports the authority's
1362
+ * answer as malformed rather than handing back material that cannot be used. */
1363
+ var want = oid.kemParams(value.kem.oid);
1364
+ if (!value.ct || value.ct.length !== want.ct) {
1365
+ throw _err("cmp/bad-info-value", "a KEM ciphertext answer for " +
1366
+ (value.kem.name || value.kem.oid) + " carries " + (value.ct ? value.ct.length : 0) +
1367
+ " bytes, and that algorithm's ciphertext is " + want.ct);
1368
+ }
1369
+ return value;
1370
+ }
1344
1371
  if (op.name === "rootCaCert") {
1345
1372
  if (value.newWithOld == null) throw _err("cmp/bad-info-value", "a rootCaKeyUpdate response must carry newWithOld -- the certificate that lets an entity trusting the OLD root gain trust in the new one (RFC 9483 sec. 4.3.2)");
1346
1373
  return _checkRootCaKeyUpdate(value, requestValue);
package/lib/cmp-verify.js CHANGED
@@ -51,8 +51,10 @@ var _certExtDecoders = pkix.certExtensionDecoders(NS).byOid;
51
51
 
52
52
  var KNOWN_VERIFY_OPTS = intrinsic.assign(intrinsic.create(null), {
53
53
  sharedSecret: 1, signerCert: 1, trustAnchors: 1, intermediates: 1, time: 1,
54
- transactionID: 1, expectRecipNonce: 1, revocationChecker: 1, maxIterations: 1,
54
+ transactionID: 1, expectRecipNonce: 1, revocationChecker: 1, maxIterations: 1, kem: 1,
55
55
  });
56
+ var KNOWN_KEM_VERIFY_KEYS = intrinsic.assign(intrinsic.create(null),
57
+ { sharedSecret: 1, transactionID: 1 });
56
58
 
57
59
  var PRF_HASH = _create(null);
58
60
  PRF_HASH[oid.byName("hmacWithSHA256")] = "SHA-256";
@@ -68,7 +70,7 @@ var PBMAC1_KEYLEN_MAX = 1024;
68
70
  var PRF_HLEN = intrinsic.assign(intrinsic.create(null), { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 });
69
71
 
70
72
  var UNSUPPORTED_MAC_OIDS = _create(null);
71
- ["passwordBasedMac", "dhBasedMac", "kemBasedMac"].forEach(function (n) {
73
+ ["passwordBasedMac", "dhBasedMac"].forEach(function (n) {
72
74
  var o = oid.byName(n);
73
75
  if (o) UNSUPPORTED_MAC_OIDS[o] = n;
74
76
  });
@@ -375,6 +377,66 @@ function _keyUsageAllowsSigning(parsed) {
375
377
  return true;
376
378
  }
377
379
 
380
+ /** @internal The verifying half of RFC 9810 sec. 5.1.3.4. The peer that encapsulated holds the same
381
+ * shared secret, and derives the key from it through the derivation `pki.cmp.build` uses, so the two
382
+ * halves cannot disagree about the context. The transaction identifier the context binds to is the one
383
+ * from the message that carried the ciphertext, which the caller names when it is not this message's. */
384
+ async function _verifyKem(m, protectedPart, protectionAlg, protection, opts) {
385
+ var kemOpts = opts.kem;
386
+ if (!kemOpts) {
387
+ throw _err("cmp/bad-input", "a KEM-protected message requires opts.kem { sharedSecret, transactionID? }");
388
+ }
389
+ if (!_nonEmptySecret(kemOpts.sharedSecret)) {
390
+ throw _err("cmp/bad-input", "opts.kem.sharedSecret is the KEM shared secret the peer encapsulated");
391
+ }
392
+ if (protectionAlg.parameters === null) {
393
+ return _fail(m, "kem", protectionAlg, "cmp/protection-failed", "the KEM protectionAlg carries no KemBMParameter (RFC 9810 sec. 5.1.3.4)");
394
+ }
395
+ var params;
396
+ try { params = cmp.readKemBMParameter(protectionAlg.parameters); }
397
+ catch (e) {
398
+ return _fail(m, "kem", protectionAlg, e instanceof CmpError ? e.code : "cmp/protection-failed",
399
+ "the KemBMParameter did not decode: " + ((e && e.message) || e));
400
+ }
401
+ if (params.kdf.oid !== oid.byName("hkdfWithSha256")) {
402
+ return _fail(m, "kem", protectionAlg, "cmp/unsupported-algorithm",
403
+ "unsupported KEM key-derivation " + _stringify(params.kdf.name || params.kdf.oid) + " (HKDF-SHA256 only)");
404
+ }
405
+ if (params.mac.oid !== oid.byName("hmacWithSHA256")) {
406
+ return _fail(m, "kem", protectionAlg, "cmp/unsupported-algorithm",
407
+ "unsupported KEM message-authentication algorithm " + _stringify(params.mac.name || params.mac.oid) + " (HMAC-SHA256 only)");
408
+ }
409
+ /** @internal The sender chooses `len`, so a peer could name a key short enough to search. The
410
+ * message is refused rather than verified under a key that carries no strength. */
411
+ if (params.len < constants.LIMITS.CMP_KEM_MIN_KEY_BYTES) {
412
+ return _fail(m, "kem", protectionAlg, "cmp/protection-failed",
413
+ "the KemBMParameter names a " + params.len + "-byte protection key, and a key shorter than " +
414
+ constants.LIMITS.CMP_KEM_MIN_KEY_BYTES + " bytes would not carry the strength the MAC is meant to have");
415
+ }
416
+ var txid = kemOpts.transactionID != null
417
+ ? guard.bytes.view(kemOpts.transactionID, CmpError, "cmp/bad-input", "opts.kem.transactionID")
418
+ : m.header.transactionID;
419
+ if (txid == null) {
420
+ return _fail(m, "kem", protectionAlg, "cmp/protection-failed",
421
+ "a KEM-derived key is bound to the transaction identifier of the message that carried the ciphertext, and neither this message nor opts.kem names one");
422
+ }
423
+ var secret = guard.bytes.view(kemOpts.sharedSecret, CmpError, "cmp/bad-input", "opts.kem.sharedSecret");
424
+ var ssk = null;
425
+ try {
426
+ ssk = await cmpBuild.kemSharedKey(secret, params.len, txid, params.kemContext);
427
+ var computed = await cmpBuild.kemMac(ssk, protectedPart);
428
+ if (!guard.crypto.constantTimeEqual(computed, protection.bytes)) {
429
+ return _fail(m, "kem", protectionAlg, "cmp/protection-failed",
430
+ "the KEM-based MAC does not verify (a different shared secret, a different derivation context, or a tampered ProtectedPart)");
431
+ }
432
+ } finally {
433
+ if (ssk) guard.secret.zeroize(ssk, CmpError, "cmp/bad-input", "the KEM-derived protection key");
434
+ }
435
+ var khc = _headerChecks(m, opts);
436
+ if (khc) return _fail(m, "kem", protectionAlg, khc.code, khc.reason);
437
+ return _ok(m, "kem", protectionAlg, true, null);
438
+ }
439
+
378
440
  async function _verifyMac(m, protectedPart, protectionAlg, protection, opts) {
379
441
  if (protectionAlg.parameters === null) {
380
442
  return _fail(m, "mac", protectionAlg, "cmp/protection-failed", "the PBMAC1 protectionAlg carries no PBMAC1-params (RFC 9579 sec. 4)");
@@ -561,15 +623,36 @@ function _assertEchoBytes(v, name) {
561
623
  throw _err("cmp/bad-input", "opts." + name + " must be a Buffer / Uint8Array");
562
624
  }
563
625
 
564
- function _fixSecret(v, made) {
626
+ /** @internal A shared secret, taken once and registered for wiping. Every byte-source shape is
627
+ * accepted, not only the ones a caller happens to have converted: a KEM encapsulation hands back an
628
+ * ArrayBuffer, and a secret read from a larger allocation arrives as a view over it. */
629
+ function _fixSecret(v, made, label) {
565
630
  if (typeof v === "string") {
566
631
  var fromString = _bufferFrom(v, "utf8");
567
632
  _append(made, fromString);
568
633
  return fromString;
569
634
  }
570
- var copy = _fixByteish(v, "sharedSecret");
571
- if (util.types.isUint8Array(copy)) _append(made, copy);
572
- return copy;
635
+ if (v != null && guard.bytes.isByteSource(v)) {
636
+ var snap = guard.bytes.snapshotSource(v, CmpError, "cmp/bad-input", "opts." + (label || "sharedSecret"));
637
+ _append(made, snap);
638
+ return snap;
639
+ }
640
+ return v;
641
+ }
642
+
643
+ /** @internal The KEM verification material, taken once at the door like every other caller value: the
644
+ * shared secret is copied and registered for wiping, and the transaction identifier is snapshot, so
645
+ * neither can answer differently when the derivation reads it. */
646
+ function _fixKem(v, made) {
647
+ if (v == null) return null;
648
+ if (typeof v !== "object" || guard.bytes.isByteSource(v)) {
649
+ throw _err("cmp/bad-input", "opts.kem must be an object { sharedSecret, transactionID? }");
650
+ }
651
+ guard.identifier.assertKnownKeys(v, KNOWN_KEM_VERIFY_KEYS, _err, "cmp/bad-input", "unknown opts.kem field ");
652
+ return {
653
+ sharedSecret: _fixSecret(v.sharedSecret, made, "kem.sharedSecret"),
654
+ transactionID: _fixByteish(v.transactionID, "kem.transactionID"),
655
+ };
573
656
  }
574
657
 
575
658
  function _fixCertList(v, label) {
@@ -609,6 +692,7 @@ function _fixVerifyOptions(opts, made) {
609
692
  expectRecipNonce: _fixByteish(opts.expectRecipNonce, "expectRecipNonce"),
610
693
  revocationChecker: opts.revocationChecker,
611
694
  maxIterations: opts.maxIterations,
695
+ kem: _fixKem(opts.kem, made),
612
696
  };
613
697
 
614
698
  _assertEchoBytes(f.transactionID, "transactionID");
@@ -634,9 +718,17 @@ async function _verify(message, opts) {
634
718
  }
635
719
 
636
720
  var isMac = protectionAlg.oid === oid.byName("pbmac1");
721
+ var isKem = protectionAlg.oid === oid.byName("kemBasedMac");
637
722
  var hasSigCred = opts.signerCert != null || opts.trustAnchors != null;
638
723
  var hasSecret = opts.sharedSecret != null;
639
724
 
725
+ if (isKem) {
726
+ if (hasSigCred) throw _err("cmp/bad-input", "a KEM-protected message takes opts.kem, not signerCert/trustAnchors");
727
+ if (hasSecret) throw _err("cmp/bad-input", "a KEM-protected message takes opts.kem.sharedSecret, not opts.sharedSecret");
728
+ return _verifyKem(m, protectedPart, protectionAlg, protection, opts);
729
+ }
730
+ if (opts.kem != null) throw _err("cmp/bad-input", "opts.kem verifies a KEM-based MAC, and this message is protected another way");
731
+
640
732
  if (isMac) {
641
733
  if (hasSigCred) throw _err("cmp/bad-input", "a MAC-protected message takes opts.sharedSecret, not signerCert/trustAnchors");
642
734
  if (!_nonEmptySecret(opts.sharedSecret)) throw _err("cmp/bad-input", "a PBMAC1-protected message requires a non-empty opts.sharedSecret");
@@ -683,6 +775,10 @@ async function _verify(message, opts) {
683
775
  *
684
776
  * @opts
685
777
  * - `sharedSecret` (string|Buffer) -- the PBMAC1 secret; REQUIRED for a MAC-protected message (UTF-8).
778
+ * - `kem` ({ sharedSecret, transactionID? }) -- REQUIRED for a KEM-protected message (RFC 9810
779
+ * sec. 5.1.3.4). `sharedSecret` is what this side encapsulated to the sender's KEM public key, and
780
+ * `transactionID` names the message that carried the ciphertext when it was not this one. The
781
+ * verdict's `protectionType` is `kem`.
686
782
  * - `signerCert` (Buffer|PEM) -- the expected signature signer certificate (else resolved from
687
783
  * `extraCerts` by `senderKID` or, per RFC 9483 sec. 3.3, `extraCerts[0]`).
688
784
  * - `trustAnchors` (Buffer|Buffer[]|PEM) -- when present the signer certificate is FULLY path-validated
@@ -160,7 +160,7 @@ function _kekIdMatches(ri, kekId) {
160
160
  return Buffer.compare(id, guard.bytes.view(kekId, CmsError, "cms/bad-input", "kekId")) === 0;
161
161
  }
162
162
 
163
- var _passThrough = intrinsic.assign(intrinsic.create(null), { "cms/unsupported-algorithm": 1, "cms/unsupported-recipient-type": 1, "cms/bad-input": 1, "cms/iteration-limit": 1, "cms/missing-key-derivation": 1, "cms/no-encrypted-content": 1 });
163
+ var _passThrough = intrinsic.assign(intrinsic.create(null), { "cms/unsupported-algorithm": 1, "cms/unsupported-recipient-type": 1, "cms/bad-input": 1, "cms/iteration-limit": 1, "cms/missing-key-derivation": 1, "cms/no-encrypted-content": 1, "cms/too-many-recipient-keys": 1 });
164
164
 
165
165
  async function _acquireCek(ri, km, opts) {
166
166
  try {
@@ -198,6 +198,29 @@ async function _ktriCek(ri, km) {
198
198
  }
199
199
  }
200
200
 
201
+ /** @internal One KeyAgreeRecipientInfo can carry an encrypted key for several recipients under one
202
+ * originator and one key-derivation, and a caller decrypting with a private key alone has no identifier
203
+ * to select its own entry by. The key-encryption key is derived once and every entry is offered to it;
204
+ * AES key wrap's integrity check settles which entry belongs to this key, so an entry that opens is one
205
+ * this key was addressed by. The attempt count is bounded because the list is attacker-supplied. */
206
+ async function _kariUnwrapAny(kek, reks) {
207
+ var list = reks || [];
208
+ /** @internal The list arrives with the message, so the number of unwraps it can demand is bounded.
209
+ * The bound caps the attempts rather than refusing the list, so an entry within it still opens the
210
+ * message it always opened. */
211
+ var tried = list.length < C.LIMITS.CMS_MAX_KARI_ENCRYPTED_KEYS
212
+ ? list.length : C.LIMITS.CMS_MAX_KARI_ENCRYPTED_KEYS;
213
+ for (var i = 0; i < tried; i++) {
214
+ try { return await _aesKwUnwrap(kek, list[i].encryptedKey); }
215
+ catch (_e) { continue; } // allow:swallow-unverified this entry is not addressed to this key; a later one may be
216
+ }
217
+ if (list.length > tried) {
218
+ throw _err("cms/too-many-recipient-keys", "a KeyAgreeRecipientInfo names " + list.length +
219
+ " encrypted keys and none of the first " + tried + " opened; at most " + tried + " are tried");
220
+ }
221
+ throw _fail();
222
+ }
223
+
201
224
  async function _kariCek(ri, km) {
202
225
  var k = _normKeyDer(km.key);
203
226
  var kek, z, mz;
@@ -207,7 +230,7 @@ async function _kariCek(ri, km) {
207
230
  var wrapAlg = _kariWrap(kea);
208
231
  var scheme = kea.oid;
209
232
  var origSpki = _originatorSpki(ri.originator);
210
- var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || ri.recipientEncryptedKeys[0];
233
+ var rek = (km.cert != null && _kariRekFor(ri, x509.parse(_normCertDer(km.cert)))) || null;
211
234
  var kekBytes = WRAP_KEK_LENGTHS[wrapAlg.oid];
212
235
  if (!kekBytes) throw _err("cms/unsupported-algorithm", "unsupported kari key-wrap");
213
236
  var ukm = ri.ukm || null;
@@ -222,7 +245,8 @@ async function _kariCek(ri, km) {
222
245
  } else {
223
246
  var origAlg = asn1.decode(origSpki).children[0];
224
247
  var origHasParams = origAlg.children.length > 1;
225
- var curveOid = (km.cert != null && _ecCurveFromCert(km.cert)) || (origHasParams ? asn1.read.oid(origAlg.children[1]) : null);
248
+ var curveOid = (km.cert != null && _ecCurveFromCert(km.cert)) ||
249
+ (origHasParams ? asn1.read.oid(origAlg.children[1]) : _ecCurveFromPrivateKey(keyDer));
226
250
  var curve = curveOid ? CURVE[curveOid] : null;
227
251
  if (!curve) throw _err("cms/unsupported-algorithm", "unsupported or missing originator EC curve");
228
252
  var origSpkiFull = origHasParams ? origSpki : _withEcCurveParams(origSpki, curveOid);
@@ -232,7 +256,15 @@ async function _kariCek(ri, km) {
232
256
  var zKey = await subtle.importKey("raw", z, { name: "X963KDF" }, false, ["deriveBits"]);
233
257
  kek = Buffer.from(await subtle.deriveBits({ name: "X963KDF", hash: _x963Hash(scheme), info: _eccSharedInfo(wrapAlg.name, ukm, kekBytes) }, zKey, kekBytes * 8));
234
258
  }
235
- return await _aesKwUnwrap(kek, rek.encryptedKey);
259
+ /** @internal A caller that supplied a certificate selects its entry by recipient identifier, which
260
+ * is what that certificate is for. Offering every entry is for the caller that has only a key, and
261
+ * so has no identifier to select by. */
262
+ if (km.cert != null) {
263
+ var chosen = rek || ri.recipientEncryptedKeys[0];
264
+ if (!chosen) throw _fail();
265
+ return await _aesKwUnwrap(kek, chosen.encryptedKey);
266
+ }
267
+ return await _kariUnwrapAny(kek, ri.recipientEncryptedKeys);
236
268
  } finally {
237
269
  guard.secret.zeroizeAll([z, mz, kek], CmsError, "cms/bad-input", "the key-agreement shared secret");
238
270
  _releaseKeyDer(k);
@@ -507,6 +539,19 @@ function _ecCurveFromCert(cert) {
507
539
  if (spki.algorithm.oid !== O("ecPublicKey") || spki.algorithm.parameters == null) return null;
508
540
  return asn1.read.oid(asn1.decode(spki.algorithm.parameters));
509
541
  }
542
+ /** @internal The curve of the recipient's own private key. RFC 5753 sec. 7.1 lets an originator omit its
543
+ * curve parameters because the recipient already knows the curve; a recipient decrypting with a key and
544
+ * no certificate has that knowledge in the key itself, and nowhere else. */
545
+ function _ecCurveFromPrivateKey(keyDer) {
546
+ var spki;
547
+ try {
548
+ spki = nodeCrypto.createPublicKey({ key: keyDer, format: "der", type: "pkcs8" })
549
+ .export({ format: "der", type: "spki" });
550
+ } catch (_e) { return null; } // allow:swallow-unverified a key this runtime cannot read names no curve
551
+ var alg = asn1.decode(spki).children[0];
552
+ if (asn1.read.oid(alg.children[0]) !== O("ecPublicKey") || alg.children.length < 2) return null;
553
+ return asn1.read.oid(alg.children[1]);
554
+ }
510
555
  function _withEcCurveParams(spki, curveOid) {
511
556
  var node = asn1.decode(spki);
512
557
  var alg = node.children[0];
package/lib/constants.js CHANGED
@@ -124,6 +124,9 @@ var LIMITS = Object.assign(Object.create(null), {
124
124
  CBOR_MAX_ITEMS: 1000000,
125
125
  SCT_MAX_BYTES: BYTES.kib(64) + 1,
126
126
  SCT_MAX_COUNT: 256,
127
+ CMS_MAX_KARI_ENCRYPTED_KEYS: 64,
128
+ CMP_KEM_MAX_KEY_BYTES: 1024,
129
+ CMP_KEM_MIN_KEY_BYTES: 20,
127
130
  MERKLE_MAX_PROOF_NODES: 65,
128
131
  PROXY_MAX_TLS_ANCHORS: 256,
129
132
  PATH_MAX_CERTS: 100,
@@ -4,6 +4,7 @@
4
4
  // @internal
5
5
 
6
6
  var _utilTypes = require("util").types;
7
+ var _nodeCrypto = require("node:crypto");
7
8
 
8
9
  var _call = Function.prototype.call;
9
10
  var _bind = Function.prototype.bind;
@@ -19,6 +20,10 @@ var _bufferByteLength = Buffer.byteLength;
19
20
  var _bufferCompare = Buffer.compare;
20
21
  var _bufferEquals = uncurry(Buffer.prototype.equals);
21
22
  var _bufferToString = uncurry(Buffer.prototype.toString);
23
+ /** @internal Key identity, taken off the KeyObject prototype at load. A caller that replaced this
24
+ * method could make two different keys answer as one, which is the question a proof of possession
25
+ * turns on. */
26
+ var _keyEquals = uncurry(_nodeCrypto.KeyObject.prototype.equals);
22
27
 
23
28
  var _isView = ArrayBuffer.isView;
24
29
  var _isInteger = Number.isInteger;
@@ -146,6 +151,7 @@ module.exports = Object.freeze({
146
151
  byteLength: _bufferByteLength,
147
152
  compare: _bufferCompare,
148
153
  bufferEquals: _bufferEquals,
154
+ keyEquals: _keyEquals,
149
155
  isView: _isView,
150
156
  isInteger: _isInteger,
151
157
  isSafeInteger: _isSafeInteger,