lemon-tls 0.9.0 → 0.11.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 +168 -41
- package/index.cjs +9 -7
- package/index.d.ts +626 -30
- package/package.json +45 -7
- package/src/ca_store.js +31 -0
- package/src/compat.js +76 -116
- package/src/crypto.js +21 -114
- package/src/dtls_session.js +1284 -390
- package/src/dtls_socket.js +318 -96
- package/src/node_options.js +154 -17
- package/src/record.js +355 -191
- package/src/secure_context.js +97 -39
- package/src/session/ecdh.js +140 -122
- package/src/session/ech.js +92 -32
- package/src/session/hpke.js +12 -44
- package/src/session/message.js +29 -9
- package/src/session/signing.js +80 -22
- package/src/session/ticket.js +5 -3
- package/src/session/x509.js +385 -0
- package/src/tls_session.js +2221 -902
- package/src/tls_socket.js +451 -452
- package/src/wire.js +271 -158
- 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
package/src/secure_context.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
+
import * as PEM from 'cryptographic-formats/pem';
|
|
5
|
+
import * as PKCS8 from 'cryptographic-formats/pkcs8';
|
|
6
|
+
import { parse_cert } from './session/x509.js';
|
|
4
7
|
|
|
5
8
|
function looksLikePath(x) {
|
|
6
9
|
return typeof x === 'string' && (x.indexOf('\n') === -1) && (x.length < 4096) &&
|
|
@@ -22,31 +25,36 @@ function isPEM(buf) {
|
|
|
22
25
|
return s.indexOf('-----BEGIN ') !== -1 && s.indexOf('-----END ') !== -1;
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
// The PEM blocks of a text (cryptographic-formats, RFC 7468): [{ type, der, block }].
|
|
29
|
+
// Text between blocks — the comments of a CA bundle — is passed over. A
|
|
30
|
+
// block that does not decode fails the whole text: nothing is taken from a
|
|
31
|
+
// file that is not what it says (see normalizeCA).
|
|
25
32
|
function splitPEMBlocks(pemText) {
|
|
26
|
-
|
|
27
|
-
let re = /-----BEGIN ([A-Z0-9 \-]+)-----([\s\S]*?)-----END \1-----/g;
|
|
28
|
-
let m;
|
|
29
|
-
while ((m = re.exec(pemText)) !== null) {
|
|
30
|
-
let typ = m[1].trim();
|
|
31
|
-
let b64 = m[2].replace(/[\r\n\s]/g, '');
|
|
32
|
-
let derBuf = Buffer.from(b64, 'base64');
|
|
33
|
-
out.push({ type: typ, der: new Uint8Array(derBuf) });
|
|
34
|
-
}
|
|
35
|
-
return out;
|
|
33
|
+
return PEM.decode(pemText).map(function (b) { return { type: b.label, der: b.der, block: b }; });
|
|
36
34
|
}
|
|
35
|
+
// A certificate block's label: 'CERTIFICATE', and the 'X509 CERTIFICATE' /
|
|
36
|
+
// 'TRUSTED CERTIFICATE' spellings (OpenSSL's aux form, whose trailing trust
|
|
37
|
+
// data the certificate reader then refuses).
|
|
38
|
+
function isCertificateBlock(b) { return b.type.indexOf('CERTIFICATE') !== -1; }
|
|
37
39
|
|
|
38
40
|
function ensureArray(x) { return x == null ? [] : (Array.isArray(x) ? x : [x]); }
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
// The `ca` certificates, as DER. A PEM block that does not decode is
|
|
43
|
+
// skipped, as node:tls skips it — not silently: each is in `skipped`
|
|
44
|
+
// ({ label, line, code, detail }, cryptographic-formats' decode_skipping), which
|
|
45
|
+
// createSecureContext returns as `caSkipped`.
|
|
46
|
+
function normalizeCA(caOption, skipped) {
|
|
41
47
|
let arr = ensureArray(caOption);
|
|
42
48
|
let ders = [];
|
|
43
49
|
for (let i = 0; i < arr.length; i++) {
|
|
44
50
|
let raw = readMaybeFile(arr[i]);
|
|
45
51
|
if (!raw) continue;
|
|
46
52
|
if (isPEM(raw)) {
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
let read;
|
|
54
|
+
try { read = PEM.decode_skipping(raw.toString('utf8')); } catch (e) { read = { blocks: [], skipped: [] }; }
|
|
55
|
+
for (let j = 0; j < read.skipped.length; j++) skipped.push(read.skipped[j]);
|
|
56
|
+
for (let j = 0; j < read.blocks.length; j++) {
|
|
57
|
+
if (read.blocks[j].label.indexOf('CERTIFICATE') !== -1) ders.push(read.blocks[j].der);
|
|
50
58
|
}
|
|
51
59
|
} else {
|
|
52
60
|
ders.push(new Uint8Array(raw));
|
|
@@ -55,37 +63,72 @@ function normalizeCA(caOption) {
|
|
|
55
63
|
return ders;
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
// node:tls's X509Certificate, for the `certObjs` this returns (an API
|
|
67
|
+
// field). Everything decided here reads the certificate with cryptographic-formats.
|
|
58
68
|
function makeX509FromDerOrPem(buf) {
|
|
59
69
|
return new crypto.X509Certificate(Buffer.from(buf));
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
// A certificate's public key as node:crypto exports it (SPKI DER): the form
|
|
73
|
+
// a private key's public half is compared in. Through a KeyObject, so two
|
|
74
|
+
// encodings of one key — named or explicit curve parameters, a compressed
|
|
75
|
+
// point — compare equal, as they did.
|
|
76
|
+
function certPublicKeyDer(der) {
|
|
77
|
+
let cert = parse_cert(der);
|
|
78
|
+
if (!cert) {
|
|
79
|
+
let e = new Error('createSecureContext: the certificate does not parse');
|
|
80
|
+
e.code = 'ERR_INVALID_CERTIFICATE';
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
let key = cert.publicKey();
|
|
84
|
+
return key ? key.export({ type: 'spki', format: 'der' }) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The DER form of a private key, by what cryptographic-formats reads it as:
|
|
88
|
+
// PKCS#8 (plain or encrypted), PKCS#1 (RSA) or SEC 1 (EC) — node:crypto is
|
|
89
|
+
// told which, rather than tried with each in turn.
|
|
90
|
+
function derKeyType(der) {
|
|
91
|
+
let readers = [['pkcs8', PKCS8.from_der], ['pkcs8', PKCS8.encrypted_from_der],
|
|
92
|
+
['pkcs1', PKCS8.pkcs1_from_der], ['sec1', PKCS8.sec1_from_der]];
|
|
93
|
+
for (let i = 0; i < readers.length; i++) {
|
|
94
|
+
try { readers[i][1](der); return readers[i][0]; } catch (e) { /* the next */ }
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
// The same, from a PEM label (RFC 7468 §10, §11; OpenSSL's traditional ones)
|
|
99
|
+
const PEM_KEY_TYPES = {
|
|
100
|
+
'PRIVATE KEY': 'pkcs8', 'ENCRYPTED PRIVATE KEY': 'pkcs8',
|
|
101
|
+
'RSA PRIVATE KEY': 'pkcs1', 'EC PRIVATE KEY': 'sec1',
|
|
102
|
+
};
|
|
103
|
+
|
|
62
104
|
function makePrivateKeyFromDerOrPem(buf, passphrase) {
|
|
63
105
|
// node:crypto rejects an explicit `passphrase: null` with ERR_INVALID_ARG_VALUE
|
|
64
106
|
// — for an unencrypted key the property must be ABSENT, not present-and-null.
|
|
65
107
|
// Callers that forward an optional option straight through would otherwise
|
|
66
108
|
// break every unencrypted key, which is the overwhelmingly common case.
|
|
67
109
|
let pass = (passphrase === null || passphrase === undefined) ? undefined : passphrase;
|
|
110
|
+
let withPass = function (o) { if (pass !== undefined) o.passphrase = pass; return o; };
|
|
68
111
|
|
|
112
|
+
let der, type;
|
|
69
113
|
if (isPEM(buf)) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
// The key's block (a file from `openssl ecparam -genkey` starts with an
|
|
115
|
+
// EC PARAMETERS block), read by cryptographic-formats.
|
|
116
|
+
let blocks = splitPEMBlocks(buf.toString('utf8'));
|
|
117
|
+
let block = null;
|
|
118
|
+
for (let i = 0; i < blocks.length && !block; i++) if (PEM_KEY_TYPES[blocks[i].type]) block = blocks[i];
|
|
119
|
+
if (!block) throw new Error('createSecureContext: no private key in the PEM (' +
|
|
120
|
+
blocks.map(function (b) { return b.type; }).join(', ') + ')');
|
|
121
|
+
// RFC 1421's encrypted form (Proc-Type / DEK-Info headers): its
|
|
122
|
+
// decryption is node:crypto's, and node:crypto reads it only as PEM.
|
|
123
|
+
if (block.block.encrypted) return crypto.createPrivateKey(withPass({ key: buf, format: 'pem' }));
|
|
124
|
+
der = Buffer.from(block.der);
|
|
125
|
+
type = PEM_KEY_TYPES[block.type];
|
|
73
126
|
} else {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
pass !== undefined ? { passphrase: pass } : {}));
|
|
78
|
-
} catch (e1) {
|
|
79
|
-
try {
|
|
80
|
-
keyObj = crypto.createPrivateKey(Object.assign({ key: der, format: 'der', type: 'pkcs1' },
|
|
81
|
-
pass !== undefined ? { passphrase: pass } : {}));
|
|
82
|
-
} catch (e2) {
|
|
83
|
-
keyObj = crypto.createPrivateKey(Object.assign({ key: der, format: 'der', type: 'sec1' },
|
|
84
|
-
pass !== undefined ? { passphrase: pass } : {}));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return keyObj;
|
|
127
|
+
der = Buffer.from(buf);
|
|
128
|
+
type = derKeyType(der);
|
|
129
|
+
if (type === null) throw new Error('createSecureContext: the key is not PKCS#8, PKCS#1 or SEC 1 DER');
|
|
88
130
|
}
|
|
131
|
+
return crypto.createPrivateKey(withPass({ key: der, format: 'der', type: type }));
|
|
89
132
|
}
|
|
90
133
|
|
|
91
134
|
function exportKeyPkcs8Der(keyObj) {
|
|
@@ -134,10 +177,10 @@ function createSecureContext(options) {
|
|
|
134
177
|
let cert = null;
|
|
135
178
|
for (let j = 0; j < certs.length && cert === null; j++) {
|
|
136
179
|
let raw = readMaybeFile(certs[j]);
|
|
137
|
-
let first = isPEM(raw) ? splitPEMBlocks(raw.toString('utf8')).filter(
|
|
180
|
+
let first = isPEM(raw) ? splitPEMBlocks(raw.toString('utf8')).filter(isCertificateBlock)[0] : { der: new Uint8Array(raw) };
|
|
138
181
|
if (!first) continue;
|
|
139
|
-
let leafPub =
|
|
140
|
-
if (Buffer.compare(Buffer.from(leafPub), Buffer.from(pub)) === 0) cert = certs[j];
|
|
182
|
+
let leafPub = certPublicKeyDer(first.der);
|
|
183
|
+
if (leafPub && Buffer.compare(Buffer.from(leafPub), Buffer.from(pub)) === 0) cert = certs[j];
|
|
141
184
|
}
|
|
142
185
|
if (cert === null) throw new Error('createSecureContext: no certificate in `cert` for key ' + i);
|
|
143
186
|
return createSecureContext(Object.assign({}, options, { key: keyIn, passphrase: pass, cert: cert }));
|
|
@@ -159,7 +202,7 @@ function createSecureContext(options) {
|
|
|
159
202
|
if (isPEM(cRaw)) {
|
|
160
203
|
let blocks = splitPEMBlocks(cRaw.toString('utf8'));
|
|
161
204
|
for (let i=0;i<blocks.length;i++) {
|
|
162
|
-
if (blocks[i]
|
|
205
|
+
if (isCertificateBlock(blocks[i])) {
|
|
163
206
|
certBlocksDer.push(blocks[i].der);
|
|
164
207
|
certObjs.push(makeX509FromDerOrPem(blocks[i].der));
|
|
165
208
|
}
|
|
@@ -186,8 +229,23 @@ function createSecureContext(options) {
|
|
|
186
229
|
if (!privateKey) throw new Error('createSecureContext: missing private key.');
|
|
187
230
|
}
|
|
188
231
|
|
|
232
|
+
// The key must be the certificate's — node:tls throws on this too
|
|
233
|
+
// (KEY_VALUES_MISMATCH). Unchecked, a renewal that paired the new key with
|
|
234
|
+
// the old certificate went in without a word, and every handshake after it
|
|
235
|
+
// failed at the client's signature check.
|
|
236
|
+
if (keyObj && certBlocksDer.length > 0) {
|
|
237
|
+
let fromKey = crypto.createPublicKey(keyObj).export({ type: 'spki', format: 'der' });
|
|
238
|
+
let fromCert = certPublicKeyDer(certBlocksDer[0]);
|
|
239
|
+
if (!fromCert || Buffer.compare(Buffer.from(fromKey), Buffer.from(fromCert)) !== 0) {
|
|
240
|
+
let e = new Error('createSecureContext: the private key does not match the certificate');
|
|
241
|
+
e.code = 'ERR_OSSL_X509_KEY_VALUES_MISMATCH';
|
|
242
|
+
throw e;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
189
246
|
// --- CA (Trust store) ---
|
|
190
|
-
let
|
|
247
|
+
let caSkipped = [];
|
|
248
|
+
let ca = normalizeCA(options.ca, caSkipped);
|
|
191
249
|
|
|
192
250
|
// --- OCSP stapling (אופציונלי) ---
|
|
193
251
|
let ocsp = null;
|
|
@@ -207,15 +265,15 @@ function createSecureContext(options) {
|
|
|
207
265
|
|
|
208
266
|
// מידע עזר לזיהוי סוג המפתח הציבורי של ה-leaf
|
|
209
267
|
let leafPublicKeyType = null;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
268
|
+
let leafCert = certBlocksDer.length > 0 ? parse_cert(certBlocksDer[0]) : null;
|
|
269
|
+
if (leafCert && leafCert.publicKey()) leafPublicKeyType = leafCert.publicKey().asymmetricKeyType || null;
|
|
213
270
|
|
|
214
271
|
return {
|
|
215
272
|
// חומר לשכבת ההנדשייק/רקורד:
|
|
216
273
|
certificateChain: certificateChain, // [{ cert: DER(Uint8Array) }, ...]
|
|
217
274
|
privateKey: privateKey, // PKCS#8 DER (Uint8Array)
|
|
218
275
|
ca: ca, // Trust store (DER)
|
|
276
|
+
caSkipped: caSkipped, // PEM blocks of `ca` that did not decode: [{ label, line, code, detail }]
|
|
219
277
|
ocsp: ocsp, // DER (אם הוגדר)
|
|
220
278
|
ticketKeys: ticketKeys, // Uint8Array (אם הוגדר)
|
|
221
279
|
|
package/src/session/ecdh.js
CHANGED
|
@@ -1,94 +1,105 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* The key-exchange groups: the Diffie-Hellman ones — X25519, X448, P-256,
|
|
3
|
+
* P-384, P-521 — and ML-KEM alone and in hybrids with them (below), over
|
|
4
|
+
* node:crypto. The one place a group is implemented: TLS's key exchange,
|
|
5
|
+
* the hybrids' classical halves and HPKE's DHKEMs (hpke.js) all take their
|
|
6
|
+
* primitives from here.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import * as crypto from 'crypto';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let privObj = crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
|
|
18
|
-
let pubObj = crypto.createPublicKey(privObj);
|
|
19
|
-
let spki = pubObj.export({ type: 'spki', format: 'der' });
|
|
20
|
-
return new Uint8Array(spki.subarray(X25519_SPKI_PREFIX.length));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Compute X25519 shared secret from local private key and remote public key (both raw 32 bytes).
|
|
25
|
-
*/
|
|
26
|
-
function x25519_get_shared_secret(localPrivateRaw, remotePublicRaw) {
|
|
27
|
-
let privDer = Buffer.concat([X25519_PKCS8_PREFIX, Buffer.from(localPrivateRaw)]);
|
|
28
|
-
let pubDer = Buffer.concat([X25519_SPKI_PREFIX, Buffer.from(remotePublicRaw)]);
|
|
29
|
-
let privObj = crypto.createPrivateKey({ key: privDer, format: 'der', type: 'pkcs8' });
|
|
30
|
-
let pubObj = crypto.createPublicKey({ key: pubDer, format: 'der', type: 'spki' });
|
|
31
|
-
let secret = new Uint8Array(crypto.diffieHellman({ privateKey: privObj, publicKey: pubObj }));
|
|
32
|
-
|
|
33
|
-
// RFC 7748 §6.1: implementations MUST reject an all-zero shared secret. A peer
|
|
34
|
-
// that sends a low-order/small-order public key can force the X25519 result to
|
|
35
|
-
// be all zeros — a value it knows in advance — which would let it choose the
|
|
36
|
-
// key material. The `break` here is safe (does not leak a secret): the peer's
|
|
37
|
-
// public key is public, so an early exit reveals nothing exploitable.
|
|
38
|
-
let allZero = true;
|
|
39
|
-
for (let i = 0; i < secret.length; i++) {
|
|
40
|
-
if (secret[i] !== 0) { allZero = false; break; }
|
|
41
|
-
}
|
|
42
|
-
if (allZero) {
|
|
43
|
-
throw new Error('X25519: all-zero shared secret — invalid peer public key (RFC 7748 §6.1)');
|
|
44
|
-
}
|
|
45
|
-
|
|
10
|
+
import * as SPKI from 'cryptographic-formats/spki';
|
|
11
|
+
import * as PKCS8 from 'cryptographic-formats/pkcs8';
|
|
12
|
+
|
|
13
|
+
// RFC 7748 §6: an all-zero shared secret means the peer's public key was of
|
|
14
|
+
// low order — a value it knew in advance. MUST be rejected. Constant time
|
|
15
|
+
// (the peer's key is public, but there is no reason to branch on secrets).
|
|
16
|
+
function reject_all_zero(secret, name) {
|
|
17
|
+
let acc = 0;
|
|
18
|
+
for (let i = 0; i < secret.length; i++) acc |= secret[i];
|
|
19
|
+
if (acc === 0) throw new Error(name + ': all-zero shared secret — invalid peer public key (RFC 7748 §6)');
|
|
46
20
|
return secret;
|
|
47
21
|
}
|
|
48
22
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
23
|
+
// The native key behind a private key this module generated, by the bytes
|
|
24
|
+
// it handed out. Importing a raw private key is not cheap — for X25519
|
|
25
|
+
// node:crypto computes the public key on import, ~50 µs, as much as the
|
|
26
|
+
// Diffie-Hellman itself — and every handshake imported its own key twice:
|
|
27
|
+
// once to derive the public key, once for the shared secret. The key made by
|
|
28
|
+
// generate() is kept here and used by shared(); a private key from anywhere
|
|
29
|
+
// else (HPKE's, a test vector's) is imported as before. Keyed by the
|
|
30
|
+
// Uint8Array's identity, so the entry goes with the key: nothing outlives it.
|
|
31
|
+
const NATIVE_PRIVATE = new WeakMap();
|
|
32
|
+
|
|
33
|
+
const b64u = (s) => new Uint8Array(Buffer.from(s, 'base64url'));
|
|
34
|
+
|
|
35
|
+
// A Montgomery curve. node:crypto takes and gives its raw keys only inside
|
|
36
|
+
// PKCS#8 and SPKI: cryptographic-formats writes and reads that wrapping (RFC 8410)
|
|
37
|
+
// — it was two DER prefixes written out here by hand.
|
|
38
|
+
function montgomery(name, len) {
|
|
39
|
+
let type = name.toLowerCase();
|
|
40
|
+
let priv = (sk) => NATIVE_PRIVATE.get(sk) ||
|
|
41
|
+
crypto.createPrivateKey({ key: Buffer.from(PKCS8.to_der({ algorithm: name, scalar: sk })), format: 'der', type: 'pkcs8' });
|
|
42
|
+
let c = {
|
|
43
|
+
kind: 'montgomery', len: len, privLen: len,
|
|
44
|
+
public_key: function (sk) {
|
|
45
|
+
return new Uint8Array(SPKI.from_der(crypto.createPublicKey(priv(sk)).export({ type: 'spki', format: 'der' })).point);
|
|
46
|
+
},
|
|
47
|
+
shared: function (sk, pk) {
|
|
48
|
+
let pub = crypto.createPublicKey({ key: Buffer.from(SPKI.to_der({ algorithm: name, point: pk })), format: 'der', type: 'spki' });
|
|
49
|
+
return reject_all_zero(new Uint8Array(crypto.diffieHellman({ privateKey: priv(sk), publicKey: pub })), name);
|
|
50
|
+
},
|
|
51
|
+
// Made natively (the same CSPRNG as randomBytes), and read back as JWK —
|
|
52
|
+
// the raw scalar and public key, without a DER round trip.
|
|
53
|
+
generate: function () {
|
|
54
|
+
let kp = crypto.generateKeyPairSync(type);
|
|
55
|
+
let jwk = kp.privateKey.export({ format: 'jwk' });
|
|
56
|
+
let sk = b64u(jwk.d), pk = b64u(jwk.x);
|
|
57
|
+
if (sk.length !== len || pk.length !== len) throw new Error(name + ': unexpected key size from node:crypto');
|
|
58
|
+
NATIVE_PRIVATE.set(sk, kp.privateKey);
|
|
59
|
+
return { private_key: sk, public_key: pk };
|
|
60
|
+
},
|
|
59
61
|
};
|
|
62
|
+
return c;
|
|
60
63
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Generate a P-384 keypair. Returns { private_key: Uint8Array(48), public_key: Uint8Array(97) }.
|
|
73
|
-
* Public key is uncompressed format (0x04 || x || y).
|
|
74
|
-
*/
|
|
75
|
-
function p384_generate_keypair() {
|
|
76
|
-
let ecdh = crypto.createECDH('secp384r1');
|
|
77
|
-
ecdh.generateKeys();
|
|
64
|
+
// A NIST prime curve: shares are uncompressed points (0x04 || X || Y,
|
|
65
|
+
// RFC 8446 §4.2.8.2), the shared secret the x-coordinate.
|
|
66
|
+
function nist(curve, coordLen) {
|
|
67
|
+
let ecdh = (sk) => {
|
|
68
|
+
let e = NATIVE_PRIVATE.get(sk);
|
|
69
|
+
if (e) return e;
|
|
70
|
+
e = crypto.createECDH(curve); e.setPrivateKey(Buffer.from(sk));
|
|
71
|
+
return e;
|
|
72
|
+
};
|
|
78
73
|
return {
|
|
79
|
-
|
|
80
|
-
public_key:
|
|
74
|
+
kind: 'nist', len: 1 + 2 * coordLen, coordLen: coordLen, privLen: coordLen,
|
|
75
|
+
public_key: function (sk) {
|
|
76
|
+
return new Uint8Array(ecdh(sk).getPublicKey(null, 'uncompressed'));
|
|
77
|
+
},
|
|
78
|
+
// computeSecret validates the peer point (on the curve, not infinity)
|
|
79
|
+
// before any secret exists.
|
|
80
|
+
shared: function (sk, pk) {
|
|
81
|
+
return new Uint8Array(ecdh(sk).computeSecret(Buffer.from(pk)));
|
|
82
|
+
},
|
|
83
|
+
generate: function () {
|
|
84
|
+
let e = crypto.createECDH(curve); e.generateKeys();
|
|
85
|
+
// getPrivateKey drops leading zero bytes — half of all P-521 scalars,
|
|
86
|
+
// 1 in 256 of P-256's — and a private key is privLen bytes.
|
|
87
|
+
let raw = e.getPrivateKey(), sk = new Uint8Array(coordLen);
|
|
88
|
+
sk.set(raw, coordLen - raw.length);
|
|
89
|
+
NATIVE_PRIVATE.set(sk, e);
|
|
90
|
+
return { private_key: sk, public_key: new Uint8Array(e.getPublicKey(null, 'uncompressed')) };
|
|
91
|
+
},
|
|
81
92
|
};
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
const CURVES = {
|
|
96
|
+
x25519: montgomery('X25519', 32),
|
|
97
|
+
x448: montgomery('X448', 56),
|
|
98
|
+
p256: nist('prime256v1', 32),
|
|
99
|
+
p384: nist('secp384r1', 48),
|
|
100
|
+
p521: nist('secp521r1', 66),
|
|
101
|
+
};
|
|
102
|
+
|
|
92
103
|
|
|
93
104
|
/**
|
|
94
105
|
* Named groups this module actually implements, in preference order.
|
|
@@ -104,8 +115,10 @@ function p384_get_shared_secret(localPrivateRaw, remotePublicRaw) {
|
|
|
104
115
|
*/
|
|
105
116
|
const NAMED_GROUP = {
|
|
106
117
|
X25519: 0x001d,
|
|
118
|
+
X448: 0x001e,
|
|
107
119
|
SECP256R1: 0x0017,
|
|
108
120
|
SECP384R1: 0x0018,
|
|
121
|
+
SECP521R1: 0x0019,
|
|
109
122
|
// Post-quantum. KEMs, not Diffie-Hellman: the server does not make a key
|
|
110
123
|
// pair of its own, it ENCAPSULATES to the client's — see kem_server_exchange.
|
|
111
124
|
// Hybrids (draft-ietf-tls-ecdhe-mlkem): ML-KEM (FIPS 203) and an ECDH
|
|
@@ -124,12 +137,24 @@ const NAMED_GROUP = {
|
|
|
124
137
|
|
|
125
138
|
// Each group's name — the IANA registry's, as OpenSSL and node:tls spell them.
|
|
126
139
|
const GROUP_NAMES = {
|
|
127
|
-
0x001d: 'X25519', 0x0017: 'P-256', 0x0018: 'P-384',
|
|
140
|
+
0x001d: 'X25519', 0x001e: 'X448', 0x0017: 'P-256', 0x0018: 'P-384', 0x0019: 'P-521',
|
|
128
141
|
0x11ec: 'X25519MLKEM768', 0x11eb: 'SecP256r1MLKEM768', 0x11ed: 'SecP384r1MLKEM1024',
|
|
129
142
|
0x0200: 'MLKEM512', 0x0201: 'MLKEM768', 0x0202: 'MLKEM1024',
|
|
130
143
|
};
|
|
131
144
|
function group_name(group) { return GROUP_NAMES[group] || null; }
|
|
132
145
|
|
|
146
|
+
// The Diffie-Hellman groups, by code point: their curve, and how node:tls
|
|
147
|
+
// (getEphemeralKeyInfo) names and sizes them.
|
|
148
|
+
const DH_GROUPS = {
|
|
149
|
+
0x001d: { curve: CURVES.x25519, ecdhName: 'x25519', bits: 253 },
|
|
150
|
+
0x001e: { curve: CURVES.x448, ecdhName: 'x448', bits: 448 },
|
|
151
|
+
0x0017: { curve: CURVES.p256, ecdhName: 'prime256v1', bits: 256 },
|
|
152
|
+
0x0018: { curve: CURVES.p384, ecdhName: 'secp384r1', bits: 384 },
|
|
153
|
+
0x0019: { curve: CURVES.p521, ecdhName: 'secp521r1', bits: 521 },
|
|
154
|
+
};
|
|
155
|
+
/** node:tls's description of a DH group ({ name, size }), or null. */
|
|
156
|
+
function dh_group_info(group) { let d = DH_GROUPS[group]; return d ? { name: d.ecdhName, size: d.bits } : null; }
|
|
157
|
+
|
|
133
158
|
// ML-KEM's three parameter sets (FIPS 203 Table 3): encapsulation key and
|
|
134
159
|
// ciphertext sizes. The shared secret is 32 bytes for each.
|
|
135
160
|
const MLKEM = {
|
|
@@ -138,18 +163,12 @@ const MLKEM = {
|
|
|
138
163
|
1024: { type: 'ml-kem-1024', ek: 1568, ct: 1568 },
|
|
139
164
|
};
|
|
140
165
|
|
|
141
|
-
// The classical halves of the hybrids:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
shared: function (priv, pub) { check_point(pub, 65); return p256_get_shared_secret(priv, pub); } },
|
|
148
|
-
p384: { len: 97, generate: function () { return p384_generate_keypair(); },
|
|
149
|
-
shared: function (priv, pub) { check_point(pub, 97); return p384_get_shared_secret(priv, pub); } },
|
|
150
|
-
};
|
|
151
|
-
function check_point(pub, len) {
|
|
152
|
-
if (pub.length !== len || pub[0] !== 0x04) throw new Error('the ECDH part must be a ' + len + '-byte uncompressed point');
|
|
166
|
+
// The classical halves of the hybrids: the curves above, their parts checked
|
|
167
|
+
// as a group's share is (normalize_dh_share).
|
|
168
|
+
const ECDH_PART = {};
|
|
169
|
+
for (const name of ['x25519', 'p256', 'p384']) {
|
|
170
|
+
let c = CURVES[name];
|
|
171
|
+
ECDH_PART[name] = { len: c.len, generate: c.generate, shared: function (sk, pk) { return c.shared(sk, normalize_dh_share(c, pk)); } };
|
|
153
172
|
}
|
|
154
173
|
|
|
155
174
|
// The KEM groups: which ML-KEM, which ECDH half (none for ML-KEM alone), and
|
|
@@ -172,9 +191,12 @@ const KEM_GROUPS = {
|
|
|
172
191
|
//
|
|
173
192
|
// Decided once per parameter set, by a real round trip rather than by the
|
|
174
193
|
// functions existing (on BoringSSL, crypto.encapsulate can be defined and
|
|
175
|
-
// still not know ML-KEM)
|
|
176
|
-
//
|
|
177
|
-
|
|
194
|
+
// still not know ML-KEM). The raw key's SPKI wrapping is cryptographic-formats'
|
|
195
|
+
// (RFC 9935), and the probe checks it is the one node:crypto writes: a set
|
|
196
|
+
// where they differ is not offered.
|
|
197
|
+
function mlkem_spki(bits, raw) {
|
|
198
|
+
return Buffer.from(SPKI.to_der({ algorithm: 'mlKem' + bits, point: raw }));
|
|
199
|
+
}
|
|
178
200
|
function probe_mlkem(bits) {
|
|
179
201
|
if (typeof crypto.encapsulate !== 'function' || typeof crypto.decapsulate !== 'function') return false;
|
|
180
202
|
try {
|
|
@@ -184,8 +206,8 @@ function probe_mlkem(bits) {
|
|
|
184
206
|
let dec = crypto.decapsulate(kp.privateKey, enc.ciphertext);
|
|
185
207
|
if (enc.ciphertext.length !== p.ct || Buffer.compare(Buffer.from(enc.sharedKey), Buffer.from(dec)) !== 0) return false;
|
|
186
208
|
let spki = kp.publicKey.export({ format: 'der', type: 'spki' });
|
|
187
|
-
|
|
188
|
-
return
|
|
209
|
+
let ek = SPKI.from_der(spki).point;
|
|
210
|
+
return ek.length === p.ek && Buffer.compare(mlkem_spki(bits, ek), Buffer.from(spki)) === 0;
|
|
189
211
|
} catch (e) {
|
|
190
212
|
return false;
|
|
191
213
|
}
|
|
@@ -196,7 +218,7 @@ const MLKEM_AVAILABLE = MLKEM_SETS[768];
|
|
|
196
218
|
// Import checks the key (FIPS 203 §7.2: every coefficient below q) and throws
|
|
197
219
|
// on a bad one — before any secret exists.
|
|
198
220
|
function mlkem_public_key(bits, raw) {
|
|
199
|
-
return crypto.createPublicKey({ key:
|
|
221
|
+
return crypto.createPublicKey({ key: mlkem_spki(bits, raw), format: 'der', type: 'spki' });
|
|
200
222
|
}
|
|
201
223
|
|
|
202
224
|
function kem_group_available(group) {
|
|
@@ -239,6 +261,11 @@ const SERVER_DEFAULT_GROUPS = [
|
|
|
239
261
|
NAMED_GROUP.X25519,
|
|
240
262
|
NAMED_GROUP.SECP256R1,
|
|
241
263
|
NAMED_GROUP.SECP384R1,
|
|
264
|
+
// Taken from a client that offers them — none offers only these by
|
|
265
|
+
// default, so they come last (OpenSSL's order: X448 after the NIST
|
|
266
|
+
// curves it offers by default, P-521 last).
|
|
267
|
+
NAMED_GROUP.X448,
|
|
268
|
+
NAMED_GROUP.SECP521R1,
|
|
242
269
|
]);
|
|
243
270
|
|
|
244
271
|
// Everything implemented here, on either side. A KEM group only where
|
|
@@ -260,16 +287,13 @@ function is_kem_group(group) {
|
|
|
260
287
|
* Returns { private_key, public_key } or null for an unsupported group.
|
|
261
288
|
*/
|
|
262
289
|
function generate_keypair(group) {
|
|
263
|
-
if (group
|
|
264
|
-
if (group === NAMED_GROUP.SECP256R1) return p256_generate_keypair();
|
|
265
|
-
if (group === NAMED_GROUP.SECP384R1) return p384_generate_keypair();
|
|
290
|
+
if (DH_GROUPS[group]) return DH_GROUPS[group].curve.generate();
|
|
266
291
|
if (kem_group_available(group)) {
|
|
267
292
|
// The CLIENT side: an ML-KEM key pair, and an ECDH one for a hybrid; the
|
|
268
293
|
// share is the two public keys in the group's order.
|
|
269
294
|
let d = KEM_GROUPS[group], sz = kem_sizes(group);
|
|
270
295
|
let kem = crypto.generateKeyPairSync(MLKEM[d.kem].type);
|
|
271
|
-
let
|
|
272
|
-
let ek = spki.subarray(spki.length - sz.ek);
|
|
296
|
+
let ek = SPKI.from_der(kem.publicKey.export({ format: 'der', type: 'spki' })).point;
|
|
273
297
|
let ec = d.ecdh ? ECDH_PART[d.ecdh].generate() : null;
|
|
274
298
|
let pub = new Uint8Array(sz.client);
|
|
275
299
|
if (!ec) pub.set(ek, 0);
|
|
@@ -351,17 +375,17 @@ function normalize_peer_public_key(group, pub) {
|
|
|
351
375
|
return pub;
|
|
352
376
|
}
|
|
353
377
|
|
|
354
|
-
if (group
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
378
|
+
if (!DH_GROUPS[group]) throw new Error('unsupported group 0x' + (group >>> 0).toString(16));
|
|
379
|
+
return normalize_dh_share(DH_GROUPS[group].curve, pub);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// A Diffie-Hellman share as the curve has it, or an error.
|
|
383
|
+
function normalize_dh_share(c, pub) {
|
|
384
|
+
if (c.kind === 'montgomery') {
|
|
385
|
+
if (pub.length !== c.len) throw new Error('key_exchange must be ' + c.len + ' bytes, got ' + pub.length);
|
|
358
386
|
return pub;
|
|
359
387
|
}
|
|
360
|
-
|
|
361
|
-
let coordLen = (group === NAMED_GROUP.SECP256R1) ? 32
|
|
362
|
-
: (group === NAMED_GROUP.SECP384R1) ? 48 : 0;
|
|
363
|
-
if (coordLen === 0) throw new Error('unsupported group 0x' + (group >>> 0).toString(16));
|
|
364
|
-
|
|
388
|
+
let coordLen = c.coordLen;
|
|
365
389
|
// RFC 8446 §4.2.8.2: for the secp*r1 groups the key_exchange is an
|
|
366
390
|
// UNCOMPRESSED point — 0x04 || X || Y — and nothing else. The length is
|
|
367
391
|
// therefore exact.
|
|
@@ -373,7 +397,7 @@ function normalize_peer_public_key(group, pub) {
|
|
|
373
397
|
// as a DIFFERENT, prefix-less point and happily fed to the ECDH. The same
|
|
374
398
|
// arithmetic applies to P-384 (97 → 96). Malformed input must be rejected,
|
|
375
399
|
// not silently reinterpreted.
|
|
376
|
-
let expected =
|
|
400
|
+
let expected = c.len;
|
|
377
401
|
|
|
378
402
|
if (pub.length !== expected) {
|
|
379
403
|
// Name the two shapes we know we are refusing, so the failure is
|
|
@@ -394,7 +418,7 @@ function normalize_peer_public_key(group, pub) {
|
|
|
394
418
|
}
|
|
395
419
|
|
|
396
420
|
// Whether the point actually lies on the curve is checked by the ECDH
|
|
397
|
-
// primitive
|
|
421
|
+
// primitive (node throws ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY), which
|
|
398
422
|
// happens before any secret is produced — so an off-curve point can never
|
|
399
423
|
// yield key material.
|
|
400
424
|
return pub;
|
|
@@ -408,9 +432,7 @@ function normalize_peer_public_key(group, pub) {
|
|
|
408
432
|
function get_shared_secret(group, localPrivateRaw, remotePublicRaw) {
|
|
409
433
|
let pub = normalize_peer_public_key(group, remotePublicRaw);
|
|
410
434
|
|
|
411
|
-
if (group
|
|
412
|
-
if (group === NAMED_GROUP.SECP256R1) return p256_get_shared_secret(localPrivateRaw, pub);
|
|
413
|
-
if (group === NAMED_GROUP.SECP384R1) return p384_get_shared_secret(localPrivateRaw, pub);
|
|
435
|
+
if (DH_GROUPS[group]) return DH_GROUPS[group].curve.shared(localPrivateRaw, pub);
|
|
414
436
|
if (kem_group_available(group)) {
|
|
415
437
|
// The CLIENT side: the server's share is the ciphertext (and, for a
|
|
416
438
|
// hybrid, the server's ECDH public key), in the group's order.
|
|
@@ -442,10 +464,6 @@ export {
|
|
|
442
464
|
generate_keypair,
|
|
443
465
|
normalize_peer_public_key,
|
|
444
466
|
get_shared_secret,
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
p256_generate_keypair,
|
|
448
|
-
p256_get_shared_secret,
|
|
449
|
-
p384_generate_keypair,
|
|
450
|
-
p384_get_shared_secret
|
|
467
|
+
dh_group_info,
|
|
468
|
+
CURVES,
|
|
451
469
|
};
|