@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.
- package/CHANGELOG.md +401 -0
- package/README.md +66 -15
- package/bin/pki.js +716 -30
- package/examples/issue-a-certificate.js +90 -0
- package/examples/post-quantum.js +66 -0
- package/examples/read-a-certificate.js +56 -0
- package/examples/revoke-and-check.js +86 -0
- package/examples/sign-and-verify-cms.js +63 -0
- package/index.js +18 -5
- package/lib/acme.js +223 -74
- package/lib/alt-sig.js +441 -0
- package/lib/asn1-der.js +48 -25
- package/lib/attrcert-sign.js +27 -19
- package/lib/cbor-det.js +24 -14
- package/lib/cmc-build.js +9 -2
- package/lib/cmc-verify.js +14 -3
- package/lib/cmp-build.js +17 -5
- package/lib/cmp-session.js +24 -11
- package/lib/cmp-verify.js +28 -19
- package/lib/cms-compress.js +3 -1
- package/lib/cms-decrypt.js +102 -7
- package/lib/cms-encrypt.js +59 -7
- package/lib/cms-sign.js +31 -9
- package/lib/cms-verify.js +22 -18
- package/lib/composite-kem.js +30 -18
- package/lib/composite-sig.js +29 -14
- package/lib/constants.js +26 -0
- package/lib/crl-sign.js +111 -19
- package/lib/crmf-sign.js +167 -15
- package/lib/csr-sign.js +117 -6
- package/lib/ct.js +1008 -58
- package/lib/est.js +29 -16
- package/lib/framework-error.js +12 -0
- package/lib/guard-async.js +1 -1
- package/lib/guard-bytes.js +103 -20
- package/lib/guard-crypto.js +205 -3
- package/lib/guard-encoding.js +6 -1
- package/lib/guard-identifier.js +55 -8
- package/lib/guard-intrinsic.js +43 -0
- package/lib/guard-json.js +122 -9
- package/lib/guard-limits.js +12 -1
- package/lib/guard-list.js +63 -8
- package/lib/guard-name.js +167 -20
- package/lib/guard-range.js +43 -5
- package/lib/guard-text.js +41 -0
- package/lib/guard-verdict.js +39 -8
- package/lib/hpke.js +688 -42
- package/lib/http-transport.js +156 -4
- package/lib/identity-match.js +4 -4
- package/lib/inspect.js +33 -6
- package/lib/jose.js +93 -25
- package/lib/key.js +18 -13
- package/lib/lint.js +165 -1
- package/lib/merkle.js +233 -27
- package/lib/ocsp.js +27 -14
- package/lib/oid.js +14 -4
- package/lib/path-validate.js +143 -61
- package/lib/pkcs12-build.js +7 -2
- package/lib/pki-build.js +31 -7
- package/lib/possession.js +575 -0
- package/lib/related-cert.js +708 -0
- package/lib/revocation-fetch.js +6 -6
- package/lib/scep.js +15 -5
- package/lib/schema-c509.js +5 -2
- package/lib/schema-cmc.js +1 -1
- package/lib/schema-cmp.js +8 -8
- package/lib/schema-cms.js +2 -15
- package/lib/schema-csr.js +69 -0
- package/lib/schema-engine.js +10 -3
- package/lib/schema-pkix.js +228 -2
- package/lib/schema-tsp.js +7 -6
- package/lib/sign-scheme.js +22 -9
- package/lib/sigstore.js +684 -168
- package/lib/sleep.js +8 -1
- package/lib/smime.js +22 -1
- package/lib/tlog.js +1235 -0
- package/lib/tls-cert-compress.js +25 -8
- package/lib/tsp-sign.js +120 -35
- package/lib/tuf.js +1395 -0
- package/lib/validator-tpm.js +4 -1
- package/lib/verify-seam.js +7 -1
- package/lib/webauthn-mds.js +41 -4
- package/lib/webauthn.js +43 -25
- package/lib/webcrypto.js +48 -46
- package/lib/x509-sign.js +225 -19
- package/package.json +2 -1
- 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.
|
|
12
|
-
* families run the RFC 9180 key schedule: the DHKEM suites P-256, P-
|
|
13
|
-
* and X448 in all four modes (base / psk / auth / auth-psk), proven against
|
|
14
|
-
* RFC 9180 Appendix A known-answer vectors,
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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
|
|
173
|
+
return guard.crypto.hmac(hash, key, ikm);
|
|
92
174
|
}
|
|
93
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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([
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
985
|
+
ss = _decap(kem, sealed, r.key, r.pkm);
|
|
560
986
|
}
|
|
561
|
-
|
|
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(
|
|
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(
|
|
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
|
};
|