@blamejs/pki 0.8.31 → 0.8.49

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/README.md +66 -15
  3. package/bin/pki.js +716 -30
  4. package/examples/issue-a-certificate.js +90 -0
  5. package/examples/post-quantum.js +66 -0
  6. package/examples/read-a-certificate.js +56 -0
  7. package/examples/revoke-and-check.js +86 -0
  8. package/examples/sign-and-verify-cms.js +63 -0
  9. package/index.js +18 -5
  10. package/lib/acme.js +223 -74
  11. package/lib/alt-sig.js +441 -0
  12. package/lib/asn1-der.js +48 -25
  13. package/lib/attrcert-sign.js +27 -19
  14. package/lib/cbor-det.js +24 -14
  15. package/lib/cmc-build.js +9 -2
  16. package/lib/cmc-verify.js +14 -3
  17. package/lib/cmp-build.js +17 -5
  18. package/lib/cmp-session.js +24 -11
  19. package/lib/cmp-verify.js +28 -19
  20. package/lib/cms-compress.js +3 -1
  21. package/lib/cms-decrypt.js +102 -7
  22. package/lib/cms-encrypt.js +59 -7
  23. package/lib/cms-sign.js +31 -9
  24. package/lib/cms-verify.js +22 -18
  25. package/lib/composite-kem.js +30 -18
  26. package/lib/composite-sig.js +29 -14
  27. package/lib/constants.js +26 -0
  28. package/lib/crl-sign.js +111 -19
  29. package/lib/crmf-sign.js +167 -15
  30. package/lib/csr-sign.js +117 -6
  31. package/lib/ct.js +1008 -58
  32. package/lib/est.js +29 -16
  33. package/lib/framework-error.js +12 -0
  34. package/lib/guard-async.js +1 -1
  35. package/lib/guard-bytes.js +103 -20
  36. package/lib/guard-crypto.js +205 -3
  37. package/lib/guard-encoding.js +6 -1
  38. package/lib/guard-identifier.js +55 -8
  39. package/lib/guard-intrinsic.js +43 -0
  40. package/lib/guard-json.js +122 -9
  41. package/lib/guard-limits.js +12 -1
  42. package/lib/guard-list.js +63 -8
  43. package/lib/guard-name.js +167 -20
  44. package/lib/guard-range.js +43 -5
  45. package/lib/guard-text.js +41 -0
  46. package/lib/guard-verdict.js +39 -8
  47. package/lib/hpke.js +688 -42
  48. package/lib/http-transport.js +156 -4
  49. package/lib/identity-match.js +4 -4
  50. package/lib/inspect.js +33 -6
  51. package/lib/jose.js +93 -25
  52. package/lib/key.js +18 -13
  53. package/lib/lint.js +165 -1
  54. package/lib/merkle.js +233 -27
  55. package/lib/ocsp.js +27 -14
  56. package/lib/oid.js +14 -4
  57. package/lib/path-validate.js +143 -61
  58. package/lib/pkcs12-build.js +7 -2
  59. package/lib/pki-build.js +31 -7
  60. package/lib/possession.js +575 -0
  61. package/lib/related-cert.js +708 -0
  62. package/lib/revocation-fetch.js +6 -6
  63. package/lib/scep.js +15 -5
  64. package/lib/schema-c509.js +5 -2
  65. package/lib/schema-cmc.js +1 -1
  66. package/lib/schema-cmp.js +8 -8
  67. package/lib/schema-cms.js +2 -15
  68. package/lib/schema-csr.js +69 -0
  69. package/lib/schema-engine.js +10 -3
  70. package/lib/schema-pkix.js +228 -2
  71. package/lib/schema-tsp.js +7 -6
  72. package/lib/sign-scheme.js +22 -9
  73. package/lib/sigstore.js +684 -168
  74. package/lib/sleep.js +8 -1
  75. package/lib/smime.js +22 -1
  76. package/lib/tlog.js +1235 -0
  77. package/lib/tls-cert-compress.js +25 -8
  78. package/lib/tsp-sign.js +120 -35
  79. package/lib/tuf.js +1395 -0
  80. package/lib/validator-tpm.js +4 -1
  81. package/lib/verify-seam.js +7 -1
  82. package/lib/webauthn-mds.js +41 -4
  83. package/lib/webauthn.js +43 -25
  84. package/lib/webcrypto.js +48 -46
  85. package/lib/x509-sign.js +225 -19
  86. package/package.json +2 -1
  87. package/sbom.cdx.json +6 -6
package/lib/hpke.js CHANGED
@@ -8,17 +8,27 @@
8
8
  * @fullname HPKE (Hybrid Public Key Encryption, RFC 9180)
9
9
  * @intro Hybrid Public Key Encryption (RFC 9180): the standard construction
10
10
  * behind TLS Encrypted Client Hello, MLS, and OHTTP that turns a recipient KEM
11
- * public key into an encapsulated key plus an AEAD-encrypting context. Two KEM
12
- * families run the RFC 9180 key schedule: the DHKEM suites P-256, P-521, X25519,
13
- * and X448 in all four modes (base / psk / auth / auth-psk), proven against the
14
- * RFC 9180 Appendix A known-answer vectors, and the post-quantum ML-KEM-512 /
11
+ * public key into an encapsulated key plus an AEAD-encrypting context. Three KEM
12
+ * families run the RFC 9180 key schedule: the DHKEM suites P-256, P-384, P-521,
13
+ * X25519, and X448 in all four modes (base / psk / auth / auth-psk), proven against
14
+ * the RFC 9180 Appendix A known-answer vectors, the post-quantum ML-KEM-512 /
15
15
  * -768 / -1024 KEMs (IANA code points 0x0040 to 0x0042, draft-ietf-hpke-pq) in
16
- * the base and psk modes, proven against that draft's Appendix A vectors. The
17
- * KDFs are HKDF-SHA256 / HKDF-SHA384 / HKDF-SHA512; the AEADs are AES-128-GCM,
18
- * AES-256-GCM, ChaCha20Poly1305, and export-only. DHKEM(P-384) is RFC-registered
19
- * but no known-answer vector pairs it with an HKDF key schedule, so a request
20
- * for it fails closed. The PQ/T hybrid KEMs and the SHAKE-based KDFs of the HPKE
21
- * revision are a data-row extension once their specifications stabilize. Pure
16
+ * the base and psk modes, proven against that draft's Appendix A vectors, and the
17
+ * PQ/T hybrid KEMs MLKEM768-P256 (0x0050), MLKEM1024-P384 (0x0051) and
18
+ * MLKEM768-X25519 (0x647a, X-Wing) in the base and psk modes, each pairing an
19
+ * ML-KEM with a nominal group under the CFRG C2PRI combiner and proven against
20
+ * the draft-irtf-cfrg-concrete-hybrid-kems Appendix B vectors. The KDFs are
21
+ * HKDF-SHA256 / HKDF-SHA384 / HKDF-SHA512, which run the two-stage key schedule,
22
+ * and SHAKE128 (0x0010) / SHAKE256 (0x0011) of draft-ietf-hpke-pq, which are
23
+ * single-stage and run the one-stage key schedule of draft-ietf-hpke-hpke,
24
+ * proven against Appendix A.7, A.8, A.11 and A.12 of the first draft. On a
25
+ * single-stage KDF the psk, psk_id and info inputs are capped at 65535 bytes and
26
+ * an export is capped at the same, each length being encoded in two bytes.
27
+ * TurboSHAKE128 (0x0012) and TurboSHAKE256 (0x0013) are registered in the same
28
+ * table and not offered, because no released OpenSSL exposes the XOF; a request
29
+ * for either fails closed. The AEADs are AES-128-GCM, AES-256-GCM,
30
+ * ChaCha20Poly1305, and export-only. deriveKeyPair, generateKeyPair, encap and
31
+ * decap reach a KEM on its own, for a caller driving its own key schedule. Pure
22
32
  * composition over node:crypto: no ASN.1, no schema engine.
23
33
  * The RFC 9180 sec. 7 registry code points live in pki.hpke.suites
24
34
  * (KEM / KDF / AEAD / MODE), passed to the setup functions to select a suite.
@@ -27,6 +37,9 @@
27
37
  */
28
38
 
29
39
  var nodeCrypto = require("crypto");
40
+ /** @internal Bound at load, like every other operation this module decides with: a replacement installed
41
+ * later would otherwise choose the bytes a fresh key pair is generated from. */
42
+ var _randomBytes = nodeCrypto.randomBytes;
30
43
  var frameworkError = require("./framework-error");
31
44
  var guard = require("./guard-all");
32
45
  var intrinsic = require("./guard-intrinsic");
@@ -34,6 +47,7 @@ var oid = require("./oid");
34
47
  var _hasOwn = intrinsic.hasOwn;
35
48
  var _isBuffer = intrinsic.isBuffer, _bufferFrom = intrinsic.bufferFrom, _bufferAlloc = intrinsic.bufferAlloc;
36
49
  var _bufferConcat = intrinsic.bufferConcat, _bufferEquals = intrinsic.bufferEquals, _compare = intrinsic.compare;
50
+ var _push = intrinsic.push;
37
51
  var _subarray = intrinsic.subarray;
38
52
 
39
53
  var HpkeError = frameworkError.HpkeError;
@@ -44,20 +58,55 @@ function i2osp(n, len) {
44
58
  var b = Buffer.alloc(len);
45
59
  var v = BigInt(n);
46
60
  for (var i = len - 1; i >= 0; i--) { b[i] = Number(v & 0xffn); v >>= 8n; }
61
+ /** @internal A value wider than `len` bytes leaves a non-zero residue here. Returning the truncated
62
+ * encoding would let two ends of an exchange derive different secrets from inputs each accepted. */
63
+ if (v !== 0n) throw _err("hpke/input-length", "the value " + n + " does not fit in " + len + " bytes");
47
64
  return b;
48
65
  }
49
- function concat(arr) { return Buffer.concat(arr); }
66
+ /** @internal draft-ietf-hpke-hpke-05 sec. 3 lengthPrefixed(x): a two-byte big-endian length, then the
67
+ * value. The limit is the width of that prefix, which sec. 7.2.1 states as a MUST for the psk, psk_id
68
+ * and info inputs of a single-stage KDF.
69
+ * It appends onto `parts` instead of returning a buffer, for two reasons. A returned buffer would be a
70
+ * second copy of a secret, which the caller would then have to wipe alongside the concatenation it ends
71
+ * up in; here the secret is copied once, when `parts` is concatenated. And every length is checked while
72
+ * the parts are collected, before any of them is joined, so a refused input leaves nothing allocated. */
73
+ var MAX_PREFIXED = 0xffff;
74
+ function _pushPrefixed(parts, b, what) {
75
+ if (b.length > MAX_PREFIXED) {
76
+ throw _err("hpke/input-length", what + " is " + b.length + " bytes; a single-stage KDF accepts at " +
77
+ "most " + MAX_PREFIXED + " (draft-ietf-hpke-hpke-05 sec. 7.2.1)");
78
+ }
79
+ _push(parts, i2osp(b.length, 2), b);
80
+ return parts;
81
+ }
82
+ /** @internal Through the capture, which is the whole point of this helper existing: every
83
+ * concatenation in the module goes through it, including the hybrid combiner's
84
+ * `[ssPQ, ssT, ctT, ekT, label]`. Reaching `Buffer.concat` live, a replacement that answered a
85
+ * five-part array with a constant made both sides hash the same public value instead of combining the
86
+ * two shared secrets, and the HPKE keys derived from it were then predictable. */
87
+ function concat(arr) { return _bufferConcat(arr); }
50
88
  function xor(a, b) { var o = Buffer.alloc(a.length); for (var i = 0; i < a.length; i++) o[i] = a[i] ^ b[i]; return o; }
51
89
 
52
90
  var HPKE_V1 = Buffer.from("HPKE-v1", "ascii");
91
+ var _EMPTY = _bufferAlloc(0);
53
92
  function L(s) { return Buffer.from(s, "ascii"); }
54
93
 
55
94
 
95
+ /** @internal A row carries `hash` for a two-stage KDF (Extract then Expand, so Nh is the Extract width)
96
+ * or `xof` for a single-stage one (a lone Derive, so Nh comes from the KDF's definition:
97
+ * draft-ietf-hpke-hpke-05 sec. 4.2). The two are exclusive and which one is present selects the key
98
+ * schedule and the export, so a row cannot reach the wrong one by omitting a flag.
99
+ * draft-ietf-hpke-pq-05 sec. 5 also registers TurboSHAKE128 (0x0012) and TurboSHAKE256 (0x0013). They
100
+ * are absent because no released OpenSSL exposes the XOF; `node:crypto` answers
101
+ * "Digest method not supported" for both on OpenSSL 3.5.8. Add the rows when it does. */
56
102
  var KDFS = intrinsic.assign(intrinsic.create(null), {
57
103
  0x0001: { hash: "sha256", Nh: 32 },
58
104
  0x0002: { hash: "sha384", Nh: 48 },
59
105
  0x0003: { hash: "sha512", Nh: 64 },
106
+ 0x0010: { xof: "shake128", Nh: 32 },
107
+ 0x0011: { xof: "shake256", Nh: 64 },
60
108
  });
109
+ function _isOneStage(kdf) { return typeof kdf.xof === "string"; }
61
110
  var AEADS = intrinsic.assign(intrinsic.create(null), {
62
111
  0x0001: { cipher: "aes-128-gcm", Nk: 16, Nn: 12, Nt: 16 },
63
112
  0x0002: { cipher: "aes-256-gcm", Nk: 32, Nn: 12, Nt: 16 },
@@ -68,14 +117,47 @@ function _mlKemRow(name, oidName, node) {
68
117
  var p = oid.kemParams(oidName);
69
118
  return { kind: "mlkem", name: name, node: node, Nsecret: p.ss, Npub: p.ek, Nenc: p.ct, Nsk: 64 };
70
119
  }
120
+ /** @internal A PQ/T hybrid KEM of the CG framework (draft-irtf-cfrg-hybrid-kems sec. 5.1.1): an ML-KEM
121
+ * alongside a nominal group, both key pairs expanded from ONE seed, which is the decapsulation key.
122
+ * `group` names the traditional half; `groupNseed` is the seed slice it takes, which for P-256 is 128
123
+ * because RandomScalar rejection-samples up to four 32-byte chunks and not 32 as the scalar width
124
+ * alone would suggest. `label` separates one combination from another and is registered by the draft. */
125
+ var NOMINAL_GROUPS = intrinsic.assign(intrinsic.create(null), {
126
+ /** @internal P-256's Nseed is 128, four times its scalar width, because RandomScalar rejection-samples
127
+ * 32-byte chunks and the draft budgets four attempts. No published vector reaches the second attempt:
128
+ * a first chunk at or above the order has probability about 2^-32, and because SHAKE is an XOF the
129
+ * first 96 bytes of a 192-byte squeeze are the same bytes, so shortening Nseed to 32 changes nothing
130
+ * observable on the Appendix B seed. The width is therefore asserted directly in the vectors rather
131
+ * than inferred from an output. P-384's Nseed equals its scalar width, so it has one attempt and
132
+ * cannot reject at all. */
133
+ "P-256": { kind: "ec", curve: "P-256", nodeCurve: "prime256v1", Nseed: 128, Nscalar: 32, Nelem: 65, Nss: 32 },
134
+ "P-384": { kind: "ec", curve: "P-384", nodeCurve: "secp384r1", Nseed: 48, Nscalar: 48, Nelem: 97, Nss: 48 },
135
+ "X25519": { kind: "okp", curve: "X25519", Nseed: 32, Nscalar: 32, Nelem: 32, Nss: 32 },
136
+ });
137
+ function _hybridRow(name, pqName, pqNode, groupName, label) {
138
+ var pq = _mlKemRow(pqName, "id-" + pqNode, pqNode);
139
+ var g = NOMINAL_GROUPS[groupName];
140
+ return {
141
+ kind: "hybrid", name: name, pq: pq, groupName: groupName, group: g,
142
+ label: _bufferFrom(label, "latin1"),
143
+ /** @internal The seed is the decapsulation key, so Nsk is the seed width and not a scalar width. */
144
+ Nsecret: 32, Npub: pq.Npub + g.Nelem, Nenc: pq.Nenc + g.Nelem, Nsk: 32,
145
+ };
146
+ }
71
147
  var KEMS = intrinsic.assign(intrinsic.create(null), {
72
148
  0x0010: { kind: "ec", name: "P-256", curve: "P-256", nodeCurve: "prime256v1", kdf: 0x0001, Nsecret: 32, Npub: 65, Nsk: 32 },
149
+ 0x0011: { kind: "ec", name: "P-384", curve: "P-384", nodeCurve: "secp384r1", kdf: 0x0002, Nsecret: 48, Npub: 97, Nsk: 48 },
73
150
  0x0012: { kind: "ec", name: "P-521", curve: "P-521", nodeCurve: "secp521r1", kdf: 0x0003, Nsecret: 64, Npub: 133, Nsk: 66 },
74
151
  0x0020: { kind: "okp", name: "X25519", curve: "X25519", kdf: 0x0001, Nsecret: 32, Npub: 32, Nsk: 32 },
75
152
  0x0021: { kind: "okp", name: "X448", curve: "X448", kdf: 0x0003, Nsecret: 64, Npub: 56, Nsk: 56 },
76
153
  0x0040: _mlKemRow("ML-KEM-512", "id-ml-kem-512", "ml-kem-512"),
77
154
  0x0041: _mlKemRow("ML-KEM-768", "id-ml-kem-768", "ml-kem-768"),
78
155
  0x0042: _mlKemRow("ML-KEM-1024", "id-ml-kem-1024", "ml-kem-1024"),
156
+ 0x0050: _hybridRow("MLKEM768-P256", "ML-KEM-768", "ml-kem-768", "P-256", "MLKEM768-P256"),
157
+ 0x0051: _hybridRow("MLKEM1024-P384", "ML-KEM-1024", "ml-kem-1024", "P-384", "MLKEM1024-P384"),
158
+ /** @internal The label is X-Wing's own, because X-Wing is what this suite became when it was folded
159
+ * into draft-irtf-cfrg-concrete-hybrid-kems. The id is not adjacent to the other two. */
160
+ 0x647a: _hybridRow("MLKEM768-X25519", "ML-KEM-768", "ml-kem-768", "X25519", "\\.//^\\"),
79
161
  });
80
162
 
81
163
  var MODE_BASE = 0x00, MODE_PSK = 0x01, MODE_AUTH = 0x02, MODE_AUTH_PSK = 0x03;
@@ -88,18 +170,25 @@ function _aead(id) { var s = AEADS[guard.text.keyOf(id)]; if (!s) throw _err("hp
88
170
 
89
171
  function _extract(hash, Nh, salt, ikm) {
90
172
  var key = (salt && salt.length) ? salt : Buffer.alloc(Nh);
91
- return nodeCrypto.createHmac(hash, key).update(ikm).digest();
173
+ return guard.crypto.hmac(hash, key, ikm);
92
174
  }
93
- function _expand(hash, Nh, prk, info, len) {
175
+ /** @internal A two-stage Expand yields at most 255 blocks (RFC 5869 sec. 2.3). The check has two callers
176
+ * because `_labeledExpand` builds the labeled info before it hands over, and that construction encodes
177
+ * the length in two bytes: asked for an unencodable length, the caller would otherwise be told its
178
+ * length does not fit rather than that it exceeds 255*Nh. */
179
+ function _assertExpandLength(Nh, len) {
94
180
  if (len > 255 * Nh) throw _err("hpke/export-length", "requested length " + len + " exceeds 255*Nh");
181
+ }
182
+ function _expand(hash, Nh, prk, info, len) {
183
+ _assertExpandLength(Nh, len);
95
184
  var out = [], t = Buffer.alloc(0), n = Math.ceil(len / Nh), fed = null, joined = null;
96
185
  try {
97
186
  for (var i = 1; i <= n; i++) {
98
187
  fed = concat([t, info, Buffer.from([i])]);
99
- t = nodeCrypto.createHmac(hash, prk).update(fed).digest();
188
+ t = guard.crypto.hmac(hash, prk, fed);
100
189
  guard.secret.zeroize(fed, HpkeError, "hpke/bad-input", "an expand feedback input");
101
190
  fed = null;
102
- out.push(t);
191
+ _push(out, t);
103
192
  }
104
193
  joined = concat(out);
105
194
  var exact = Buffer.alloc(len);
@@ -117,6 +206,7 @@ function _labeledExtract(kdf, suiteId, salt, label, ikm) {
117
206
  finally { guard.secret.zeroize(labeled, HpkeError, "hpke/bad-input", "the labeled extract input"); }
118
207
  }
119
208
  function _labeledExpand(kdf, suiteId, prk, label, info, len) {
209
+ _assertExpandLength(kdf.Nh, len);
120
210
  return _expand(kdf.hash, kdf.Nh, prk, concat([i2osp(len, 2), HPKE_V1, suiteId, L(label), info]), len);
121
211
  }
122
212
 
@@ -128,6 +218,10 @@ var EC_SEC1 = intrinsic.assign(intrinsic.create(null), {
128
218
  order: "ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551" },
129
219
  "P-521": { head: "30500201010442", tail: "a00706052b81040023",
130
220
  order: "01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409" },
221
+ /** @internal P-384 is here for the traditional half of MLKEM1024-P384. It is not a registered HPKE
222
+ * DHKEM on its own, so no KEM row names it; the hybrid row reaches it through its nominal group. */
223
+ "P-384": { head: "303e0201010430", tail: "a00706052b81040022",
224
+ order: "ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973" },
131
225
  });
132
226
  var ML_KEM_SPKI = intrinsic.assign(intrinsic.create(null), {
133
227
  "ML-KEM-512": "30820332300b06096086480165030404010382032100",
@@ -215,12 +309,147 @@ function _dh(privateKey, publicKey) {
215
309
  throw _err("hpke/bad-key", "KEM Diffie-Hellman failed: invalid or low-order public key", e);
216
310
  }
217
311
  }
312
+ /** @internal A DHKEM key pair. Only the ec and okp kinds reach here: an ML-KEM or hybrid key is a seed,
313
+ * drawn and expanded by generateKeyPair, and an ML-KEM encapsulation takes its randomness from the
314
+ * runtime rather than from an ephemeral key pair. */
218
315
  function _generate(kem) {
219
316
  if (kem.kind === "ec") return nodeCrypto.generateKeyPairSync("ec", { namedCurve: kem.nodeCurve });
220
317
  return nodeCrypto.generateKeyPairSync(kem.curve.toLowerCase());
221
318
  }
222
319
 
223
320
 
321
+ /** @internal The traditional half of a hybrid, as a KEM row the existing import paths already read. The
322
+ * group is not a registered DHKEM, so it has no row of its own; this borrows the shape rather than
323
+ * duplicating the import logic for it. */
324
+ function _groupRow(kem) {
325
+ var g = kem.group;
326
+ return { kind: g.kind, name: kem.name + " " + kem.groupName, curve: g.curve, nodeCurve: g.nodeCurve,
327
+ Npub: g.Nelem, Nsk: g.Nscalar, Nsecret: g.Nss };
328
+ }
329
+ /** @internal A single-stage KDF's Derive: an extendable-output function squeezed to `outLen` bytes
330
+ * (draft-ietf-hpke-pq-05 sec. 5, `SHAKE<SIZE>.Derive(ikm, L) = SHAKE<SIZE>(M = ikm, d = 8*L)`). */
331
+ function _derive(xof, input, outLen) {
332
+ return guard.crypto.xof(xof, input, outLen);
333
+ }
334
+ /** @internal LabeledDerive over a single-stage KDF (draft-ietf-hpke-hpke-05 sec. 4.4):
335
+ *
336
+ * labeled_ikm = concat(ikm, "HPKE-v1", suite_id, lengthPrefixed(label), I2OSP(L, 2), context)
337
+ * return Derive(labeled_ikm, L)
338
+ *
339
+ * The order is NOT LabeledExtract's. The ikm comes FIRST, the label carries a two-byte big-endian
340
+ * length prefix (sec. 3), and the output length is encoded after it rather than at the front. Getting
341
+ * any of that wrong yields a key pair no other implementation derives from the same input, which is why
342
+ * the published ikm-to-key vectors drive it rather than a round trip through this module.
343
+ * Both consumers reach it here: the KEM's DeriveKeyPair, whose suite_id is the KEM one and whose XOF the
344
+ * KEM fixes, and the HPKE key schedule, whose suite_id names the whole suite and whose XOF is the
345
+ * registered KDF's. */
346
+ function _labeledDerive(xof, suiteId, ikm, label, context, outLen) {
347
+ var lab = _bufferFrom(label, "latin1");
348
+ var parts = _pushPrefixed([ikm, HPKE_V1, suiteId], lab, "a derive label");
349
+ _push(parts, i2osp(outLen, 2), context);
350
+ var labeledIkm = concat(parts);
351
+ try { return _derive(xof, labeledIkm, outLen); }
352
+ finally { guard.secret.zeroize(labeledIkm, HpkeError, "hpke/bad-input", "the labeled derive input"); }
353
+ }
354
+ /** @internal RandomScalar (draft-irtf-cfrg-concrete-hybrid-kems sec. 3.1.1): rejection sampling over
355
+ * Nscalar-byte chunks of the seed, taking the first that is in [1, order). A chunk at or above the
356
+ * order would be a biased scalar, so it is skipped rather than reduced. */
357
+ function _randomScalar(kem, seedT) {
358
+ var g = kem.group;
359
+ if (g.kind === "okp") return _subarray(seedT, 0, g.Nscalar);
360
+ var order = _hex(EC_SEC1[g.curve].order);
361
+ for (var start = 0; start + g.Nscalar <= seedT.length; start += g.Nscalar) {
362
+ var chunk = _subarray(seedT, start, start + g.Nscalar);
363
+ if (_scalarInRange(chunk, order)) return chunk;
364
+ }
365
+ throw _err("hpke/bad-key", kem.name + " rejection sampling found no scalar in the seed");
366
+ }
367
+ /** @internal expandDecapsKeyG (draft-irtf-cfrg-hybrid-kems sec. 5.1.1). One seed expands, through
368
+ * SHAKE-256, into the ML-KEM seed and the group's scalar seed. The ML-KEM half goes in as the FIPS 203
369
+ * seed form, which is what makes this deterministic: Node's own `seed` generation option is ignored
370
+ * silently, so a key derived through it would differ on every call. */
371
+ function _hybridExpand(kem, seed) {
372
+ if (!_isBuffer(seed) || seed.length !== kem.Nsk) {
373
+ throw _err("hpke/bad-key", kem.name + " private key must be the " + kem.Nsk + "-byte seed as a Buffer");
374
+ }
375
+ var full = _derive("shake256", seed, kem.pq.Nsk + kem.group.Nseed);
376
+ try {
377
+ var skPQ = _importPrivate(kem.pq, _subarray(full, 0, kem.pq.Nsk));
378
+ var skT = _importPrivate(_groupRow(kem), _randomScalar(kem, _subarray(full, kem.pq.Nsk)));
379
+ return {
380
+ skPQ: skPQ, skT: skT,
381
+ ekPQ: _exportPublic(kem.pq, nodeCrypto.createPublicKey(skPQ)),
382
+ ekT: _exportPublic(_groupRow(kem), nodeCrypto.createPublicKey(skT)),
383
+ };
384
+ } finally {
385
+ guard.secret.zeroize(full, HpkeError, "hpke/bad-input", "the hybrid seed expansion");
386
+ }
387
+ }
388
+ /** @internal The C2PRI combiner (draft-irtf-cfrg-hybrid-kems sec. 5.2). The PQ ciphertext and
389
+ * encapsulation key are deliberately ABSENT: the PQ KEM is already ciphertext-collision-resistant, so
390
+ * only the traditional half needs binding. Adding them would be the UniversalCombiner and would derive
391
+ * a different secret from the same exchange. */
392
+ function _hybridCombine(kem, ssPQ, ssT, ctT, ekT) {
393
+ var input = concat([ssPQ, ssT, ctT, ekT, kem.label]);
394
+ /** @internal The digest through the captured operations too. This one IS the shared secret, so the
395
+ * hash prototype's `update` deciding what it covers is the same defect as the concatenation's. */
396
+ try { return guard.crypto.digest("sha3-256", input); }
397
+ finally { guard.secret.zeroize(input, HpkeError, "hpke/bad-input", "the hybrid combiner input"); }
398
+ }
399
+ function _hybridSplitPublic(kem, raw) {
400
+ if (!_isBuffer(raw) || raw.length !== kem.Npub) {
401
+ throw _err("hpke/bad-key", kem.name + " public key must be " + kem.Npub + " bytes: the " +
402
+ kem.pq.Npub + "-byte " + kem.pq.name + " key then the " + kem.group.Nelem + "-byte " + kem.groupName + " key");
403
+ }
404
+ return { pq: _subarray(raw, 0, kem.pq.Npub), t: _subarray(raw, kem.pq.Npub) };
405
+ }
406
+ function _hybridEncap(kem, pkRaw, eph) {
407
+ _refuseEph(kem, eph);
408
+ var halves = _hybridSplitPublic(kem, pkRaw);
409
+ var pqPub = _importPublic(kem.pq, halves.pq);
410
+ var gRow = _groupRow(kem);
411
+ var tPub = _importPublic(gRow, halves.t);
412
+ var pqOut = _mlKemEncap(kem.pq, pqPub, undefined);
413
+ var e, ssT = null;
414
+ try {
415
+ /** @internal Inside the try, so a key-generation fault still wipes the PQ shared secret above it. */
416
+ e = _ephemeral(gRow, undefined);
417
+ ssT = _elementToSharedSecret(kem, _dh(e.skE, tPub));
418
+ return {
419
+ sharedSecret: _hybridCombine(kem, pqOut.sharedSecret, ssT, e.enc, halves.t),
420
+ enc: concat([pqOut.enc, e.enc]),
421
+ };
422
+ } finally {
423
+ _wipeDh(ssT);
424
+ guard.secret.zeroize(pqOut.sharedSecret, HpkeError, "hpke/bad-input", "the hybrid PQ shared secret");
425
+ }
426
+ }
427
+ function _hybridDecap(kem, enc, seed) {
428
+ if (!_isBuffer(enc) || enc.length !== kem.Nenc) {
429
+ throw _err("hpke/bad-key", kem.name + " encapsulated key must be " + kem.Nenc + " bytes: the " +
430
+ kem.pq.Nenc + "-byte " + kem.pq.name + " ciphertext then the " + kem.group.Nelem + "-byte " + kem.groupName + " element");
431
+ }
432
+ var ctPQ = _subarray(enc, 0, kem.pq.Nenc);
433
+ var ctT = _subarray(enc, kem.pq.Nenc);
434
+ var st = _hybridExpand(kem, seed);
435
+ var ssPQ = null, ssT = null;
436
+ try {
437
+ ssPQ = _mlKemDecap(kem.pq, ctPQ, st.skPQ);
438
+ ssT = _elementToSharedSecret(kem, _dh(st.skT, _importPublic(_groupRow(kem), ctT)));
439
+ return _hybridCombine(kem, ssPQ, ssT, ctT, st.ekT);
440
+ } finally { _wipeDh(ssPQ); _wipeDh(ssT); }
441
+ }
442
+ /** @internal ElementToSharedSecret: the X coordinate for a NIST curve, the value itself for Curve25519.
443
+ * Node's diffieHellman already yields the X coordinate for an EC curve and the raw output for X25519,
444
+ * so both are already the value the specification names, and the length is asserted rather than
445
+ * assumed. */
446
+ function _elementToSharedSecret(kem, dh) {
447
+ if (dh.length !== kem.group.Nss) {
448
+ throw _err("hpke/bad-key", kem.name + " " + kem.groupName + " shared secret must be " + kem.group.Nss + " bytes");
449
+ }
450
+ return dh;
451
+ }
452
+
224
453
  function _kemSuiteId(kemId) { return concat([L("KEM"), i2osp(kemId, 2)]); }
225
454
  function _extractAndExpand(kem, dh, kemContext) {
226
455
  var kdf = _kdf(kem.kdf), sid = _kemSuiteId(_kemId(kem));
@@ -237,17 +466,23 @@ function _kemId(kem) {
237
466
  function _ephemeral(kem, eph) {
238
467
  if (eph != null) {
239
468
  if (typeof eph !== "object") throw _err("hpke/bad-input", "eph must be { skm, pkm } (a fixed ephemeral key pair), got " + guard.text.showValue(eph));
240
- var skE = _importPrivate(kem, eph.skm);
241
- return { skE: skE, enc: _boundPublic(kem, skE, eph.pkm) };
469
+ var skE = _importPrivate(kem, _rawKey(eph.skm, kem.Nsk));
470
+ return { skE: skE, enc: _boundPublic(kem, skE, _rawKey(eph.pkm, kem.Npub)) };
242
471
  }
243
472
  var kp = _generate(kem);
244
473
  return { skE: kp.privateKey, enc: _exportPublic(kem, kp.publicKey) };
245
474
  }
246
475
  function _wipeDh(dh) { guard.secret.zeroize(dh, HpkeError, "hpke/bad-input", "the KEM shared secret"); }
476
+ /** @internal A fixed ephemeral key pair applies to a DHKEM suite only, the ML-KEM and hybrid KEMs taking
477
+ * their encapsulation randomness from the runtime. One refusal for both, because a caller that supplied
478
+ * an `eph` and got a success would believe the key it named was the one used. */
479
+ function _refuseEph(kem, eph) {
480
+ if (eph == null) return;
481
+ throw _err("hpke/bad-input", "eph (a fixed ephemeral key pair) applies to a DHKEM suite; " + kem.name +
482
+ " encapsulation takes its randomness from the runtime");
483
+ }
247
484
  function _mlKemEncap(kem, pkR, eph) {
248
- if (eph != null) {
249
- throw _err("hpke/bad-input", "eph (a fixed ephemeral key pair) applies to a DHKEM suite; " + kem.name + " encapsulation takes its randomness from the runtime");
250
- }
485
+ _refuseEph(kem, eph);
251
486
  var r;
252
487
  try { r = nodeCrypto.encapsulate(pkR); }
253
488
  catch (e) { throw _err("hpke/bad-key", kem.name + " encapsulation failed", e); }
@@ -261,6 +496,7 @@ function _mlKemDecap(kem, enc, skR) {
261
496
  catch (e) { throw _err("hpke/bad-key", kem.name + " decapsulation failed", e); }
262
497
  }
263
498
  function _encap(kem, pkR, pkRm, eph) {
499
+ if (kem.kind === "hybrid") return _hybridEncap(kem, pkRm, eph);
264
500
  if (kem.kind === "mlkem") return _mlKemEncap(kem, pkR, eph);
265
501
  var e = _ephemeral(kem, eph);
266
502
  var dh = null;
@@ -269,7 +505,21 @@ function _encap(kem, pkR, pkRm, eph) {
269
505
  return { sharedSecret: _extractAndExpand(kem, dh, concat([e.enc, pkRm])), enc: e.enc };
270
506
  } finally { _wipeDh(dh); }
271
507
  }
508
+ /** @internal The width an encapsulated key is, for the bound the decapsulations read before they copy.
509
+ * A KEM that produces a ciphertext states `Nenc`; for a Diffie-Hellman KEM the encapsulated key IS an
510
+ * ephemeral public key, so its width is `Npub` and no `Nenc` is stated. Reading `Nenc` alone left the
511
+ * bound undefined for every DH suite, which is most of them, so the copy went ahead unbounded. */
512
+ function _encWidth(kem) { return kem.Nenc === undefined ? kem.Npub : kem.Nenc; }
513
+
272
514
  function _decap(kem, enc, skR, pkRm) {
515
+ /** @internal Snapshotted here rather than at each caller, so the standalone verb and the setup path are
516
+ * held to it alike: `_importPublic` and `_mlKemDecap` read a width off this buffer, and the KEM context
517
+ * concatenates it, so the two must be the same bytes. */
518
+ /** @internal Bounded at the KEM's encapsulation width before the copy. An encapsulated key is a fixed
519
+ * width and the checks enforcing it (`_importPublic`, `_mlKemDecap`) read it off the copy, so a value
520
+ * handed where a 32-byte key belongs was duplicated in full and only then refused. */
521
+ enc = _rawKey(enc, _encWidth(kem));
522
+ if (kem.kind === "hybrid") return _hybridDecap(kem, enc, skR);
273
523
  if (kem.kind === "mlkem") return _mlKemDecap(kem, enc, skR);
274
524
  var pkE = _importPublic(kem, enc);
275
525
  var dh = null;
@@ -289,6 +539,8 @@ function _authEncap(kem, pkR, pkRm, skS, pkSm, eph) {
289
539
  } finally { _wipeDh(dh1); _wipeDh(dh2); _wipeDh(dh); }
290
540
  }
291
541
  function _authDecap(kem, enc, skR, pkRm, pkS, pkSm) {
542
+ /** @internal Bounded before the copy, for the reason at `_decap`. */
543
+ enc = _rawKey(enc, _encWidth(kem));
292
544
  var pkE = _importPublic(kem, enc);
293
545
  var dh1 = null, dh2 = null, dh = null;
294
546
  try {
@@ -309,12 +561,42 @@ function _verifyPsk(mode, psk, pskId) {
309
561
  if (!gotPsk && (mode === MODE_PSK || mode === MODE_AUTH_PSK)) throw _err("hpke/inconsistent-psk", "mode requires a PSK");
310
562
  }
311
563
 
564
+ /** @internal CombineSecrets_OneStage (draft-ietf-hpke-hpke-05 sec. 5.1). One derive of Nk + Nn + Nh bytes
565
+ * replaces the two-stage schedule's five Extract / Expand calls, and the key, the base nonce and the
566
+ * exporter secret are consecutive slices of it. The export-only AEAD has Nk and Nn of 0 (sec. 7.3
567
+ * Table 5), so the derive is Nh bytes and all of it is the exporter secret; no published vector pairs
568
+ * export-only with a single-stage KDF, so that combination follows from the two tables.
569
+ * Each slice is copied out because the derive buffer is wiped on the way out. */
570
+ function _keyScheduleOneStage(suite, mode, sharedSecret, info, psk, pskId, role) {
571
+ var kdf = suite.kdf, aead = suite.aead;
572
+ var nk = aead.exportOnly ? 0 : aead.Nk, nn = aead.exportOnly ? 0 : aead.Nn;
573
+ /** @internal Both lists hold references to the caller's buffers rather than copies of them, and every
574
+ * length is checked as the lists are built, so an over-long input is refused before anything carrying
575
+ * secret material exists. The two joins below are the only buffers that do, and both are wiped. */
576
+ var secretParts = _pushPrefixed([], psk, "psk");
577
+ _pushPrefixed(secretParts, sharedSecret, "the KEM shared secret");
578
+ var kscParts = _pushPrefixed([_bufferFrom([mode])], pskId, "pskId");
579
+ _pushPrefixed(kscParts, info, "info");
580
+ var secrets = null, secret = null;
581
+ try {
582
+ secrets = concat(secretParts);
583
+ secret = _labeledDerive(kdf.xof, suite.suiteId, secrets, "secret", concat(kscParts), nk + nn + kdf.Nh);
584
+ var key = nk ? _bufferFrom(_subarray(secret, 0, nk)) : null;
585
+ var baseNonce = nn ? _bufferFrom(_subarray(secret, nk, nk + nn)) : null;
586
+ return new Context(suite, key, baseNonce, _bufferFrom(_subarray(secret, nk + nn)), role);
587
+ } finally {
588
+ guard.secret.zeroize(secrets, HpkeError, "hpke/bad-input", "the key-schedule secrets input");
589
+ guard.secret.zeroize(secret, HpkeError, "hpke/bad-input", "the key-schedule derive output");
590
+ }
591
+ }
592
+
312
593
  function _keySchedule(suite, mode, sharedSecret, info, psk, pskId, role) {
313
594
  _verifyPsk(mode, psk, pskId);
314
595
  var kdf = suite.kdf, sid = suite.suiteId, aead = suite.aead;
596
+ if (_isOneStage(kdf)) return _keyScheduleOneStage(suite, mode, sharedSecret, info, psk, pskId, role);
315
597
  var pskIdHash = _labeledExtract(kdf, sid, Buffer.alloc(0), "psk_id_hash", pskId);
316
598
  var infoHash = _labeledExtract(kdf, sid, Buffer.alloc(0), "info_hash", info);
317
- var ksc = concat([Buffer.from([mode]), pskIdHash, infoHash]);
599
+ var ksc = concat([_bufferFrom([mode]), pskIdHash, infoHash]);
318
600
  var secret = _labeledExtract(kdf, sid, sharedSecret, "secret", psk);
319
601
  try {
320
602
  var exporterSecret = _labeledExpand(kdf, sid, secret, "exp", ksc, kdf.Nh);
@@ -351,9 +633,16 @@ Context.prototype.seal = function (aad, pt) {
351
633
  var aead = this._suite.aead;
352
634
  if (this._role !== "S") throw _err("hpke/wrong-role", "seal is only available on a sender context (RFC 9180 sec. 5.2)");
353
635
  if (aead.exportOnly) throw _err("hpke/export-only", "seal is not available for an export-only AEAD");
636
+ /** @internal The AAD is normalized like every other byte input before its width decides anything.
637
+ * Read as `aad.length`, a DataView and an ArrayBuffer answer `undefined`, so the test was falsy and
638
+ * `setAAD` was never called: the additional data was SILENTLY DROPPED and the tag authenticated
639
+ * nothing. Both sides dropped it, so the message still opened and a caller testing their own code saw
640
+ * it work while the binding they relied on was absent. An empty AAD still means no additional data,
641
+ * which is what RFC 9180 sec. 5.2 uses. */
642
+ var aadBytes = _buf(aad);
354
643
  var nonce = this._nonce();
355
644
  var c = nodeCrypto.createCipheriv(aead.cipher, this._key, nonce, { authTagLength: aead.Nt });
356
- if (aad && aad.length) c.setAAD(aad);
645
+ if (aadBytes.length) c.setAAD(aadBytes);
357
646
  var body = guard.secret.cipherFinish(c, _buf(pt), HpkeError, "hpke/bad-input", "a seal intermediate");
358
647
  var ct = concat([body, c.getAuthTag()]);
359
648
  this._inc();
@@ -365,9 +654,12 @@ Context.prototype.open = function (aad, ct) {
365
654
  if (aead.exportOnly) throw _err("hpke/export-only", "open is not available for an export-only AEAD");
366
655
  ct = _buf(ct);
367
656
  if (ct.length < aead.Nt) throw _err("hpke/open-failed", "ciphertext is shorter than the AEAD tag");
657
+ /** @internal Normalized for the reason at `seal`: read as `aad.length` a DataView answered `undefined`,
658
+ * so the AAD was dropped on this side too and the pair round-tripped with nothing bound. */
659
+ var aadBytes = _buf(aad);
368
660
  var nonce = this._nonce();
369
661
  var d = nodeCrypto.createDecipheriv(aead.cipher, this._key, nonce, { authTagLength: aead.Nt });
370
- if (aad && aad.length) d.setAAD(aad);
662
+ if (aadBytes.length) d.setAAD(aadBytes);
371
663
  d.setAuthTag(ct.subarray(ct.length - aead.Nt));
372
664
  var pt;
373
665
  try { pt = guard.secret.cipherFinish(d, ct.subarray(0, ct.length - aead.Nt), HpkeError, "hpke/bad-input", "the recovered plaintext"); }
@@ -377,9 +669,66 @@ Context.prototype.open = function (aad, ct) {
377
669
  };
378
670
  Context.prototype.export = function (exporterContext, len) {
379
671
  if (!Number.isInteger(len) || len < 0) throw _err("hpke/export-length", "export length must be a non-negative integer");
380
- return _labeledExpand(this._suite.kdf, this._suite.suiteId, this._exporterSecret, "sec", _buf(exporterContext), len);
672
+ var kdf = this._suite.kdf;
673
+ if (_isOneStage(kdf)) {
674
+ /** @internal LabeledDerive encodes L in two bytes (sec. 4.4), so 65535 is the longest export a
675
+ * single-stage KDF can name. SHAKE has no input length limit, so sec. 7.2.1's further cap on
676
+ * exporter_context does not bind for these two KDFs. */
677
+ if (len > MAX_PREFIXED) {
678
+ throw _err("hpke/export-length", "requested length " + len + " exceeds " + MAX_PREFIXED +
679
+ ", the longest a single-stage KDF encodes (draft-ietf-hpke-hpke-05 sec. 4.4)");
680
+ }
681
+ return _labeledDerive(kdf.xof, this._suite.suiteId, this._exporterSecret, "sec", _buf(exporterContext), len);
682
+ }
683
+ return _labeledExpand(kdf, this._suite.suiteId, this._exporterSecret, "sec", _buf(exporterContext), len);
381
684
  };
382
- function _buf(x) { return Buffer.isBuffer(x) ? x : Buffer.from(x || []); }
685
+ /** @internal Every caller-supplied byte input is snapshotted here rather than used where it lies. A Buffer
686
+ * can shadow `length` with an own property while `byteLength` still reports its real size, so a check
687
+ * reading `length` and a copy reading `byteLength` see different values: that is how a 65536-byte `info`
688
+ * passed the single-stage limit and still reached the derive. `guard.bytes.snapshot` re-views the source
689
+ * and copies it, so every later read is of the bytes actually used, and a caller holding a reference can
690
+ * no longer change them after they are checked. */
691
+ /** @internal A caller's byte source as a byte VIEW, before anything measures or copies it. The admitted set
692
+ * is `guard.bytes.isByteSource`'s, which `deriveKeyPair` names in its own refusal: Buffer, TypedArray,
693
+ * DataView and ArrayBuffer. `guard.bytes.snapshot` takes Buffer and Uint8Array only, so handing it the raw
694
+ * input refused half the set the message promises, and `Response.arrayBuffer()` and
695
+ * `subtle.exportKey("raw", ...)` both return the refused half. `guard.bytes.source` shares the caller's
696
+ * backing store rather than copying it, so normalizing first leaves every cap ahead of every allocation. */
697
+ function _view(x, code, label) {
698
+ return guard.bytes.source(x, _err, code, label);
699
+ }
700
+ function _buf(x, maxBytes) {
701
+ if (x == null) return _EMPTY;
702
+ var v = _view(x, "hpke/bad-input", "a byte input");
703
+ /** @internal When the caller's own limit is known here, it is checked BEFORE the copy: the snapshot
704
+ * allocates, so a 256 MiB `info` was copied in full and only then refused for exceeding 65535. The size
705
+ * comes from `guard.bytes.lengthOf`, the same authoritative byte count the copy uses, not from `.length`,
706
+ * so this early check cannot be told a different size than the copy sees. */
707
+ if (maxBytes !== undefined && guard.bytes.lengthOf(v) > maxBytes) {
708
+ throw _err("hpke/input-length", "an input of " + guard.bytes.lengthOf(v) + " bytes exceeds the " +
709
+ maxBytes + "-byte limit a single-stage KDF imposes (draft-ietf-hpke-hpke-05 sec. 7.2.1)");
710
+ }
711
+ return guard.bytes.snapshot(v, _err, "hpke/bad-input", "a byte input");
712
+ }
713
+ /** @internal Raw key material, snapshotted for the same reason as `_buf` and bound to a local so the width
714
+ * check and the import read one value. A node KeyObject is not a byte source and passes through. */
715
+ function _rawKey(x, maxBytes) {
716
+ if (x == null || !guard.bytes.isByteSource(x)) return x;
717
+ var keyView = _view(x, "hpke/bad-key", "raw key material");
718
+ /** @internal `maxBytes` is the width the suite's keys are, read BEFORE the copy. Every key reaching
719
+ * here is a fixed width and the check enforcing it ran on the copy, so a value handed where a
720
+ * 32-byte key belongs was duplicated in full and only then refused: measured, 64 MiB copied to
721
+ * reject a key. A value within the width falls through unchanged to the checks that own it, whose
722
+ * messages name the form each key takes. Viewing costs no copy. */
723
+ if (maxBytes !== undefined) {
724
+ var size = guard.bytes.lengthOf(keyView);
725
+ if (size > maxBytes) {
726
+ throw _err("hpke/bad-key", "raw key material of " + size + " bytes is wider than the " +
727
+ maxBytes + " bytes this suite's keys are");
728
+ }
729
+ }
730
+ return guard.bytes.snapshot(keyView, _err, "hpke/bad-key", "raw key material");
731
+ }
383
732
 
384
733
 
385
734
  function _suite(ids) {
@@ -390,20 +739,67 @@ function _suite(ids) {
390
739
  }
391
740
 
392
741
  function _recipPublic(suite, pk) {
393
- if (Buffer.isBuffer(pk)) return { key: _importPublic(suite.kem, pk), pkm: pk };
394
- if (pk && pk.pkm) return { key: _importPublic(suite.kem, pk.pkm), pkm: pk.pkm };
742
+ /** @internal A hybrid public key is two keys concatenated, so there is no single KeyObject for it. The
743
+ * raw form is the canonical one and each half is imported inside the KEM, where the split is known. */
744
+ var supplied = _rawKey(_isBuffer(pk) ? pk : (pk && pk.pkm), suite.kem.Npub);
745
+ if (suite.kem.kind === "hybrid") {
746
+ if (!_isBuffer(supplied)) {
747
+ throw _err("hpke/bad-key", suite.kem.name + " public key must be the " + suite.kem.Npub +
748
+ "-byte concatenation as a Buffer or { pkm }, there being no single KeyObject for two keys");
749
+ }
750
+ _hybridSplitPublic(suite.kem, supplied);
751
+ return { key: null, pkm: supplied };
752
+ }
753
+ if (_isBuffer(supplied)) return { key: _importPublic(suite.kem, supplied), pkm: supplied };
395
754
  try {
396
755
  return { key: pk, pkm: _exportPublic(suite.kem, pk) };
397
756
  } catch (e) {
398
757
  if (e instanceof HpkeError) throw e;
399
- throw _err("hpke/bad-key", "invalid KEM public key (expected a node KeyObject, a raw buffer, or { pkm })", e);
758
+ /** @internal The forms are named exactly, because "a raw buffer" reads as any byte view and a bare
759
+ * TypedArray is NOT one of them: for a key the alternative to a Buffer is a KeyObject, and a value
760
+ * that is neither reaches the KeyObject export and fails there rather than at the door. A caller
761
+ * holding a Uint8Array is told to wrap it rather than left to infer which reading was meant. */
762
+ throw _err("hpke/bad-key", "invalid KEM public key (expected a node KeyObject, a Buffer, or " +
763
+ "{ pkm: Buffer }; a bare TypedArray, DataView or ArrayBuffer is not a key here, so wrap it with " +
764
+ "Buffer.from first)", e);
400
765
  }
401
766
  }
402
767
  function _recipPrivate(suite, sk) {
403
768
  var kem = suite.kem;
404
- if (sk && sk.skm != null) {
405
- var key = _importPrivate(kem, sk.skm);
406
- return { key: key, pkm: _boundPublic(kem, key, sk.pkm) };
769
+ /** @internal A hybrid decapsulation key IS the seed, so it stays a Buffer and the component keys are
770
+ * expanded inside the KEM. Deriving the public key here also checks the seed before anything else
771
+ * runs, so a wrong-length seed is refused at the door. */
772
+ /** @internal Both members are read ONCE, into a local, and every test below asks the local. Written as
773
+ * `sk.skm != null ? sk.skm : sk` the member was read twice in that one expression and a third time to
774
+ * pick the branch further down, so an accessor answering differently across those reads chose a
775
+ * branch for one value and ran it on another. The outcomes were refusals rather than substitutions,
776
+ * and the shape is still one read: whether a member is PRESENT and what it HOLDS are one question
777
+ * asked once.
778
+ * Each is bounded at the width the suite's keys are before it is copied, and `pkm` is read the same
779
+ * way `skm` is. An own-property test on `pkm` alone made the two fields disagree about what counts
780
+ * as supplied: one on the key pair's PROTOTYPE was treated as absent, so the rule that a supplied
781
+ * `pkm` must match `skm` was skipped rather than applied, and a key pair carrying a mismatching one
782
+ * passed a check that never ran. */
783
+ var suppliedSkm = sk ? sk.skm : undefined;
784
+ var hasSkm = suppliedSkm != null;
785
+ var suppliedSk = _rawKey(hasSkm ? suppliedSkm : sk, kem.Nsk);
786
+ var suppliedPkm = sk == null ? undefined : sk.pkm;
787
+ var suppliedPk = suppliedPkm == null ? undefined : _rawKey(suppliedPkm, kem.Npub);
788
+ if (kem.kind === "hybrid") {
789
+ if (!_isBuffer(suppliedSk)) {
790
+ throw _err("hpke/bad-key", kem.name + " private key must be the " + kem.Nsk +
791
+ "-byte seed, as { skm } or a Buffer");
792
+ }
793
+ var st = _hybridExpand(kem, suppliedSk);
794
+ var derivedPkm = concat([st.ekPQ, st.ekT]);
795
+ if (suppliedPk !== undefined && (!_isBuffer(suppliedPk) || !_bufferEquals(derivedPkm, suppliedPk))) {
796
+ throw _err("hpke/bad-key", "pkm is not the " + kem.name + " public key derived from skm");
797
+ }
798
+ return { key: suppliedSk, pkm: derivedPkm };
799
+ }
800
+ if (hasSkm) {
801
+ var key = _importPrivate(kem, suppliedSk);
802
+ return { key: key, pkm: _boundPublic(kem, key, suppliedPk) };
407
803
  }
408
804
  if (_isBuffer(sk)) {
409
805
  throw _err("hpke/bad-key", "a serialized " + kem.name + " private key must be provided as { skm" +
@@ -419,7 +815,10 @@ function _recipPrivate(suite, sk) {
419
815
  }
420
816
  }
421
817
  function _requireAuthCapable(kem) {
422
- if (kem.kind === "mlkem") {
818
+ /** @internal The auth modes need a static-static Diffie-Hellman, which a KEM interface does not offer.
819
+ * A hybrid is a KEM for the same reason its PQ half is, so it is refused alongside it rather than
820
+ * having the check apply to only one of the two KEM kinds. */
821
+ if (kem.kind === "mlkem" || kem.kind === "hybrid") {
423
822
  throw _err("hpke/auth-unsupported", kem.name + " defines no AuthEncap / AuthDecap (draft-ietf-hpke-pq sec. 7.2); use the psk mode or a signature");
424
823
  }
425
824
  }
@@ -463,10 +862,15 @@ function _options(opts, keys, who) {
463
862
  // @module @intro; a data registry, not a callable primitive).
464
863
  var suites = intrinsic.assign(intrinsic.create(null), {
465
864
  KEM: intrinsic.assign(intrinsic.create(null), {
466
- DHKEM_P256_HKDF_SHA256: 0x0010, DHKEM_P521_HKDF_SHA512: 0x0012, DHKEM_X25519_HKDF_SHA256: 0x0020, DHKEM_X448_HKDF_SHA512: 0x0021,
865
+ DHKEM_P256_HKDF_SHA256: 0x0010, DHKEM_P384_HKDF_SHA384: 0x0011, DHKEM_P521_HKDF_SHA512: 0x0012,
866
+ DHKEM_X25519_HKDF_SHA256: 0x0020, DHKEM_X448_HKDF_SHA512: 0x0021,
467
867
  ML_KEM_512: 0x0040, ML_KEM_768: 0x0041, ML_KEM_1024: 0x0042,
868
+ MLKEM768_P256: 0x0050, MLKEM1024_P384: 0x0051, MLKEM768_X25519: 0x647a,
869
+ }),
870
+ KDF: intrinsic.assign(intrinsic.create(null), {
871
+ HKDF_SHA256: 0x0001, HKDF_SHA384: 0x0002, HKDF_SHA512: 0x0003,
872
+ SHAKE128: 0x0010, SHAKE256: 0x0011,
468
873
  }),
469
- KDF: intrinsic.assign(intrinsic.create(null), { HKDF_SHA256: 0x0001, HKDF_SHA384: 0x0002, HKDF_SHA512: 0x0003 }),
470
874
  AEAD: intrinsic.assign(intrinsic.create(null), { AES_128_GCM: 0x0001, AES_256_GCM: 0x0002, CHACHA20_POLY1305: 0x0003, EXPORT_ONLY: 0xFFFF }),
471
875
  MODE: intrinsic.assign(intrinsic.create(null), { BASE: MODE_BASE, PSK: MODE_PSK, AUTH: MODE_AUTH, AUTH_PSK: MODE_AUTH_PSK }),
472
876
  });
@@ -491,7 +895,9 @@ var suites = intrinsic.assign(intrinsic.create(null), {
491
895
  * and `opts.senderKey` (auth modes) supply the corresponding inputs; an option that
492
896
  * belongs to another mode, such as `senderKey` in base mode, is refused with
493
897
  * `hpke/bad-input` rather than left unused. The auth modes exist for the DHKEM
494
- * suites only: an ML-KEM suite throws `hpke/auth-unsupported`.
898
+ * suites only: an ML-KEM suite throws `hpke/auth-unsupported`. On a single-stage
899
+ * KDF (SHAKE128, SHAKE256) `opts.info`, `opts.psk` and `opts.pskId` are capped at
900
+ * 65535 bytes each and a longer one throws `hpke/input-length`.
495
901
  *
496
902
  * @example
497
903
  * var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_128_GCM };
@@ -501,9 +907,17 @@ var suites = intrinsic.assign(intrinsic.create(null), {
501
907
  */
502
908
  function _setupS(ids, pkR, opts) {
503
909
  var suite = _suite(ids);
910
+ /** @internal The RECIPIENT KEY is copied before the options bag is read, because reading that bag runs
911
+ * caller code: a getter on `opts.info` fired while the recipient's key was still the caller's buffer,
912
+ * and one that copied ANOTHER recipient's key over it had this setup encapsulate to that other
913
+ * recipient while the caller had named the first. Measured: a sender naming recipient A produced a
914
+ * message only B could open, with every width check still passing because both keys are the same
915
+ * width. `_setupR` takes its ciphertext first for this reason; the sender side is the same shape with
916
+ * the subject being the key rather than the ciphertext. `_suite(ids)` stays above both, by necessity:
917
+ * which KEM this is decides the width the key is held to. */
918
+ var r = _recipPublic(suite, pkR);
504
919
  opts = _options(opts || {}, _SETUP_S_KEYS, "sender setup");
505
920
  var mode = opts.mode;
506
- var r = _recipPublic(suite, pkR);
507
921
  var kem = suite.kem, k;
508
922
  if (mode === MODE_AUTH || mode === MODE_AUTH_PSK) {
509
923
  _requireAuthCapable(kem);
@@ -514,7 +928,8 @@ function _setupS(ids, pkR, opts) {
514
928
  _refuseUnusedAuthKey(opts.senderKey, "senderKey", mode);
515
929
  k = _encap(kem, r.key, r.pkm, opts.eph);
516
930
  }
517
- var ctx = _keySchedule(suite, mode, k.sharedSecret, _buf(opts.info), _buf(opts.psk), _buf(opts.pskId), "S");
931
+ var cap = _isOneStage(suite.kdf) ? MAX_PREFIXED : undefined;
932
+ var ctx = _keySchedule(suite, mode, k.sharedSecret, _buf(opts.info, cap), _buf(opts.psk, cap), _buf(opts.pskId, cap), "S");
518
933
  return { enc: k.enc, context: ctx, sharedSecret: k.sharedSecret };
519
934
  }
520
935
  /**
@@ -545,6 +960,17 @@ function _setupS(ids, pkR, opts) {
545
960
  */
546
961
  function _setupR(ids, enc, skR, opts) {
547
962
  var suite = _suite(ids);
963
+ /** @internal The CIPHERTEXT is copied before the OPTIONS bag and the KEY RECORD are read, because
964
+ * reading either runs caller code: an `skm` accessor on the private key, or one on
965
+ * `opts.senderPublicKey`, fires while the encapsulation is still the caller's buffer, and one that
966
+ * copies a DIFFERENT encapsulation over it has this setup derive the secret for the other
967
+ * ciphertext. Copied once here and used by both arms below, so the bytes the KEM context commits to
968
+ * are the bytes that arrived. `_suite(ids)` above it reads the caller's identifier record first, by
969
+ * necessity: which KEM this is decides the width the copy is held to. Bounded at that width as it
970
+ * is taken, so the early copy that closes the substitution window is not itself an allocation the
971
+ * size of whatever was supplied: a 64 MiB value handed where a 32-byte encapsulation belongs was
972
+ * copied in full and only then refused. */
973
+ var sealed = _rawKey(enc, _encWidth(suite.kem));
548
974
  opts = _options(opts || {}, _SETUP_R_KEYS, "recipient setup");
549
975
  var mode = opts.mode;
550
976
  var r = _recipPrivate(suite, skR);
@@ -553,12 +979,13 @@ function _setupR(ids, enc, skR, opts) {
553
979
  _requireAuthCapable(kem);
554
980
  if (opts.senderPublicKey == null) throw _err("hpke/auth-key-required", "an authenticated mode requires opts.senderPublicKey (the sender's KEM public key, RFC 9180 sec. 5.1.3)");
555
981
  var pkS = _recipPublic(suite, opts.senderPublicKey);
556
- ss = _authDecap(kem, _buf(enc), r.key, r.pkm, pkS.key, pkS.pkm);
982
+ ss = _authDecap(kem, sealed, r.key, r.pkm, pkS.key, pkS.pkm);
557
983
  } else {
558
984
  _refuseUnusedAuthKey(opts.senderPublicKey, "senderPublicKey", mode);
559
- ss = _decap(kem, _buf(enc), r.key, r.pkm);
985
+ ss = _decap(kem, sealed, r.key, r.pkm);
560
986
  }
561
- try { return _keySchedule(suite, mode, ss, _buf(opts.info), _buf(opts.psk), _buf(opts.pskId), "R"); }
987
+ var rCap = _isOneStage(suite.kdf) ? MAX_PREFIXED : undefined;
988
+ try { return _keySchedule(suite, mode, ss, _buf(opts.info, rCap), _buf(opts.psk, rCap), _buf(opts.pskId, rCap), "R"); }
562
989
  finally { guard.secret.zeroize(ss, HpkeError, "hpke/bad-input", "the KEM shared secret"); }
563
990
  }
564
991
 
@@ -581,10 +1008,16 @@ function _setupR(ids, enc, skR, opts) {
581
1008
  * var out = pki.hpke.seal(ids, mlKemPkR, {}, Buffer.from("aad"), Buffer.from("msg"));
582
1009
  */
583
1010
  function seal(ids, pkR, opts, aad, pt) {
1011
+ /** @internal The PAYLOAD is copied before the setup runs. The setup reads the suite identifiers, the
1012
+ * options bag and the recipient's key record, and reading any of them runs caller code, so a getter
1013
+ * there could overwrite the plaintext or the additional data this verb goes on to encrypt and
1014
+ * authenticate. Both are taken here, at the door, before anything the caller controls is consulted. */
1015
+ var authData = _buf(aad);
1016
+ var plain = _buf(pt);
584
1017
  var s = null;
585
1018
  try {
586
1019
  s = _setupS(ids, pkR, opts);
587
- return { enc: s.enc, ct: s.context.seal(_buf(aad), _buf(pt)) };
1020
+ return { enc: s.enc, ct: s.context.seal(authData, plain) };
588
1021
  } finally {
589
1022
  if (s) {
590
1023
  s.context._dispose();
@@ -613,13 +1046,226 @@ function seal(ids, pkR, opts, aad, pt) {
613
1046
  * var pt = pki.hpke.open(ids, o.enc, mlKemSkR, {}, Buffer.alloc(0), o.ct);
614
1047
  */
615
1048
  function open(ids, enc, skR, opts, aad, ct) {
1049
+ /** @internal The CIPHERTEXT and its additional data are copied before the setup runs, for the reason
1050
+ * `seal` copies its payload first: the setup reads the suite identifiers, the options bag and the
1051
+ * key record, and a getter on any of them could overwrite what this verb goes on to authenticate.
1052
+ * The encapsulation is copied inside the setup, which is where both its callers reach it. */
1053
+ var authData = _buf(aad);
1054
+ var sealed = _buf(ct);
616
1055
  var ctx = _setupR(ids, enc, skR, opts);
617
- try { return ctx.open(_buf(aad), _buf(ct)); }
1056
+ try { return ctx.open(authData, sealed); }
618
1057
  finally { ctx._dispose(); }
619
1058
  }
620
1059
 
1060
+ /**
1061
+ * @primitive pki.hpke.deriveKeyPair
1062
+ * @signature pki.hpke.deriveKeyPair(kemId, ikm) -> { publicKey, privateKey }
1063
+ * @since 0.8.45
1064
+ * @status stable
1065
+ * @spec RFC 9180 sec. 7.1.3, draft-ietf-hpke-pq sec. 3, draft-ietf-hpke-pq sec. 4
1066
+ * @defends kem-key-nondeterminism (CWE-330)
1067
+ * @related pki.hpke.generateKeyPair, pki.hpke.encap, pki.hpke.setupS
1068
+ *
1069
+ * The KEM's `DeriveKeyPair`: a key pair derived deterministically from input keying material, returned as
1070
+ * raw byte strings in the encodings the KEM registers. The same `ikm` always gives the same pair, so a
1071
+ * key can be rebuilt from backed-up material instead of stored in full, and the pair matches what any
1072
+ * other conforming implementation derives from the same input.
1073
+ *
1074
+ * `ikm` is keying material, not a private key. Each KEM runs it through its own derivation first: a
1075
+ * DHKEM through `LabeledExtract` and `LabeledExpand`, with the prime curves rejection-sampling
1076
+ * candidates until one lands in [1, order) (RFC 9180 section 7.1.3); an ML-KEM and a PQ/T hybrid through
1077
+ * `SHAKE256.LabeledDerive` to the KEM's seed, which is then expanded (draft-ietf-hpke-pq sections 3 and
1078
+ * 4). To use a private key you already hold, pass it as `{ skm }` to `decap`, `setupR` or `open`
1079
+ * instead; that door takes the KEM's own private encoding and checks its width.
1080
+ *
1081
+ * Any length of `ikm` is accepted. The hybrid suites state a 32-byte minimum as a SHOULD, which is not
1082
+ * enforced here, and an ML-KEM takes input of any length.
1083
+ *
1084
+ * `privateKey` is the KEM's private encoding: the 32-byte seed for a hybrid suite, the 64-byte seed for
1085
+ * a bare ML-KEM, and the scalar for a DHKEM.
1086
+ *
1087
+ * @example
1088
+ * var ikm = Buffer.alloc(32, 7);
1089
+ * var kp = pki.hpke.deriveKeyPair(pki.hpke.suites.KEM.MLKEM768_X25519, ikm);
1090
+ * kp.publicKey.length; // -> 1216, the ML-KEM-768 key then the X25519 key
1091
+ * kp.privateKey.length; // -> 32, the seed the ikm derived
1092
+ */
1093
+ function deriveKeyPair(kemId, ikm) {
1094
+ var kem = _kem(kemId);
1095
+ if (!_isBuffer(ikm) && !guard.bytes.isByteSource(ikm)) {
1096
+ throw _err("hpke/bad-input", "ikm must be a byte source (Buffer / TypedArray / DataView / ArrayBuffer)");
1097
+ }
1098
+ /** @internal Viewed then copied, so the four forms the message above names are the four forms this
1099
+ * takes: `guard.bytes.snapshot` alone wants a Buffer or a Uint8Array, and the two the message names
1100
+ * last were refused by the line after the one that admitted them. */
1101
+ var input = guard.bytes.snapshot(_view(ikm, "hpke/bad-input", "ikm"), _err, "hpke/bad-input", "ikm");
1102
+ if (kem.kind === "hybrid") {
1103
+ /** @internal draft-ietf-hpke-pq sec. 4: the ikm is derived down to the KEM's 32-byte seed, and the
1104
+ * CG framework then expands both component keys from THAT. The seed is not the ikm. */
1105
+ var seed = _labeledDerive("shake256", _kemSuiteId(_kemId(kem)), input, "DeriveKeyPair", _EMPTY, kem.Nsk);
1106
+ var st = _hybridExpand(kem, seed);
1107
+ return { publicKey: concat([st.ekPQ, st.ekT]), privateKey: seed };
1108
+ }
1109
+ if (kem.kind === "mlkem") {
1110
+ /** @internal draft-ietf-hpke-pq sec. 3: dk = SHAKE256.LabeledDerive(ikm, "DeriveKeyPair", "", 64),
1111
+ * and the decapsulation key returned is that 64-byte value. */
1112
+ var dk = _labeledDerive("shake256", _kemSuiteId(_kemId(kem)), input, "DeriveKeyPair", _EMPTY, kem.Nsk);
1113
+ var mlSk = _importPrivate(kem, dk);
1114
+ return { publicKey: _exportPublic(kem, nodeCrypto.createPublicKey(mlSk)), privateKey: dk };
1115
+ }
1116
+ var raw = _deriveKeyPairDhkem(kem, input);
1117
+ var sk = _importPrivate(kem, raw);
1118
+ return { publicKey: _exportPublic(kem, nodeCrypto.createPublicKey(sk)), privateKey: raw };
1119
+ }
1120
+
1121
+ /** @internal DeriveKeyPair for a DHKEM (RFC 9180 sec. 7.1.3). X25519 and X448 expand the extract output
1122
+ * straight to a scalar; the prime curves reject-sample candidates until one lands in [1, order), which
1123
+ * is what keeps the scalar uniform rather than a reduction of a wider value:
1124
+ *
1125
+ * dkp_prk = LabeledExtract("", "dkp_prk", ikm)
1126
+ * sk = LabeledExpand(dkp_prk, "sk", "", Nsk) -- X25519 / X448
1127
+ * bytes = LabeledExpand(dkp_prk, "candidate", I2OSP(counter,1), Nsk); bytes[0] &= bitmask -- NIST
1128
+ *
1129
+ * The bitmask is 0xFF for P-256 and P-384 and 0x01 for P-521, which is what makes the 66-byte P-521
1130
+ * candidate a 521-bit value rather than a 528-bit one. The counter is bounded at 255 by the text. */
1131
+ function _deriveKeyPairDhkem(kem, ikm) {
1132
+ var kdf = _kdf(kem.kdf), sid = _kemSuiteId(_kemId(kem));
1133
+ var dkpPrk = _labeledExtract(kdf, sid, _EMPTY, "dkp_prk", ikm);
1134
+ try {
1135
+ if (kem.kind !== "ec") return _labeledExpand(kdf, sid, dkpPrk, "sk", _EMPTY, kem.Nsk);
1136
+ var order = _hex(EC_SEC1[kem.curve].order);
1137
+ var bitmask = kem.curve === "P-521" ? 0x01 : 0xff;
1138
+ for (var counter = 0; counter <= 255; counter++) {
1139
+ var candidate = _labeledExpand(kdf, sid, dkpPrk, "candidate", i2osp(counter, 1), kem.Nsk);
1140
+ candidate[0] = candidate[0] & bitmask;
1141
+ if (_scalarInRange(candidate, order)) return candidate;
1142
+ guard.secret.zeroize(candidate, HpkeError, "hpke/bad-input", "a rejected DeriveKeyPair candidate");
1143
+ }
1144
+ throw _err("hpke/bad-key",
1145
+ "DeriveKeyPair found no scalar in [1, order) in 256 candidates (RFC 9180 sec. 7.1.3)");
1146
+ } finally {
1147
+ guard.secret.zeroize(dkpPrk, HpkeError, "hpke/bad-input", "the DeriveKeyPair extract PRK");
1148
+ }
1149
+ }
1150
+
1151
+ /**
1152
+ * @primitive pki.hpke.generateKeyPair
1153
+ * @signature pki.hpke.generateKeyPair(kemId) -> { publicKey, privateKey }
1154
+ * @since 0.8.45
1155
+ * @status stable
1156
+ * @spec RFC 9180 sec. 4, draft-ietf-hpke-pq sec. 3
1157
+ * @defends kem-key-reuse (CWE-330)
1158
+ * @related pki.hpke.deriveKeyPair, pki.hpke.setupR
1159
+ *
1160
+ * A fresh KEM key pair, as raw byte strings in the encodings the KEM registers. For a hybrid suite the
1161
+ * private key is a 32-byte seed the whole pair expands from, and for the others it is the KEM's own
1162
+ * private encoding.
1163
+ *
1164
+ * @example
1165
+ * var kp = pki.hpke.generateKeyPair(pki.hpke.suites.KEM.MLKEM768_P256);
1166
+ * kp.publicKey.length; // -> 1249
1167
+ * kp.privateKey.length; // -> 32
1168
+ */
1169
+ function generateKeyPair(kemId) {
1170
+ var kem = _kem(kemId);
1171
+ /** @internal A hybrid key and an ML-KEM key are both a SEED, so both are drawn here and expanded
1172
+ * through the deterministic derivation rather than read back out of a generated key. Reading it back
1173
+ * is not merely awkward for ML-KEM, it is impossible on some of the Node versions this toolkit runs
1174
+ * on: the PKCS#8 an ML-KEM generation emits is the seed form on one and the EXPANDED key on another,
1175
+ * and a seed cannot be recovered from an expanded key. Drawing it is the same value either way. */
1176
+ if (kem.kind === "hybrid" || kem.kind === "mlkem") return deriveKeyPair(kemId, _randomBytes(kem.Nsk));
1177
+ var kp = _generate(kem);
1178
+ return {
1179
+ publicKey: _exportPublic(kem, kp.publicKey),
1180
+ privateKey: _exportPrivateRaw(kem, kp.privateKey),
1181
+ };
1182
+ }
1183
+
1184
+ /**
1185
+ * @primitive pki.hpke.encap
1186
+ * @signature pki.hpke.encap(kemId, recipientPublicKey) -> { enc, sharedSecret }
1187
+ * @since 0.8.45
1188
+ * @status stable
1189
+ * @spec RFC 9180 sec. 4, draft-irtf-cfrg-hybrid-kems sec. 5.2
1190
+ * @defends kem-shared-secret-confusion (CWE-347)
1191
+ * @related pki.hpke.decap, pki.hpke.setupS
1192
+ *
1193
+ * The KEM half of HPKE on its own: encapsulate to a recipient public key, returning the encapsulated
1194
+ * key to send and the shared secret it establishes. `pki.hpke.setupS` runs this and then the key
1195
+ * schedule; this verb is for a caller driving its own.
1196
+ *
1197
+ * For a hybrid KEM the encapsulated key is the PQ ciphertext followed by the traditional group element,
1198
+ * and the shared secret is the two component secrets combined with the traditional ciphertext, the
1199
+ * traditional public key and the suite label, which is the C2PRI combiner that framework specifies.
1200
+ *
1201
+ * @example
1202
+ * var kp = pki.hpke.generateKeyPair(pki.hpke.suites.KEM.MLKEM768_X25519);
1203
+ * var e = pki.hpke.encap(pki.hpke.suites.KEM.MLKEM768_X25519, kp.publicKey);
1204
+ * e.enc.length; // -> 1120
1205
+ * e.sharedSecret.length; // -> 32
1206
+ */
1207
+ function encap(kemId, recipientPublicKey) {
1208
+ var kem = _kem(kemId);
1209
+ var r = _recipPublic({ kem: kem }, recipientPublicKey);
1210
+ return _encap(kem, r.key, r.pkm, undefined);
1211
+ }
1212
+
1213
+ /**
1214
+ * @primitive pki.hpke.decap
1215
+ * @signature pki.hpke.decap(kemId, enc, recipientPrivateKey) -> sharedSecret
1216
+ * @since 0.8.45
1217
+ * @status stable
1218
+ * @spec RFC 9180 sec. 4, draft-irtf-cfrg-hybrid-kems sec. 5.2
1219
+ * @defends kem-shared-secret-confusion (CWE-347)
1220
+ * @related pki.hpke.encap, pki.hpke.setupR
1221
+ *
1222
+ * The receiving half of the KEM: recover the shared secret from an encapsulated key. A KEM does not
1223
+ * report failure by throwing where the specification says it returns a value, so a ciphertext this key
1224
+ * did not receive yields a different secret rather than an error, and the AEAD above it is what refuses.
1225
+ * A structurally wrong input, such as an encapsulated key of the wrong length, is refused.
1226
+ *
1227
+ * @example
1228
+ * var id = pki.hpke.suites.KEM.MLKEM768_X25519;
1229
+ * var kp = pki.hpke.generateKeyPair(id);
1230
+ * var e = pki.hpke.encap(id, kp.publicKey);
1231
+ * pki.hpke.decap(id, e.enc, { skm: kp.privateKey }).equals(e.sharedSecret); // -> true
1232
+ */
1233
+ function decap(kemId, enc, recipientPrivateKey) {
1234
+ var kem = _kem(kemId);
1235
+ /** @internal The CIPHERTEXT is copied before the key record is read, because reading that record runs
1236
+ * caller code: an `skm` accessor fired while the encapsulation was still the caller's buffer, and one
1237
+ * that copied a DIFFERENT encapsulation over it had this verb return the shared secret for the other
1238
+ * ciphertext. `_decap` snapshots too, which is the right place for the setup path, and by then the
1239
+ * substitution has already happened. The subject is taken first, here, at the door, and bounded at
1240
+ * the KEM's encapsulation width as it is taken so the copy is not the size of whatever arrived. */
1241
+ var sealed = _rawKey(enc, _encWidth(kem));
1242
+ var r = _recipPrivate({ kem: kem }, recipientPrivateKey);
1243
+ return _decap(kem, sealed, r.key, r.pkm);
1244
+ }
1245
+
1246
+ /** @internal The raw scalar of a generated DHKEM key. It comes from the JWK the producer emits, whose
1247
+ * `d` is the fixed-width big-endian value, and NOT from an offset into a DER: the SEC1 head in EC_SEC1
1248
+ * is what this module BUILDS around a scalar, and a built SEC1 carries no public key and fits a
1249
+ * short-form length while the one node exports carries both and needs a long-form one, so the two
1250
+ * differ by a byte wherever the content passes 127 octets. The width is asserted against the KEM's own
1251
+ * Nsk rather than trusted. An ML-KEM or hybrid key never reaches here, its private half being a seed
1252
+ * that generateKeyPair draws. */
1253
+ function _exportPrivateRaw(kem, privateKey) {
1254
+ var raw = _b64u(privateKey.export({ format: "jwk" }).d);
1255
+ if (!_isBuffer(raw) || raw.length !== kem.Nsk) {
1256
+ throw _err("hpke/bad-key", "the generated " + kem.name + " private key is not the " +
1257
+ kem.Nsk + "-byte scalar this verb returns");
1258
+ }
1259
+ return raw;
1260
+ }
1261
+
621
1262
  module.exports = {
622
1263
  suites: suites,
623
1264
  setupS: _setupS, setupR: _setupR,
624
1265
  seal: seal, open: open,
1266
+ deriveKeyPair: deriveKeyPair, generateKeyPair: generateKeyPair,
1267
+ encap: encap, decap: decap,
1268
+ /** @internal The nominal-group constants, so the vectors can assert the seed widths the draft states
1269
+ * where no published vector makes the difference observable. Not on the curated surface. */
1270
+ nominalGroups: NOMINAL_GROUPS,
625
1271
  };