lemon-tls 0.7.0 → 0.10.0
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/README.md +258 -20
- package/index.cjs +9 -7
- package/index.d.ts +323 -23
- package/index.js +126 -0
- package/package.json +39 -6
- package/src/compat.js +325 -46
- package/src/crypto.js +19 -0
- package/src/dtls_session.js +141 -17
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +192 -57
- package/src/secure_context.js +63 -4
- package/src/session/cert_compression.js +157 -0
- package/src/session/ecdh.js +237 -10
- package/src/session/ech.js +715 -0
- package/src/session/hpke.js +414 -0
- package/src/session/message.js +39 -4
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +38 -25
- package/src/tls_session.js +2835 -205
- package/src/tls_socket.js +839 -148
- package/src/utils.js +68 -1
- package/src/wire.js +148 -9
- package/types/crypto.d.ts +40 -0
- package/types/record.d.ts +40 -0
- package/types/session.d.ts +3 -0
- package/types/wire.d.ts +62 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HPKE — Hybrid Public Key Encryption (RFC 9180).
|
|
3
|
+
*
|
|
4
|
+
* Encrypting to a public key: the sender makes an ephemeral key pair, a
|
|
5
|
+
* Diffie-Hellman with the recipient's public key gives a shared secret, and
|
|
6
|
+
* from it come an AEAD key and nonce for any number of messages — while
|
|
7
|
+
* only the ephemeral public key (`enc`) has to travel with them. TLS
|
|
8
|
+
* Encrypted Client Hello (ECH) encrypts the real ClientHello this way, to
|
|
9
|
+
* the key a server publishes in DNS; MLS and Oblivious HTTP use it too.
|
|
10
|
+
*
|
|
11
|
+
* All four modes (base, psk, auth, auth_psk); the KEMs, KDFs and AEADs of
|
|
12
|
+
* the RFC's registry that node:crypto has:
|
|
13
|
+
*
|
|
14
|
+
* KEM 0x0010 DHKEM(P-256, HKDF-SHA256) 0x0011 DHKEM(P-384, HKDF-SHA384)
|
|
15
|
+
* 0x0012 DHKEM(P-521, HKDF-SHA512) 0x0020 DHKEM(X25519, HKDF-SHA256)
|
|
16
|
+
* 0x0021 DHKEM(X448, HKDF-SHA512)
|
|
17
|
+
* KDF 0x0001 HKDF-SHA256 0x0002 HKDF-SHA384 0x0003 HKDF-SHA512
|
|
18
|
+
* AEAD 0x0001 AES-128-GCM 0x0002 AES-256-GCM 0x0003 ChaCha20-Poly1305
|
|
19
|
+
* 0xFFFF export-only
|
|
20
|
+
*
|
|
21
|
+
* Synchronous, like the rest of the handshake's crypto.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import crypto from 'node:crypto';
|
|
25
|
+
import { hkdf_extract, hkdf_expand } from '../crypto.js';
|
|
26
|
+
import { x25519_get_public_key, x25519_get_shared_secret } from './ecdh.js';
|
|
27
|
+
|
|
28
|
+
var MODE_BASE = 0x00, MODE_PSK = 0x01, MODE_AUTH = 0x02, MODE_AUTH_PSK = 0x03;
|
|
29
|
+
|
|
30
|
+
var te = new TextEncoder();
|
|
31
|
+
function u8(s) { return te.encode(s); }
|
|
32
|
+
function i2osp(n, len) {
|
|
33
|
+
var out = new Uint8Array(len);
|
|
34
|
+
for (var i = len - 1; i >= 0; i--) { out[i] = n & 0xff; n = Math.floor(n / 256); }
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
function concat() {
|
|
38
|
+
var len = 0, i;
|
|
39
|
+
for (i = 0; i < arguments.length; i++) len += arguments[i].length;
|
|
40
|
+
var out = new Uint8Array(len), off = 0;
|
|
41
|
+
for (i = 0; i < arguments.length; i++) { out.set(arguments[i], off); off += arguments[i].length; }
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
function hexToBig(h) { return BigInt('0x' + h); }
|
|
45
|
+
function bytesToBig(b) { return b.length ? BigInt('0x' + Buffer.from(b).toString('hex')) : 0n; }
|
|
46
|
+
|
|
47
|
+
var HPKE_V1 = u8('HPKE-v1');
|
|
48
|
+
var EMPTY = new Uint8Array(0);
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
// ============================================================
|
|
52
|
+
// Labeled HKDF (RFC 9180 §4)
|
|
53
|
+
// ============================================================
|
|
54
|
+
|
|
55
|
+
function labeled_extract(hash, suite_id, salt, label, ikm) {
|
|
56
|
+
return hkdf_extract(hash, salt, concat(HPKE_V1, suite_id, u8(label), ikm));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function labeled_expand(hash, suite_id, prk, label, info, L) {
|
|
60
|
+
return hkdf_expand(hash, prk, concat(i2osp(L, 2), HPKE_V1, suite_id, u8(label), info), L);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
// ============================================================
|
|
65
|
+
// KEMs: DH groups (RFC 9180 §4.1, §7.1)
|
|
66
|
+
// ============================================================
|
|
67
|
+
|
|
68
|
+
// The two Montgomery curves, through node:crypto's raw-key DER wrapping.
|
|
69
|
+
var X448_PKCS8 = Buffer.from('3046020100300506032b656f043a0438', 'hex');
|
|
70
|
+
var X448_SPKI = Buffer.from('3042300506032b656f033900', 'hex');
|
|
71
|
+
|
|
72
|
+
function x448_public_key(sk) {
|
|
73
|
+
var priv = crypto.createPrivateKey({ key: Buffer.concat([X448_PKCS8, Buffer.from(sk)]), format: 'der', type: 'pkcs8' });
|
|
74
|
+
return new Uint8Array(crypto.createPublicKey(priv).export({ type: 'spki', format: 'der' }).subarray(X448_SPKI.length));
|
|
75
|
+
}
|
|
76
|
+
function x448_shared(sk, pk) {
|
|
77
|
+
var priv = crypto.createPrivateKey({ key: Buffer.concat([X448_PKCS8, Buffer.from(sk)]), format: 'der', type: 'pkcs8' });
|
|
78
|
+
var pub = crypto.createPublicKey({ key: Buffer.concat([X448_SPKI, Buffer.from(pk)]), format: 'der', type: 'spki' });
|
|
79
|
+
var dh = new Uint8Array(crypto.diffieHellman({ privateKey: priv, publicKey: pub }));
|
|
80
|
+
// RFC 9180 §7.1.4 / RFC 7748 §6.2: an all-zero result means a low-order
|
|
81
|
+
// public key — the sender would know the "secret". Constant time.
|
|
82
|
+
var acc = 0;
|
|
83
|
+
for (var i = 0; i < dh.length; i++) acc |= dh[i];
|
|
84
|
+
if (acc === 0) throw new Error('HPKE: X448 shared secret is all zero (invalid public key)');
|
|
85
|
+
return dh;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The NIST curves: a private key is a scalar below the group order;
|
|
89
|
+
// DeriveKeyPair draws candidates until one is (§7.1.3).
|
|
90
|
+
function nist(curve, order, bitmask) {
|
|
91
|
+
return {
|
|
92
|
+
order: order,
|
|
93
|
+
bitmask: bitmask,
|
|
94
|
+
public_key: function (sk) {
|
|
95
|
+
var e = crypto.createECDH(curve); e.setPrivateKey(Buffer.from(sk));
|
|
96
|
+
return new Uint8Array(e.getPublicKey(null, 'uncompressed'));
|
|
97
|
+
},
|
|
98
|
+
// computeSecret validates the peer point (on the curve, not infinity)
|
|
99
|
+
// and gives the x-coordinate: the DH value of §7.1.
|
|
100
|
+
shared: function (sk, pk) {
|
|
101
|
+
var e = crypto.createECDH(curve); e.setPrivateKey(Buffer.from(sk));
|
|
102
|
+
return new Uint8Array(e.computeSecret(Buffer.from(pk)));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var KEMS = {};
|
|
108
|
+
KEMS[0x0010] = Object.assign(nist('prime256v1',
|
|
109
|
+
hexToBig('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'), 0xff),
|
|
110
|
+
{ id: 0x0010, name: 'DHKEM(P-256, HKDF-SHA256)', hash: 'sha256', Nsecret: 32, Nenc: 65, Npk: 65, Nsk: 32 });
|
|
111
|
+
KEMS[0x0011] = Object.assign(nist('secp384r1',
|
|
112
|
+
hexToBig('ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973'), 0xff),
|
|
113
|
+
{ id: 0x0011, name: 'DHKEM(P-384, HKDF-SHA384)', hash: 'sha384', Nsecret: 48, Nenc: 97, Npk: 97, Nsk: 48 });
|
|
114
|
+
KEMS[0x0012] = Object.assign(nist('secp521r1',
|
|
115
|
+
hexToBig('01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'), 0x01),
|
|
116
|
+
{ id: 0x0012, name: 'DHKEM(P-521, HKDF-SHA512)', hash: 'sha512', Nsecret: 64, Nenc: 133, Npk: 133, Nsk: 66 });
|
|
117
|
+
KEMS[0x0020] = { id: 0x0020, name: 'DHKEM(X25519, HKDF-SHA256)', hash: 'sha256', Nsecret: 32, Nenc: 32, Npk: 32, Nsk: 32,
|
|
118
|
+
public_key: x25519_get_public_key,
|
|
119
|
+
shared: x25519_get_shared_secret }; // rejects the all-zero result itself
|
|
120
|
+
KEMS[0x0021] = { id: 0x0021, name: 'DHKEM(X448, HKDF-SHA512)', hash: 'sha512', Nsecret: 64, Nenc: 56, Npk: 56, Nsk: 56,
|
|
121
|
+
public_key: x448_public_key,
|
|
122
|
+
shared: x448_shared };
|
|
123
|
+
|
|
124
|
+
function kem_suite_id(kem) { return concat(u8('KEM'), i2osp(kem.id, 2)); }
|
|
125
|
+
|
|
126
|
+
/** §7.1.3: a key pair from `ikm` (at least Nsk bytes of entropy). */
|
|
127
|
+
function kem_derive_key_pair(kem, ikm) {
|
|
128
|
+
var sid = kem_suite_id(kem);
|
|
129
|
+
var prk = labeled_extract(kem.hash, sid, EMPTY, 'dkp_prk', ikm);
|
|
130
|
+
var sk;
|
|
131
|
+
if (!kem.order) {
|
|
132
|
+
sk = labeled_expand(kem.hash, sid, prk, 'sk', EMPTY, kem.Nsk);
|
|
133
|
+
} else {
|
|
134
|
+
for (var counter = 0; ; counter++) {
|
|
135
|
+
if (counter > 255) throw new Error('HPKE: DeriveKeyPair found no key');
|
|
136
|
+
var bytes = labeled_expand(kem.hash, sid, prk, 'candidate', i2osp(counter, 1), kem.Nsk);
|
|
137
|
+
bytes[0] &= kem.bitmask;
|
|
138
|
+
var n = bytesToBig(bytes);
|
|
139
|
+
if (n !== 0n && n < kem.order) { sk = bytes; break; }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { private_key: sk, public_key: kem.public_key(sk) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function kem_generate_key_pair(kem) {
|
|
146
|
+
return kem_derive_key_pair(kem, new Uint8Array(crypto.randomBytes(kem.Nsk)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function kem_check_public_key(kem, pk) {
|
|
150
|
+
if (!(pk instanceof Uint8Array) || pk.length !== kem.Npk) throw new Error('HPKE: public key must be ' + kem.Npk + ' bytes for ' + kem.name);
|
|
151
|
+
if (kem.order && pk[0] !== 0x04) throw new Error('HPKE: public key is not an uncompressed point');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function kem_extract_and_expand(kem, dh, kem_context) {
|
|
155
|
+
var sid = kem_suite_id(kem);
|
|
156
|
+
var prk = labeled_extract(kem.hash, sid, EMPTY, 'eae_prk', dh);
|
|
157
|
+
return labeled_expand(kem.hash, sid, prk, 'shared_secret', kem_context, kem.Nsecret);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// §4.1 Encap / Decap, and their authenticated forms. `ephemeral` replaces
|
|
161
|
+
// the random key pair — for the RFC's test vectors only.
|
|
162
|
+
function kem_encap(kem, pkR, skS, ephemeral) {
|
|
163
|
+
kem_check_public_key(kem, pkR);
|
|
164
|
+
var e = ephemeral || kem_generate_key_pair(kem);
|
|
165
|
+
var dh = kem.shared(e.private_key, pkR);
|
|
166
|
+
var kem_context = concat(e.public_key, pkR);
|
|
167
|
+
if (skS) {
|
|
168
|
+
dh = concat(dh, kem.shared(skS, pkR));
|
|
169
|
+
kem_context = concat(kem_context, kem.public_key(skS));
|
|
170
|
+
}
|
|
171
|
+
return { shared_secret: kem_extract_and_expand(kem, dh, kem_context), enc: e.public_key };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function kem_decap(kem, enc, skR, pkS, pkR) {
|
|
175
|
+
kem_check_public_key(kem, enc);
|
|
176
|
+
var dh = kem.shared(skR, enc);
|
|
177
|
+
var kem_context = concat(enc, pkR || kem.public_key(skR));
|
|
178
|
+
if (pkS) {
|
|
179
|
+
kem_check_public_key(kem, pkS);
|
|
180
|
+
dh = concat(dh, kem.shared(skR, pkS));
|
|
181
|
+
kem_context = concat(kem_context, pkS);
|
|
182
|
+
}
|
|
183
|
+
return kem_extract_and_expand(kem, dh, kem_context);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
// ============================================================
|
|
188
|
+
// KDFs and AEADs (RFC 9180 §7.2, §7.3)
|
|
189
|
+
// ============================================================
|
|
190
|
+
|
|
191
|
+
var KDFS = {
|
|
192
|
+
0x0001: { id: 0x0001, hash: 'sha256', Nh: 32 },
|
|
193
|
+
0x0002: { id: 0x0002, hash: 'sha384', Nh: 48 },
|
|
194
|
+
0x0003: { id: 0x0003, hash: 'sha512', Nh: 64 }
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
var AEADS = {
|
|
198
|
+
0x0001: { id: 0x0001, algo: 'aes-128-gcm', Nk: 16, Nn: 12, Nt: 16 },
|
|
199
|
+
0x0002: { id: 0x0002, algo: 'aes-256-gcm', Nk: 32, Nn: 12, Nt: 16 },
|
|
200
|
+
0x0003: { id: 0x0003, algo: 'chacha20-poly1305', Nk: 32, Nn: 12, Nt: 16 },
|
|
201
|
+
0xffff: { id: 0xffff, algo: null, Nk: 0, Nn: 0, Nt: 0 } // export-only
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
function aead_seal(aead, key, nonce, aad, pt) {
|
|
205
|
+
var c = crypto.createCipheriv(aead.algo, key, nonce, { authTagLength: aead.Nt });
|
|
206
|
+
c.setAAD(aad);
|
|
207
|
+
var a = c.update(pt), b = c.final();
|
|
208
|
+
return new Uint8Array(Buffer.concat([a, b, c.getAuthTag()]));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function aead_open(aead, key, nonce, aad, ct) {
|
|
212
|
+
if (ct.length < aead.Nt) return null;
|
|
213
|
+
var d = crypto.createDecipheriv(aead.algo, key, nonce, { authTagLength: aead.Nt });
|
|
214
|
+
d.setAAD(aad);
|
|
215
|
+
d.setAuthTag(ct.subarray(ct.length - aead.Nt));
|
|
216
|
+
try {
|
|
217
|
+
var a = d.update(ct.subarray(0, ct.length - aead.Nt)), b = d.final();
|
|
218
|
+
return new Uint8Array(Buffer.concat([a, b]));
|
|
219
|
+
} catch (e) { return null; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
// ============================================================
|
|
224
|
+
// Suites and the key schedule (RFC 9180 §5.1)
|
|
225
|
+
// ============================================================
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A cipher suite by its registry ids. Throws on one this build does not
|
|
229
|
+
* have. `{ kem, kdf, aead, id: suite_id, Nenc, … }`.
|
|
230
|
+
*/
|
|
231
|
+
function hpke_suite(kem_id, kdf_id, aead_id) {
|
|
232
|
+
var kem = KEMS[kem_id], kdf = KDFS[kdf_id], aead = AEADS[aead_id];
|
|
233
|
+
if (!kem) throw new Error('HPKE: unsupported KEM 0x' + (kem_id >>> 0).toString(16));
|
|
234
|
+
if (!kdf) throw new Error('HPKE: unsupported KDF 0x' + (kdf_id >>> 0).toString(16));
|
|
235
|
+
if (!aead) throw new Error('HPKE: unsupported AEAD 0x' + (aead_id >>> 0).toString(16));
|
|
236
|
+
return {
|
|
237
|
+
kem: kem, kdf: kdf, aead: aead,
|
|
238
|
+
suite_id: concat(u8('HPKE'), i2osp(kem.id, 2), i2osp(kdf.id, 2), i2osp(aead.id, 2)),
|
|
239
|
+
Nenc: kem.Nenc, Npk: kem.Npk, Nsk: kem.Nsk, Nk: aead.Nk, Nn: aead.Nn, Nt: aead.Nt
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function is_supported_hpke_suite(kem_id, kdf_id, aead_id) {
|
|
244
|
+
return !!(KEMS[kem_id] && KDFS[kdf_id] && AEADS[aead_id]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function verify_psk_inputs(mode, psk, psk_id) {
|
|
248
|
+
var got_psk = psk.length > 0, got_id = psk_id.length > 0;
|
|
249
|
+
if (got_psk !== got_id) throw new Error('HPKE: psk and psk_id must be given together');
|
|
250
|
+
var needs = (mode === MODE_PSK || mode === MODE_AUTH_PSK);
|
|
251
|
+
if (got_psk !== needs) throw new Error(needs ? 'HPKE: this mode needs a psk' : 'HPKE: a psk was given to a mode without one');
|
|
252
|
+
// §5.1.1: at least 32 bytes of entropy
|
|
253
|
+
if (got_psk && psk.length < 32) throw new Error('HPKE: psk must be at least 32 bytes');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function key_schedule(suite, mode, shared_secret, info, psk, psk_id, role) {
|
|
257
|
+
verify_psk_inputs(mode, psk, psk_id);
|
|
258
|
+
var h = suite.kdf.hash, sid = suite.suite_id;
|
|
259
|
+
var psk_id_hash = labeled_extract(h, sid, EMPTY, 'psk_id_hash', psk_id);
|
|
260
|
+
var info_hash = labeled_extract(h, sid, EMPTY, 'info_hash', info);
|
|
261
|
+
var key_schedule_context = concat(new Uint8Array([mode]), psk_id_hash, info_hash);
|
|
262
|
+
var secret = labeled_extract(h, sid, shared_secret, 'secret', psk);
|
|
263
|
+
|
|
264
|
+
var key = EMPTY, base_nonce = EMPTY;
|
|
265
|
+
if (suite.aead.algo) {
|
|
266
|
+
key = labeled_expand(h, sid, secret, 'key', key_schedule_context, suite.Nk);
|
|
267
|
+
base_nonce = labeled_expand(h, sid, secret, 'base_nonce', key_schedule_context, suite.Nn);
|
|
268
|
+
}
|
|
269
|
+
var exporter_secret = labeled_expand(h, sid, secret, 'exp', key_schedule_context, suite.kdf.Nh);
|
|
270
|
+
return new HpkeContext(suite, role, key, base_nonce, exporter_secret);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
// ============================================================
|
|
275
|
+
// The encryption context (RFC 9180 §5.2, §5.3)
|
|
276
|
+
// ============================================================
|
|
277
|
+
|
|
278
|
+
function HpkeContext(suite, role, key, base_nonce, exporter_secret) {
|
|
279
|
+
this.suite = suite;
|
|
280
|
+
this.role = role; // 'sender' | 'recipient'
|
|
281
|
+
this.key = key;
|
|
282
|
+
this.base_nonce = base_nonce;
|
|
283
|
+
this.exporter_secret = exporter_secret;
|
|
284
|
+
this.seq = 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
HpkeContext.prototype._nonce = function () {
|
|
288
|
+
// base_nonce XOR seq, seq big-endian in the nonce's low bytes. 2^53
|
|
289
|
+
// messages is far past any use; seal/open refuse beyond it.
|
|
290
|
+
if (this.seq >= Number.MAX_SAFE_INTEGER) throw new Error('HPKE: message limit reached');
|
|
291
|
+
var n = this.base_nonce.slice(0), s = i2osp(this.seq, n.length);
|
|
292
|
+
for (var i = 0; i < n.length; i++) n[i] ^= s[i];
|
|
293
|
+
return n;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/** Encrypt the next message. Sender only. */
|
|
297
|
+
HpkeContext.prototype.seal = function (aad, pt) {
|
|
298
|
+
if (this.role !== 'sender') throw new Error('HPKE: seal on a recipient context');
|
|
299
|
+
if (!this.suite.aead.algo) throw new Error('HPKE: export-only suite cannot seal');
|
|
300
|
+
var ct = aead_seal(this.suite.aead, this.key, this._nonce(), aad || EMPTY, pt);
|
|
301
|
+
this.seq++;
|
|
302
|
+
return ct;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Decrypt the next message: the plaintext, or null if it does not open —
|
|
307
|
+
* and then the sequence does not advance, so the next valid message still
|
|
308
|
+
* does. Recipient only.
|
|
309
|
+
*/
|
|
310
|
+
HpkeContext.prototype.open = function (aad, ct) {
|
|
311
|
+
if (this.role !== 'recipient') throw new Error('HPKE: open on a sender context');
|
|
312
|
+
if (!this.suite.aead.algo) throw new Error('HPKE: export-only suite cannot open');
|
|
313
|
+
var pt = aead_open(this.suite.aead, this.key, this._nonce(), aad || EMPTY, ct);
|
|
314
|
+
if (pt) this.seq++;
|
|
315
|
+
return pt;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/** §5.3: a secret of `L` bytes for `exporter_context`. */
|
|
319
|
+
HpkeContext.prototype.export = function (exporter_context, L) {
|
|
320
|
+
if (!(L >= 0) || L > 255 * this.suite.kdf.Nh) throw new Error('HPKE: export length out of range');
|
|
321
|
+
return labeled_expand(this.suite.kdf.hash, this.suite.suite_id, this.exporter_secret, 'sec', exporter_context || EMPTY, L);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
// ============================================================
|
|
326
|
+
// Setup (RFC 9180 §5.1.1 - §5.1.4)
|
|
327
|
+
// ============================================================
|
|
328
|
+
|
|
329
|
+
function opt_bytes(v) { return v ? (v instanceof Uint8Array ? v : new Uint8Array(v)) : EMPTY; }
|
|
330
|
+
|
|
331
|
+
function mode_of(opts) {
|
|
332
|
+
var psk = !!(opts && opts.psk), auth = !!(opts && (opts.sender_private_key || opts.sender_public_key));
|
|
333
|
+
return auth ? (psk ? MODE_AUTH_PSK : MODE_AUTH) : (psk ? MODE_PSK : MODE_BASE);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The sender's side: `{ enc, context }`. `enc` goes to the recipient with
|
|
338
|
+
* the messages; `context.seal()` encrypts them.
|
|
339
|
+
*
|
|
340
|
+
* opts (all optional): `psk` + `psk_id` (psk modes), `sender_private_key`
|
|
341
|
+
* (auth modes), `ephemeral` — a `{ private_key, public_key }` to use in
|
|
342
|
+
* place of a fresh one, for test vectors only.
|
|
343
|
+
*/
|
|
344
|
+
function setup_sender(suite, recipient_public_key, info, opts) {
|
|
345
|
+
opts = opts || {};
|
|
346
|
+
var mode = mode_of(opts);
|
|
347
|
+
var r = kem_encap(suite.kem, recipient_public_key, opts.sender_private_key || null, opts.ephemeral || null);
|
|
348
|
+
var ctx = key_schedule(suite, mode, r.shared_secret, opt_bytes(info), opt_bytes(opts.psk), opt_bytes(opts.psk_id), 'sender');
|
|
349
|
+
return { enc: r.enc, context: ctx };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The recipient's side: the context to `open()` with. Throws when `enc` is
|
|
354
|
+
* not a valid public key; a wrong key or info shows as `open()` → null.
|
|
355
|
+
*
|
|
356
|
+
* opts: `psk` + `psk_id`, `sender_public_key` (auth modes),
|
|
357
|
+
* `recipient_public_key` — the public key of `recipient_private_key`, when
|
|
358
|
+
* known, saving a scalar multiplication per setup (it must be that key's).
|
|
359
|
+
*/
|
|
360
|
+
function setup_recipient(suite, enc, recipient_private_key, info, opts) {
|
|
361
|
+
opts = opts || {};
|
|
362
|
+
var mode = mode_of(opts);
|
|
363
|
+
var ss = kem_decap(suite.kem, opt_bytes(enc), recipient_private_key, opts.sender_public_key || null,
|
|
364
|
+
opts.recipient_public_key || null);
|
|
365
|
+
return key_schedule(suite, mode, ss, opt_bytes(info), opt_bytes(opts.psk), opt_bytes(opts.psk_id), 'recipient');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Single-shot (§6.1): one message. `{ enc, ct }`. */
|
|
369
|
+
function hpke_seal(suite, recipient_public_key, info, aad, pt, opts) {
|
|
370
|
+
var s = setup_sender(suite, recipient_public_key, info, opts);
|
|
371
|
+
return { enc: s.enc, ct: s.context.seal(aad, pt) };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Single-shot (§6.1): the plaintext, or null. */
|
|
375
|
+
function hpke_open(suite, enc, recipient_private_key, info, aad, ct, opts) {
|
|
376
|
+
var ctx;
|
|
377
|
+
try { ctx = setup_recipient(suite, enc, recipient_private_key, info, opts); }
|
|
378
|
+
catch (e) { return null; }
|
|
379
|
+
return ctx.open(aad, ct);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** A new key pair for `kem_id`: `{ private_key, public_key }`, raw bytes. */
|
|
383
|
+
function hpke_generate_key_pair(kem_id) {
|
|
384
|
+
var kem = KEMS[kem_id];
|
|
385
|
+
if (!kem) throw new Error('HPKE: unsupported KEM 0x' + (kem_id >>> 0).toString(16));
|
|
386
|
+
return kem_generate_key_pair(kem);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** The key pair `ikm` determines (§7.1.3). */
|
|
390
|
+
function hpke_derive_key_pair(kem_id, ikm) {
|
|
391
|
+
var kem = KEMS[kem_id];
|
|
392
|
+
if (!kem) throw new Error('HPKE: unsupported KEM 0x' + (kem_id >>> 0).toString(16));
|
|
393
|
+
return kem_derive_key_pair(kem, opt_bytes(ikm));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** The public key of a private key. */
|
|
397
|
+
function hpke_public_key(kem_id, private_key) {
|
|
398
|
+
var kem = KEMS[kem_id];
|
|
399
|
+
if (!kem) throw new Error('HPKE: unsupported KEM 0x' + (kem_id >>> 0).toString(16));
|
|
400
|
+
return kem.public_key(private_key);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export {
|
|
404
|
+
MODE_BASE, MODE_PSK, MODE_AUTH, MODE_AUTH_PSK,
|
|
405
|
+
hpke_suite,
|
|
406
|
+
is_supported_hpke_suite,
|
|
407
|
+
setup_sender,
|
|
408
|
+
setup_recipient,
|
|
409
|
+
hpke_seal,
|
|
410
|
+
hpke_open,
|
|
411
|
+
hpke_generate_key_pair,
|
|
412
|
+
hpke_derive_key_pair,
|
|
413
|
+
hpke_public_key,
|
|
414
|
+
};
|
package/src/session/message.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as wire from '../wire.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Normalize a parsed hello into a flat object with named extension fields.
|
|
11
11
|
*/
|
|
12
|
-
function normalize_hello(hello) {
|
|
12
|
+
function normalize_hello(hello, kind) {
|
|
13
13
|
let out = {};
|
|
14
14
|
|
|
15
15
|
for (let key in hello) {
|
|
@@ -28,9 +28,16 @@ function normalize_hello(hello) {
|
|
|
28
28
|
out.alpn = value;
|
|
29
29
|
} else if (name === 'KEY_SHARE') {
|
|
30
30
|
if (!('key_groups' in out)) out.key_groups = [];
|
|
31
|
-
|
|
31
|
+
// A ServerHello's key_share names the group chosen, which the
|
|
32
|
+
// client's code reads as its supported_groups. A ClientHello's
|
|
33
|
+
// shares are NOT added to its supported_groups: they came only from
|
|
34
|
+
// that extension. Merged, the result depended on which extension came
|
|
35
|
+
// first (supported_groups after key_share replaced the list; before
|
|
36
|
+
// it, the shares' groups were appended), and a share for a group the
|
|
37
|
+
// client never listed looked listed (RFC 8446 §4.2.8).
|
|
38
|
+
if (kind !== 'client' && !('supported_groups' in out)) out.supported_groups = [];
|
|
32
39
|
for (let i2 = 0; i2 < value.length; i2++) {
|
|
33
|
-
if (out.supported_groups.indexOf(value[i2].group) < 0) {
|
|
40
|
+
if (kind !== 'client' && out.supported_groups.indexOf(value[i2].group) < 0) {
|
|
34
41
|
out.supported_groups.push(value[i2].group);
|
|
35
42
|
}
|
|
36
43
|
out.key_groups.push({
|
|
@@ -60,6 +67,8 @@ function normalize_hello(hello) {
|
|
|
60
67
|
out.status_request = value;
|
|
61
68
|
} else if (name === 'MAX_FRAGMENT_LENGTH') {
|
|
62
69
|
out.max_fragment_length = value;
|
|
70
|
+
} else if (name === 'COMPRESS_CERTIFICATE') {
|
|
71
|
+
out.compress_certificate = value;
|
|
63
72
|
} else if (name === 'CERTIFICATE_AUTHORITIES') {
|
|
64
73
|
out.certificate_authorities = value;
|
|
65
74
|
} else if (name === 'SCT') {
|
|
@@ -127,12 +136,21 @@ function build_tls_message(params) {
|
|
|
127
136
|
} else if (params.type == 'certificate') {
|
|
128
137
|
type = wire.TLS_MESSAGE_TYPE.CERTIFICATE;
|
|
129
138
|
body = wire.build_certificate(params);
|
|
139
|
+
} else if (params.type == 'compressed_certificate') {
|
|
140
|
+
type = wire.TLS_MESSAGE_TYPE.COMPRESSED_CERTIFICATE;
|
|
141
|
+
body = wire.build_compressed_certificate(params);
|
|
142
|
+
} else if (params.type == 'certificate_status') {
|
|
143
|
+
type = wire.TLS_MESSAGE_TYPE.CERTIFICATE_STATUS;
|
|
144
|
+
body = wire.build_certificate_status(params.response);
|
|
130
145
|
} else if (params.type == 'certificate_verify') {
|
|
131
146
|
type = wire.TLS_MESSAGE_TYPE.CERTIFICATE_VERIFY;
|
|
132
147
|
body = wire.build_certificate_verify(params.scheme, params.signature);
|
|
133
148
|
} else if (params.type == 'finished') {
|
|
134
149
|
type = wire.TLS_MESSAGE_TYPE.FINISHED;
|
|
135
150
|
body = params.data;
|
|
151
|
+
} else if (params.type == 'end_of_early_data') {
|
|
152
|
+
type = wire.TLS_MESSAGE_TYPE.END_OF_EARLY_DATA;
|
|
153
|
+
body = new Uint8Array(0);
|
|
136
154
|
} else if (params.type == 'key_update') {
|
|
137
155
|
type = wire.TLS_MESSAGE_TYPE.KEY_UPDATE;
|
|
138
156
|
body = wire.build_key_update(params.request_update);
|
|
@@ -167,7 +185,7 @@ function parse_tls_message(data, negotiatedVersion) {
|
|
|
167
185
|
if (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_HELLO || message.type == wire.TLS_MESSAGE_TYPE.SERVER_HELLO) {
|
|
168
186
|
let kind = (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_HELLO) ? 'client' : 'server';
|
|
169
187
|
let hello = wire.parse_hello({ kind: kind, body: message.body });
|
|
170
|
-
out = normalize_hello(hello);
|
|
188
|
+
out = normalize_hello(hello, kind);
|
|
171
189
|
|
|
172
190
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.SERVER_KEY_EXCHANGE) {
|
|
173
191
|
out = wire.parse_server_key_exchange(message.body);
|
|
@@ -192,6 +210,17 @@ function parse_tls_message(data, negotiatedVersion) {
|
|
|
192
210
|
out = wire.parse_certificate(message.body);
|
|
193
211
|
out.type = 'certificate';
|
|
194
212
|
|
|
213
|
+
} else if (message.type == wire.TLS_MESSAGE_TYPE.COMPRESSED_CERTIFICATE) {
|
|
214
|
+
// RFC 8879: decompressed by the session, which knows what it offered.
|
|
215
|
+
out = wire.parse_compressed_certificate(message.body);
|
|
216
|
+
out.type = 'compressed_certificate';
|
|
217
|
+
|
|
218
|
+
} else if (message.type == wire.TLS_MESSAGE_TYPE.CERTIFICATE_STATUS) {
|
|
219
|
+
// TLS 1.2: the stapled OCSP response, between Certificate and
|
|
220
|
+
// ServerKeyExchange (RFC 6066 §8).
|
|
221
|
+
out = wire.parse_certificate_status(message.body);
|
|
222
|
+
out.type = 'certificate_status';
|
|
223
|
+
|
|
195
224
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.CERTIFICATE_VERIFY) {
|
|
196
225
|
// Decode scheme + signature so the session can actually VERIFY it.
|
|
197
226
|
// Previously only the raw body was handed up and no caller ever checked
|
|
@@ -219,6 +248,12 @@ function parse_tls_message(data, negotiatedVersion) {
|
|
|
219
248
|
}
|
|
220
249
|
out.type = 'new_session_ticket';
|
|
221
250
|
|
|
251
|
+
} else if (message.type == wire.TLS_MESSAGE_TYPE.END_OF_EARLY_DATA) {
|
|
252
|
+
// TLS over TCP, after early data (RFC 8446 §4.5): an empty body.
|
|
253
|
+
if (message.body.length !== 0) { let e = new Error('EndOfEarlyData with a body'); e.alertDesc = wire.TLS_ALERT.DECODE_ERROR; throw e; }
|
|
254
|
+
out.type = 'end_of_early_data';
|
|
255
|
+
out.body = message.body;
|
|
256
|
+
|
|
222
257
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.KEY_UPDATE) {
|
|
223
258
|
out = wire.parse_key_update(message.body);
|
|
224
259
|
out.type = 'key_update';
|
package/src/session/signing.js
CHANGED
|
@@ -36,6 +36,27 @@ const SIG_SCHEMES_MODERN = [
|
|
|
36
36
|
0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
|
|
37
37
|
];
|
|
38
38
|
|
|
39
|
+
// ML-DSA (FIPS 204), post-quantum signatures, TLS 1.3 only
|
|
40
|
+
// (draft-ietf-tls-mldsa): mldsa44, mldsa65, mldsa87 — where node:crypto has
|
|
41
|
+
// them (Node 24.7+, OpenSSL 3.5). Offered after the classical schemes: a
|
|
42
|
+
// server holding both an ML-DSA and a classical certificate (key/cert
|
|
43
|
+
// arrays) follows the client's order, and an ML-DSA chain is 3–4× the size
|
|
44
|
+
// of an ECDSA one — kilobytes more in every handshake — so a client gets it
|
|
45
|
+
// when it asks for it first. Offered at all so that a server with ML-DSA
|
|
46
|
+
// alone can be verified.
|
|
47
|
+
const MLDSA_SCHEMES = { 0x0904: 'ml-dsa-44', 0x0905: 'ml-dsa-65', 0x0906: 'ml-dsa-87' };
|
|
48
|
+
const MLDSA_AVAILABLE = (function () {
|
|
49
|
+
try {
|
|
50
|
+
for (const code in MLDSA_SCHEMES) {
|
|
51
|
+
let kp = crypto.generateKeyPairSync(MLDSA_SCHEMES[code]);
|
|
52
|
+
let sig = crypto.sign(null, Buffer.from('x'), kp.privateKey);
|
|
53
|
+
if (!crypto.verify(null, Buffer.from('x'), kp.publicKey, sig)) return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
} catch (e) { return false; }
|
|
57
|
+
})();
|
|
58
|
+
const SIG_SCHEMES_MLDSA = MLDSA_AVAILABLE ? [0x0904, 0x0905, 0x0906] : [];
|
|
59
|
+
|
|
39
60
|
const SIG_SCHEMES_LEGACY_PKCS1 = [
|
|
40
61
|
0x0401, 0x0501, 0x0601, // rsa_pkcs1_sha256/384/512
|
|
41
62
|
];
|
|
@@ -73,6 +94,7 @@ const SIG_SCHEMES_LEGACY_SHA1 = [
|
|
|
73
94
|
*/
|
|
74
95
|
function default_signature_schemes() {
|
|
75
96
|
return SIG_SCHEMES_MODERN
|
|
97
|
+
.concat(SIG_SCHEMES_MLDSA)
|
|
76
98
|
.concat(SIG_SCHEMES_LEGACY_PKCS1)
|
|
77
99
|
.concat(SIG_SCHEMES_LEGACY_SHA1);
|
|
78
100
|
}
|
|
@@ -98,6 +120,13 @@ function scheme_info(version, scheme) {
|
|
|
98
120
|
// verify_with_scheme share this function, so signing and verification can
|
|
99
121
|
// never drift apart.
|
|
100
122
|
|
|
123
|
+
// ML-DSA: TLS 1.3 only (draft-ietf-tls-mldsa §3), pure ML-DSA with an
|
|
124
|
+
// empty context string — the whole TLS 1.3 signature input, unhashed.
|
|
125
|
+
if (MLDSA_SCHEMES[scheme]) {
|
|
126
|
+
if (!MLDSA_AVAILABLE || version !== TLS_VERSION_TLS1_3) return null;
|
|
127
|
+
return { hash: null, sig: 'mldsa', isPSS: false };
|
|
128
|
+
}
|
|
129
|
+
|
|
101
130
|
// EdDSA
|
|
102
131
|
if (scheme === 0x0807 || scheme === 0x0808) {
|
|
103
132
|
return { hash: null, sig: 'eddsa', isPSS: false };
|
|
@@ -129,8 +158,13 @@ function scheme_info(version, scheme) {
|
|
|
129
158
|
|
|
130
159
|
/**
|
|
131
160
|
* Pick the best signature scheme given the server's key type and client's supported list.
|
|
161
|
+
* `localAllowed`, when given and not empty: our own list — only a scheme in
|
|
162
|
+
* both is used (OpenSSL's "shared sigalgs"). Without it, the `sigalgs` a
|
|
163
|
+
* server was configured with limited what it offered and accepted, and not
|
|
164
|
+
* what it signed with: an ECDSA key restricted to RSA-PSS still signed with
|
|
165
|
+
* ECDSA.
|
|
132
166
|
*/
|
|
133
|
-
function pick_scheme(version, certKeyObj, peerSupported) {
|
|
167
|
+
function pick_scheme(version, certKeyObj, peerSupported, localAllowed) {
|
|
134
168
|
// Iterate the PEER's list, in the peer's own order.
|
|
135
169
|
//
|
|
136
170
|
// RFC 8446 §4.2.3: signature_algorithms is "in descending order of
|
|
@@ -151,6 +185,7 @@ function pick_scheme(version, certKeyObj, peerSupported) {
|
|
|
151
185
|
for (let i = 0; i < peerSupported.length; i++) {
|
|
152
186
|
let s2 = peerSupported[i];
|
|
153
187
|
if (typeof s2 !== 'number') continue;
|
|
188
|
+
if (Array.isArray(localAllowed) && localAllowed.length > 0 && localAllowed.indexOf(s2) < 0) continue;
|
|
154
189
|
if (!scheme_matches_key(version, s2, certKeyObj)) continue;
|
|
155
190
|
picked = s2;
|
|
156
191
|
break;
|
|
@@ -168,7 +203,7 @@ function sign_with_scheme(version, scheme, tbs, certKeyObj) {
|
|
|
168
203
|
let info = scheme_info(version, scheme);
|
|
169
204
|
if (!info) return null;
|
|
170
205
|
|
|
171
|
-
if (info.sig === 'eddsa') {
|
|
206
|
+
if (info.sig === 'eddsa' || info.sig === 'mldsa') {
|
|
172
207
|
return new Uint8Array(crypto.sign(null, tbs, certKeyObj));
|
|
173
208
|
}
|
|
174
209
|
|
|
@@ -213,6 +248,7 @@ function sign_with_scheme(version, scheme, tbs, certKeyObj) {
|
|
|
213
248
|
* CertificateVerify (scheme_info returns null there)
|
|
214
249
|
* ecdsa_* → EC key on exactly the curve the scheme names
|
|
215
250
|
* ed25519/ed448 → the matching Edwards key
|
|
251
|
+
* mldsa44/65/87 → the ML-DSA key of that parameter set
|
|
216
252
|
* Returns false for any scheme unusable at this version, so callers get
|
|
217
253
|
* version filtering for free.
|
|
218
254
|
*/
|
|
@@ -247,6 +283,8 @@ function scheme_matches_key(version, scheme, keyObj) {
|
|
|
247
283
|
return want === null || curve === want;
|
|
248
284
|
}
|
|
249
285
|
|
|
286
|
+
if (info.sig === 'mldsa') return kt === MLDSA_SCHEMES[scheme];
|
|
287
|
+
|
|
250
288
|
if (info.sig === 'eddsa') {
|
|
251
289
|
if (scheme === 0x0807) return kt === 'ed25519';
|
|
252
290
|
if (scheme === 0x0808) return kt === 'ed448';
|
|
@@ -277,7 +315,7 @@ function verify_with_scheme(version, scheme, tbs, publicKey, signature) {
|
|
|
277
315
|
if (!scheme_matches_key(version, scheme, publicKey)) return false;
|
|
278
316
|
|
|
279
317
|
try {
|
|
280
|
-
if (info.sig === 'eddsa') {
|
|
318
|
+
if (info.sig === 'eddsa' || info.sig === 'mldsa') {
|
|
281
319
|
return crypto.verify(null, tbs, publicKey, signature);
|
|
282
320
|
}
|
|
283
321
|
|
|
@@ -310,7 +348,20 @@ function verify_with_scheme(version, scheme, tbs, publicKey, signature) {
|
|
|
310
348
|
return false;
|
|
311
349
|
}
|
|
312
350
|
|
|
351
|
+
// IANA's names (RFC 8446 §4.2.3; draft-ietf-tls-mldsa).
|
|
352
|
+
const SCHEME_NAMES = {
|
|
353
|
+
0x0401: 'rsa_pkcs1_sha256', 0x0501: 'rsa_pkcs1_sha384', 0x0601: 'rsa_pkcs1_sha512',
|
|
354
|
+
0x0403: 'ecdsa_secp256r1_sha256', 0x0503: 'ecdsa_secp384r1_sha384', 0x0603: 'ecdsa_secp521r1_sha512',
|
|
355
|
+
0x0804: 'rsa_pss_rsae_sha256', 0x0805: 'rsa_pss_rsae_sha384', 0x0806: 'rsa_pss_rsae_sha512',
|
|
356
|
+
0x0807: 'ed25519', 0x0808: 'ed448',
|
|
357
|
+
0x0809: 'rsa_pss_pss_sha256', 0x080a: 'rsa_pss_pss_sha384', 0x080b: 'rsa_pss_pss_sha512',
|
|
358
|
+
0x0904: 'mldsa44', 0x0905: 'mldsa65', 0x0906: 'mldsa87',
|
|
359
|
+
0x0201: 'rsa_pkcs1_sha1', 0x0203: 'ecdsa_sha1',
|
|
360
|
+
};
|
|
361
|
+
function scheme_name(scheme) { return SCHEME_NAMES[scheme] || null; }
|
|
362
|
+
|
|
313
363
|
export {
|
|
364
|
+
scheme_name,
|
|
314
365
|
scheme_info,
|
|
315
366
|
pick_scheme,
|
|
316
367
|
sign_with_scheme,
|
|
@@ -318,5 +369,7 @@ export {
|
|
|
318
369
|
SIG_SCHEMES_MODERN,
|
|
319
370
|
SIG_SCHEMES_LEGACY_PKCS1,
|
|
320
371
|
SIG_SCHEMES_LEGACY_SHA1,
|
|
372
|
+
SIG_SCHEMES_MLDSA,
|
|
373
|
+
MLDSA_AVAILABLE,
|
|
321
374
|
default_signature_schemes
|
|
322
375
|
};
|