tunnelfetch 1.0.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/LICENSE +28 -0
- package/README.md +617 -0
- package/README.zh-CN.md +470 -0
- package/package.json +74 -0
- package/src/client/cookies.js +429 -0
- package/src/client/decode.js +346 -0
- package/src/client/redirect.js +249 -0
- package/src/client.js +704 -0
- package/src/errors.js +181 -0
- package/src/http1/chunked.js +289 -0
- package/src/http1/index.js +10 -0
- package/src/http1/request.js +143 -0
- package/src/http1/response.js +493 -0
- package/src/http2/connection.js +1170 -0
- package/src/http2/constants.js +129 -0
- package/src/http2/frames.js +291 -0
- package/src/http2/hpack.js +420 -0
- package/src/http2/huffman.js +203 -0
- package/src/http2/index.js +21 -0
- package/src/index.js +46 -0
- package/src/pool.js +256 -0
- package/src/proxy/direct.js +62 -0
- package/src/proxy/http-connect.js +206 -0
- package/src/proxy/index.js +197 -0
- package/src/proxy/socks5.js +344 -0
- package/src/tls/aead.js +263 -0
- package/src/tls/connect.js +407 -0
- package/src/tls/constants.js +334 -0
- package/src/tls/extensions.js +376 -0
- package/src/tls/handshake-messages.js +901 -0
- package/src/tls/handshake.js +568 -0
- package/src/tls/handshake12.js +507 -0
- package/src/tls/index.js +44 -0
- package/src/tls/keyschedule.js +473 -0
- package/src/tls/record.js +872 -0
- package/src/tls/tickets.js +145 -0
- package/src/tls/transcript.js +101 -0
- package/src/tls/wire.js +224 -0
- package/src/transport.js +296 -0
- package/src/trust/der.js +551 -0
- package/src/trust/index.js +375 -0
- package/src/trust/name.js +235 -0
- package/src/trust/ocsp.js +759 -0
- package/src/trust/path.js +595 -0
- package/src/trust/roots.js +454 -0
- package/src/trust/x509.js +902 -0
- package/src/util/bytes.js +470 -0
- package/src/util/deadline.js +266 -0
- package/src/warmup-fixture.js +85 -0
- package/src/warmup.js +243 -0
- package/types/client/cookies.d.ts +159 -0
- package/types/client/decode.d.ts +54 -0
- package/types/client/redirect.d.ts +96 -0
- package/types/client.d.ts +323 -0
- package/types/errors.d.ts +141 -0
- package/types/http1/chunked.d.ts +48 -0
- package/types/http1/index.d.ts +3 -0
- package/types/http1/request.d.ts +44 -0
- package/types/http1/response.d.ts +183 -0
- package/types/http2/connection.d.ts +282 -0
- package/types/http2/constants.d.ts +95 -0
- package/types/http2/frames.d.ts +116 -0
- package/types/http2/hpack.d.ts +99 -0
- package/types/http2/huffman.d.ts +21 -0
- package/types/http2/index.d.ts +5 -0
- package/types/index.d.ts +17 -0
- package/types/pool.d.ts +135 -0
- package/types/proxy/direct.d.ts +26 -0
- package/types/proxy/http-connect.d.ts +37 -0
- package/types/proxy/index.d.ts +62 -0
- package/types/proxy/socks5.d.ts +47 -0
- package/types/tls/aead.d.ts +67 -0
- package/types/tls/connect.d.ts +280 -0
- package/types/tls/constants.d.ts +275 -0
- package/types/tls/extensions.d.ts +195 -0
- package/types/tls/handshake-messages.d.ts +430 -0
- package/types/tls/handshake.d.ts +90 -0
- package/types/tls/handshake12.d.ts +35 -0
- package/types/tls/index.d.ts +9 -0
- package/types/tls/keyschedule.d.ts +272 -0
- package/types/tls/record.d.ts +361 -0
- package/types/tls/tickets.d.ts +66 -0
- package/types/tls/transcript.d.ts +52 -0
- package/types/tls/wire.d.ts +106 -0
- package/types/transport.d.ts +222 -0
- package/types/trust/der.d.ts +239 -0
- package/types/trust/index.d.ts +194 -0
- package/types/trust/name.d.ts +33 -0
- package/types/trust/ocsp.d.ts +138 -0
- package/types/trust/path.d.ts +139 -0
- package/types/trust/roots.d.ts +36 -0
- package/types/trust/x509.d.ts +401 -0
- package/types/util/bytes.d.ts +183 -0
- package/types/util/deadline.d.ts +133 -0
- package/types/warmup-fixture.d.ts +11 -0
- package/types/warmup.d.ts +45 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
// Handshake message encoding, decoding, and the negotiation decisions that follow from them.
|
|
2
|
+
//
|
|
3
|
+
// Deliberately free of any dependency on the record layer: these are byte-level functions over
|
|
4
|
+
// complete handshake message bodies, which makes the whole of negotiation — including every
|
|
5
|
+
// downgrade guard — testable from fixed vectors with no I/O.
|
|
6
|
+
//
|
|
7
|
+
// All randomness and key generation is injectable. That is not a testing nicety: without it a
|
|
8
|
+
// TLS handshake cannot be replayed, and a transport that cannot be replayed cannot be tested
|
|
9
|
+
// offline at all.
|
|
10
|
+
|
|
11
|
+
import { TlsError, TlsUnsupportedError, CertificateError, codes, hex16 } from '../errors.js';
|
|
12
|
+
import { concat, equal, timingSafeEqual, utf8 } from '../util/bytes.js';
|
|
13
|
+
import { Builder, Cursor, vector, handshakeMessage } from './wire.js';
|
|
14
|
+
// der.js is a strict ASN.1 reader with no trust policy in it; the signature-format conversion
|
|
15
|
+
// lives there so the certificate path builder and this file cannot drift apart.
|
|
16
|
+
import { ecdsaDerToRaw } from '../trust/der.js';
|
|
17
|
+
import {
|
|
18
|
+
CIPHER_NAME,
|
|
19
|
+
CIPHER_PARAMS,
|
|
20
|
+
DOWNGRADE_SENTINEL_11,
|
|
21
|
+
DOWNGRADE_SENTINEL_12,
|
|
22
|
+
EXTENSION,
|
|
23
|
+
GROUP_PARAMS,
|
|
24
|
+
HANDSHAKE_TYPE,
|
|
25
|
+
HELLO_RETRY_REQUEST_RANDOM,
|
|
26
|
+
LEGACY_VERSION,
|
|
27
|
+
SIG_SCHEME_PARAMS,
|
|
28
|
+
TLS12,
|
|
29
|
+
TLS12_CIPHERS,
|
|
30
|
+
TLS13,
|
|
31
|
+
TLS13_CIPHERS,
|
|
32
|
+
SUPPORTED_GROUPS,
|
|
33
|
+
SUPPORTED_SIG_SCHEMES,
|
|
34
|
+
ALPN_HTTP11,
|
|
35
|
+
} from './constants.js';
|
|
36
|
+
import {
|
|
37
|
+
decodeAlpn,
|
|
38
|
+
decodeExtensionBlock,
|
|
39
|
+
decodeKeyShareEntry,
|
|
40
|
+
decodeKeyShareHrr,
|
|
41
|
+
decodeSelectedVersion,
|
|
42
|
+
describeSigScheme,
|
|
43
|
+
describeVersion,
|
|
44
|
+
encodeAlpn,
|
|
45
|
+
encodeEcPointFormats,
|
|
46
|
+
encodeExtendedMasterSecret,
|
|
47
|
+
encodeExtensionBlock,
|
|
48
|
+
encodeKeyShare,
|
|
49
|
+
encodePreSharedKey,
|
|
50
|
+
encodePskKeyExchangeModes,
|
|
51
|
+
encodeRenegotiationInfo,
|
|
52
|
+
encodeServerName,
|
|
53
|
+
encodeSignatureAlgorithms,
|
|
54
|
+
encodeStatusRequest,
|
|
55
|
+
encodeSupportedGroups,
|
|
56
|
+
encodeSupportedVersions,
|
|
57
|
+
pskBinderTrailerLength,
|
|
58
|
+
rejectUnofferedExtensions,
|
|
59
|
+
requireSupportedGroup,
|
|
60
|
+
} from './extensions.js';
|
|
61
|
+
|
|
62
|
+
const defaultRandom = (n) => crypto.getRandomValues(new Uint8Array(n));
|
|
63
|
+
|
|
64
|
+
// ------------------------------------------------------------------ key shares
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* An ephemeral key share: the public half as sent in key_share, plus the private key the
|
|
68
|
+
* eventual ServerHello selection will feed into deriveSharedSecret.
|
|
69
|
+
* @typedef {object} KeyShare
|
|
70
|
+
* @property {number} group
|
|
71
|
+
* @property {Uint8Array} keyExchange raw public key, the exact bytes on the wire
|
|
72
|
+
* @property {CryptoKey} privateKey non-extractable
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generate an ephemeral key share for one group.
|
|
77
|
+
* `generateKeyPair` is injectable so a recorded handshake can be replayed with the exact private
|
|
78
|
+
* key that produced it.
|
|
79
|
+
*
|
|
80
|
+
* @param {number} group
|
|
81
|
+
* @param {import('./connect.js').TlsDeps} [deps]
|
|
82
|
+
* @returns {Promise<KeyShare>}
|
|
83
|
+
*/
|
|
84
|
+
export async function generateKeyShare(group, { generateKeyPair } = {}) {
|
|
85
|
+
const params = requireSupportedGroup(group, 'ClientHello');
|
|
86
|
+
const gen =
|
|
87
|
+
generateKeyPair ??
|
|
88
|
+
((algorithm) => crypto.subtle.generateKey(algorithm, false, ['deriveBits']));
|
|
89
|
+
const pair = await gen(params.algorithm, group);
|
|
90
|
+
const raw = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));
|
|
91
|
+
if (raw.byteLength !== params.publicLen) {
|
|
92
|
+
throw new TlsError(
|
|
93
|
+
codes.TLS_HANDSHAKE,
|
|
94
|
+
`generated a ${raw.byteLength}-byte public key for group ${hex16(group)}, expected ${params.publicLen}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return { group, keyExchange: raw, privateKey: pair.privateKey };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* ECDH/X25519 shared secret. The peer's key is imported in raw form, which is where a malformed
|
|
102
|
+
* point is caught: WebCrypto rejects a point that is not on the curve, so we do not have to
|
|
103
|
+
* implement that check ourselves — but a wrong LENGTH would be accepted by some implementations,
|
|
104
|
+
* so it is checked here first.
|
|
105
|
+
*
|
|
106
|
+
* @param {number} group
|
|
107
|
+
* @param {CryptoKey} privateKey our ephemeral private key for the group
|
|
108
|
+
* @param {Uint8Array} peerKey the server's raw public key from its key_share
|
|
109
|
+
* @returns {Promise<Uint8Array>} throws on any degenerate or malformed peer key
|
|
110
|
+
*/
|
|
111
|
+
export async function deriveSharedSecret(group, privateKey, peerKey) {
|
|
112
|
+
const params = requireSupportedGroup(group, 'ServerHello');
|
|
113
|
+
if (peerKey.byteLength !== params.publicLen) {
|
|
114
|
+
throw new TlsError(
|
|
115
|
+
codes.TLS_HANDSHAKE,
|
|
116
|
+
`server key_share for group ${hex16(group)} is ${peerKey.byteLength} bytes, expected ${params.publicLen}`,
|
|
117
|
+
{ group, got: peerKey.byteLength, expected: params.publicLen },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (params.kind === 'ec' && peerKey[0] !== 0x04) {
|
|
121
|
+
throw new TlsUnsupportedError(
|
|
122
|
+
codes.TLS_GROUP_UNSUPPORTED,
|
|
123
|
+
`server sent a compressed or invalid EC point (first byte 0x${peerKey[0].toString(16)}) ` +
|
|
124
|
+
`for group ${hex16(group)}; only uncompressed points are supported`,
|
|
125
|
+
{ group, firstByte: peerKey[0] },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
let peer;
|
|
129
|
+
try {
|
|
130
|
+
peer = await crypto.subtle.importKey('raw', peerKey, params.algorithm, false, []);
|
|
131
|
+
} catch (cause) {
|
|
132
|
+
throw new TlsError(
|
|
133
|
+
codes.TLS_HANDSHAKE,
|
|
134
|
+
`server key_share for group ${hex16(group)} is not a valid public key: ${cause?.message}`,
|
|
135
|
+
{ group },
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const bits = params.kind === 'x25519' ? params.secretLen * 8 : params.secretBits;
|
|
139
|
+
let shared;
|
|
140
|
+
try {
|
|
141
|
+
shared = new Uint8Array(
|
|
142
|
+
await crypto.subtle.deriveBits({ ...params.algorithm, public: peer }, privateKey, bits),
|
|
143
|
+
);
|
|
144
|
+
} catch (cause) {
|
|
145
|
+
// WebCrypto rejects small-order X25519 peers itself, with an OperationError carrying no
|
|
146
|
+
// useful text. Letting that escape untyped would put a bare DOMException in front of a
|
|
147
|
+
// caller who is catching our error taxonomy, so it is translated here.
|
|
148
|
+
throw new TlsError(
|
|
149
|
+
codes.TLS_HANDSHAKE,
|
|
150
|
+
`key agreement failed for group ${hex16(group)}: ${cause?.message ?? cause}. ` +
|
|
151
|
+
'This usually means the server sent a degenerate or small-order public key.',
|
|
152
|
+
{ group },
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
// RFC 7748 s6.1: an all-zero X25519 output means a small-order peer key. Reject.
|
|
156
|
+
if (params.kind === 'x25519' && shared.every((b) => b === 0)) {
|
|
157
|
+
throw new TlsError(
|
|
158
|
+
codes.TLS_HANDSHAKE,
|
|
159
|
+
'X25519 shared secret is all zeroes, indicating a small-order server key_share',
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return shared;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ------------------------------------------------------------------ ClientHello
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @typedef {object} ClientHelloOptions
|
|
169
|
+
* @property {string} hostname SNI, unless it is an IP literal (then no SNI is sent)
|
|
170
|
+
* @property {Array<{ group: number, keyExchange: Uint8Array }>} keyShares public halves to
|
|
171
|
+
* offer; empty for a 1.2-only hello, whose wire form must not carry the extension at all
|
|
172
|
+
* @property {Uint8Array} [random] fixed ClientHello.random, for reproducible handshakes
|
|
173
|
+
* @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise
|
|
174
|
+
* @property {number[]} [ciphers] default: the union for the offered versions, 1.3 first
|
|
175
|
+
* @property {number[]} [groups] supported_groups, default SUPPORTED_GROUPS
|
|
176
|
+
* @property {number[]} [sigSchemes] default SUPPORTED_SIG_SCHEMES
|
|
177
|
+
* @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
|
|
178
|
+
* @property {number[]} [versions] default [TLS13, TLS12]
|
|
179
|
+
* @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
|
|
180
|
+
* @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
|
|
181
|
+
* offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
|
|
182
|
+
* the real binder over `message.subarray(0, truncatedLength)` and patch it in at
|
|
183
|
+
* `binderOffset` before the hello touches the wire — a zero binder on the wire is a hello
|
|
184
|
+
* every honest server must reject.
|
|
185
|
+
* @property {(n: number) => Uint8Array} [randomBytes] injectable randomness
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The built hello plus everything later steps need to police the server's answer against what
|
|
190
|
+
* was actually offered — negotiation checks must run against this record, never against the
|
|
191
|
+
* defaults they might have come from.
|
|
192
|
+
* @typedef {object} ClientHello
|
|
193
|
+
* @property {Uint8Array} message framed handshake message, ready for the record layer
|
|
194
|
+
* @property {Uint8Array} clientRandom
|
|
195
|
+
* @property {Uint8Array} legacySessionId
|
|
196
|
+
* @property {number[]} offeredCiphers
|
|
197
|
+
* @property {number[]} offeredGroups
|
|
198
|
+
* @property {number[]} offeredSigSchemes
|
|
199
|
+
* @property {Set<number>} offeredExtensions extension types present in the hello
|
|
200
|
+
* @property {string[]} offeredAlpn
|
|
201
|
+
* @property {number} [binderOffset] psk only: where the binder's bytes sit in `message`
|
|
202
|
+
* @property {number} [truncatedLength] psk only: how many leading bytes of `message` the binder
|
|
203
|
+
* transcript covers (RFC 8446 s4.2.11.2 truncation — everything except the binders list)
|
|
204
|
+
*/
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build a ClientHello. Returns the framed handshake message plus the metadata the rest of the
|
|
208
|
+
* handshake needs to police the server's answer.
|
|
209
|
+
*
|
|
210
|
+
* @param {ClientHelloOptions} opts
|
|
211
|
+
* @returns {ClientHello}
|
|
212
|
+
*/
|
|
213
|
+
export function buildClientHello({
|
|
214
|
+
hostname,
|
|
215
|
+
keyShares,
|
|
216
|
+
random,
|
|
217
|
+
legacySessionId,
|
|
218
|
+
ciphers,
|
|
219
|
+
groups = SUPPORTED_GROUPS,
|
|
220
|
+
sigSchemes = SUPPORTED_SIG_SCHEMES,
|
|
221
|
+
alpn = [ALPN_HTTP11],
|
|
222
|
+
versions = [TLS13, TLS12],
|
|
223
|
+
extraExtensions = [],
|
|
224
|
+
psk = null,
|
|
225
|
+
randomBytes = defaultRandom,
|
|
226
|
+
}) {
|
|
227
|
+
const clientRandom = random ?? randomBytes(32);
|
|
228
|
+
if (clientRandom.byteLength !== 32) {
|
|
229
|
+
throw new TlsError(codes.CONFIG_INVALID, `ClientHello.random must be 32 bytes, got ${clientRandom.byteLength}`);
|
|
230
|
+
}
|
|
231
|
+
// RFC 8446 s4.1.2: a non-empty legacy_session_id makes middleboxes treat the flow as a
|
|
232
|
+
// resumed TLS 1.2 session and leave it alone. 32 random bytes is what every browser sends.
|
|
233
|
+
const sessionId = legacySessionId ?? randomBytes(32);
|
|
234
|
+
|
|
235
|
+
const offersTls13 = versions.includes(TLS13);
|
|
236
|
+
const offersTls12 = versions.includes(TLS12);
|
|
237
|
+
const suites = ciphers ?? [
|
|
238
|
+
...(offersTls13 ? TLS13_CIPHERS : []),
|
|
239
|
+
...(offersTls12 ? TLS12_CIPHERS : []),
|
|
240
|
+
];
|
|
241
|
+
if (suites.length === 0) {
|
|
242
|
+
throw new TlsError(codes.CONFIG_INVALID, 'ClientHello would offer no cipher suites');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const suiteBytes = new Builder();
|
|
246
|
+
for (const s of suites) suiteBytes.u16(s);
|
|
247
|
+
|
|
248
|
+
if (psk && !offersTls13) {
|
|
249
|
+
// A pre_shared_key of this kind exists only in TLS 1.3 (RFC 5077 tickets are a different,
|
|
250
|
+
// unimplemented mechanism), so offering one from a hello that cannot negotiate 1.3 is a
|
|
251
|
+
// wiring bug that must not pass silently as "the server just declined".
|
|
252
|
+
throw new TlsError(codes.CONFIG_INVALID,
|
|
253
|
+
'a resumption PSK was supplied but TLS 1.3 is not among the offered versions');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const extensionParts = [
|
|
257
|
+
encodeServerName(hostname),
|
|
258
|
+
// Always offered, for either version: without it a server may not staple (RFC 6066 s8), and
|
|
259
|
+
// a stapled OCSP response is the only revocation signal this package can consume.
|
|
260
|
+
encodeStatusRequest(),
|
|
261
|
+
encodeSupportedGroups(groups),
|
|
262
|
+
encodeSignatureAlgorithms(sigSchemes),
|
|
263
|
+
alpn.length ? encodeAlpn(alpn) : null,
|
|
264
|
+
offersTls13 ? encodeSupportedVersions(versions) : null,
|
|
265
|
+
offersTls13 ? encodeKeyShare(keyShares.map(({ group, keyExchange }) => ({ group, keyExchange }))) : null,
|
|
266
|
+
offersTls13 ? encodePskKeyExchangeModes() : null,
|
|
267
|
+
offersTls12 ? encodeExtendedMasterSecret() : null,
|
|
268
|
+
offersTls12 ? encodeEcPointFormats() : null,
|
|
269
|
+
offersTls12 ? encodeRenegotiationInfo() : null,
|
|
270
|
+
// A HelloRetryRequest cookie arrives here, already encoded, and must go out verbatim.
|
|
271
|
+
...extraExtensions,
|
|
272
|
+
// pre_shared_key MUST be the last extension in the hello (RFC 8446 s4.2.11) — the binder
|
|
273
|
+
// transcript is "the hello truncated just before the binders", which only names a
|
|
274
|
+
// well-defined byte range if nothing follows them. Servers are required to check.
|
|
275
|
+
psk ? encodePreSharedKey(psk) : null,
|
|
276
|
+
];
|
|
277
|
+
const offered = new Set();
|
|
278
|
+
for (const part of extensionParts) {
|
|
279
|
+
if (part) offered.add((part[0] << 8) | part[1]);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const body = new Builder()
|
|
283
|
+
.u16(LEGACY_VERSION)
|
|
284
|
+
.push(clientRandom)
|
|
285
|
+
.vector(1, sessionId)
|
|
286
|
+
.vector(2, suiteBytes.build())
|
|
287
|
+
.vector(1, Uint8Array.from([0])) // legacy_compression_methods: null only
|
|
288
|
+
.push(encodeExtensionBlock(extensionParts))
|
|
289
|
+
.build();
|
|
290
|
+
|
|
291
|
+
const message = handshakeMessage(HANDSHAKE_TYPE.client_hello, body);
|
|
292
|
+
const out = {
|
|
293
|
+
message,
|
|
294
|
+
clientRandom,
|
|
295
|
+
legacySessionId: sessionId,
|
|
296
|
+
offeredCiphers: suites,
|
|
297
|
+
offeredGroups: groups,
|
|
298
|
+
offeredSigSchemes: sigSchemes,
|
|
299
|
+
offeredExtensions: offered,
|
|
300
|
+
offeredAlpn: alpn,
|
|
301
|
+
};
|
|
302
|
+
if (psk) {
|
|
303
|
+
// The two numbers that make the binder computable — and the two easiest to get wrong.
|
|
304
|
+
// Derived from the one formula in pskBinderTrailerLength, against the FINAL framed message,
|
|
305
|
+
// so "hello minus binders list" cannot drift from what was actually encoded.
|
|
306
|
+
out.binderOffset = message.byteLength - psk.binderLen;
|
|
307
|
+
out.truncatedLength = message.byteLength - pskBinderTrailerLength(psk.binderLen);
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Patch the real binder over the placeholder `buildClientHello` emitted. Separate from the
|
|
314
|
+
* builder because the binder is derived FROM the built message (truncated), so there is no
|
|
315
|
+
* ordering in which one function could do both.
|
|
316
|
+
* @param {ClientHello} hello a hello built with a psk offer
|
|
317
|
+
* @param {Uint8Array} binder
|
|
318
|
+
*/
|
|
319
|
+
export function setPskBinder(hello, binder) {
|
|
320
|
+
if (hello.binderOffset === undefined ||
|
|
321
|
+
hello.binderOffset + binder.byteLength !== hello.message.byteLength) {
|
|
322
|
+
throw new TlsError(codes.CONFIG_INVALID,
|
|
323
|
+
`cannot patch a ${binder.byteLength}-byte PSK binder: the hello reserved ` +
|
|
324
|
+
`${hello.binderOffset === undefined ? 'no placeholder'
|
|
325
|
+
: `${hello.message.byteLength - hello.binderOffset} bytes`}`);
|
|
326
|
+
}
|
|
327
|
+
hello.message.set(binder, hello.binderOffset);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ------------------------------------------------------------------ ServerHello
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* A parsed ServerHello. `isHelloRetryRequest` is decided by the random alone (RFC 8446 s4.1.3);
|
|
334
|
+
* everything else is exactly what the wire carried, judged later by the negotiate* functions.
|
|
335
|
+
* @typedef {object} ServerHello
|
|
336
|
+
* @property {number} legacyVersion
|
|
337
|
+
* @property {Uint8Array} random
|
|
338
|
+
* @property {Uint8Array} legacySessionIdEcho
|
|
339
|
+
* @property {number} cipherSuite
|
|
340
|
+
* @property {Map<number, Uint8Array>} extensions
|
|
341
|
+
* @property {boolean} isHelloRetryRequest
|
|
342
|
+
*/
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @param {Uint8Array} body
|
|
346
|
+
* @returns {ServerHello} throws on malformed encoding or a compression method other than null
|
|
347
|
+
*/
|
|
348
|
+
export function parseServerHello(body) {
|
|
349
|
+
const c = new Cursor(body, 'ServerHello');
|
|
350
|
+
const legacyVersion = c.u16('legacy_version');
|
|
351
|
+
const random = c.take(32, 'random');
|
|
352
|
+
const legacySessionIdEcho = c.vector(1, 'legacy_session_id_echo');
|
|
353
|
+
const cipherSuite = c.u16('cipher_suite');
|
|
354
|
+
const compressionMethod = c.u8('legacy_compression_method');
|
|
355
|
+
// RFC 5246 s7.4.1.4 makes the extension block optional, and TLS 1.2 servers with nothing to say
|
|
356
|
+
// do omit it entirely. (TLS 1.3 always has one, since supported_versions lives there — a 1.3
|
|
357
|
+
// ServerHello without extensions cannot pass negotiateVersion anyway.)
|
|
358
|
+
const extensions = c.done
|
|
359
|
+
? new Map()
|
|
360
|
+
: decodeExtensionBlock(c.vector(2, 'extensions'), 'ServerHello');
|
|
361
|
+
c.end('ServerHello');
|
|
362
|
+
|
|
363
|
+
if (compressionMethod !== 0) {
|
|
364
|
+
throw new TlsError(
|
|
365
|
+
codes.TLS_HANDSHAKE,
|
|
366
|
+
`server selected compression method ${compressionMethod}; TLS compression is not implemented and is unsafe (CRIME)`,
|
|
367
|
+
{ compressionMethod },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
legacyVersion,
|
|
372
|
+
random,
|
|
373
|
+
legacySessionIdEcho,
|
|
374
|
+
cipherSuite,
|
|
375
|
+
extensions,
|
|
376
|
+
isHelloRetryRequest: equal(random, HELLO_RETRY_REQUEST_RANDOM),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Decide the negotiated version, and refuse every shape of downgrade.
|
|
382
|
+
*
|
|
383
|
+
* The subtle one is the sentinel check (RFC 8446 s4.1.3): a server that supports TLS 1.3 but was
|
|
384
|
+
* pushed down to 1.2 by an attacker stripping our supported_versions plants a known value in the
|
|
385
|
+
* last 8 bytes of its random. A 1.3-capable client that ignores it is exactly the client the
|
|
386
|
+
* attack targets.
|
|
387
|
+
*
|
|
388
|
+
* @param {ServerHello} serverHello
|
|
389
|
+
* @param {{ offeredVersions: number[] }} offer
|
|
390
|
+
* @returns {number} the negotiated version; every downgrade shape throws instead
|
|
391
|
+
*/
|
|
392
|
+
export function negotiateVersion(serverHello, { offeredVersions }) {
|
|
393
|
+
const ext = serverHello.extensions.get(EXTENSION.supported_versions);
|
|
394
|
+
let selected;
|
|
395
|
+
if (ext) {
|
|
396
|
+
selected = decodeSelectedVersion(ext);
|
|
397
|
+
if (selected !== TLS13) {
|
|
398
|
+
throw new TlsError(
|
|
399
|
+
codes.TLS_VERSION_UNSUPPORTED,
|
|
400
|
+
`server sent supported_versions selecting ${describeVersion(selected)}; ` +
|
|
401
|
+
'that extension may only select TLS 1.3 in a ServerHello',
|
|
402
|
+
{ selected },
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
if (serverHello.legacyVersion !== LEGACY_VERSION) {
|
|
406
|
+
throw new TlsError(
|
|
407
|
+
codes.TLS_HANDSHAKE,
|
|
408
|
+
`server negotiated TLS 1.3 but set legacy_version to ${describeVersion(serverHello.legacyVersion)}, expected 0x0303`,
|
|
409
|
+
{ legacyVersion: serverHello.legacyVersion },
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
selected = serverHello.legacyVersion;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (!offeredVersions.includes(selected)) {
|
|
417
|
+
throw new TlsUnsupportedError(
|
|
418
|
+
codes.TLS_VERSION_UNSUPPORTED,
|
|
419
|
+
`server selected ${describeVersion(selected)}, which was not offered ` +
|
|
420
|
+
`(offered ${offeredVersions.map(describeVersion).join(', ')}). ` +
|
|
421
|
+
'TLS 1.0 and 1.1 are not implemented: their only cipher suites are RC4 and CBC ' +
|
|
422
|
+
'MAC-then-encrypt, whose padding check cannot be made constant-time in JavaScript.',
|
|
423
|
+
{ selected, offeredVersions },
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (selected === TLS12 && offeredVersions.includes(TLS13)) {
|
|
428
|
+
const tail = serverHello.random.subarray(24, 32);
|
|
429
|
+
if (timingSafeEqual(tail, DOWNGRADE_SENTINEL_12) || timingSafeEqual(tail, DOWNGRADE_SENTINEL_11)) {
|
|
430
|
+
throw new TlsError(
|
|
431
|
+
codes.TLS_VERSION_UNSUPPORTED,
|
|
432
|
+
'server planted the RFC 8446 downgrade sentinel in ServerHello.random while negotiating ' +
|
|
433
|
+
'TLS 1.2, which means a TLS 1.3 capable server saw a tampered ClientHello',
|
|
434
|
+
{ sentinel: true },
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return selected;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* @param {ServerHello} serverHello
|
|
443
|
+
* @param {{ offeredCiphers: number[], version: number }} offer the negotiated version re-checks
|
|
444
|
+
* the suite's family, so a union offer cannot run a 1.3 suite under 1.2 or the reverse
|
|
445
|
+
* @returns {{ suite: number, params: import('./constants.js').CipherParams }}
|
|
446
|
+
*/
|
|
447
|
+
export function negotiateCipher(serverHello, { offeredCiphers, version }) {
|
|
448
|
+
const suite = serverHello.cipherSuite;
|
|
449
|
+
if (!offeredCiphers.includes(suite)) {
|
|
450
|
+
throw new TlsUnsupportedError(
|
|
451
|
+
codes.TLS_CIPHER_UNSUPPORTED,
|
|
452
|
+
`server selected cipher suite ${hex16(suite)}` +
|
|
453
|
+
`${CIPHER_NAME[suite] ? ` (${CIPHER_NAME[suite]})` : ''} under ${describeVersion(version)}, ` +
|
|
454
|
+
'which was not offered. This package negotiates AEAD suites only: CBC suites are ' +
|
|
455
|
+
'MAC-then-encrypt and cannot be implemented without a Lucky13 padding oracle in JavaScript.',
|
|
456
|
+
{ cipherSuite: suite, version },
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const params = CIPHER_PARAMS[suite];
|
|
460
|
+
if (!params) {
|
|
461
|
+
throw new TlsUnsupportedError(
|
|
462
|
+
codes.TLS_CIPHER_UNSUPPORTED,
|
|
463
|
+
`cipher suite ${hex16(suite)} has no parameters; this is a bug in the offer list`,
|
|
464
|
+
{ cipherSuite: suite },
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
const isTls13Suite = TLS13_CIPHERS.includes(suite) || suite === 0x1303;
|
|
468
|
+
if (version === TLS13 && !isTls13Suite) {
|
|
469
|
+
throw new TlsError(
|
|
470
|
+
codes.TLS_CIPHER_UNSUPPORTED,
|
|
471
|
+
`server selected TLS 1.2 cipher suite ${hex16(suite)} under TLS 1.3`,
|
|
472
|
+
{ cipherSuite: suite },
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (version === TLS12 && isTls13Suite) {
|
|
476
|
+
throw new TlsError(
|
|
477
|
+
codes.TLS_CIPHER_UNSUPPORTED,
|
|
478
|
+
`server selected TLS 1.3 cipher suite ${hex16(suite)} under TLS 1.2`,
|
|
479
|
+
{ cipherSuite: suite },
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
return { suite, params };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* RFC 8446 s4.1.3: the server must echo legacy_session_id verbatim. A mismatch means the
|
|
487
|
+
* ServerHello does not belong to our ClientHello.
|
|
488
|
+
* @param {ServerHello} serverHello
|
|
489
|
+
* @param {Uint8Array} legacySessionId
|
|
490
|
+
* @returns {void} throws TlsError on mismatch
|
|
491
|
+
*/
|
|
492
|
+
export function checkSessionIdEcho(serverHello, legacySessionId) {
|
|
493
|
+
if (!equal(serverHello.legacySessionIdEcho, legacySessionId)) {
|
|
494
|
+
throw new TlsError(
|
|
495
|
+
codes.TLS_HANDSHAKE,
|
|
496
|
+
'server did not echo legacy_session_id; the ServerHello does not match our ClientHello',
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* The server's chosen key share, validated against what we actually offered.
|
|
503
|
+
* @param {ServerHello} serverHello
|
|
504
|
+
* @param {KeyShare[]} keyShares the shares we generated for the hello
|
|
505
|
+
* @returns {{ group: number, keyExchange: Uint8Array, privateKey: CryptoKey }} the server's
|
|
506
|
+
* group and public key, paired with OUR private key for it
|
|
507
|
+
*/
|
|
508
|
+
export function selectServerKeyShare(serverHello, keyShares) {
|
|
509
|
+
const ext = serverHello.extensions.get(EXTENSION.key_share);
|
|
510
|
+
if (!ext) {
|
|
511
|
+
throw new TlsError(
|
|
512
|
+
codes.TLS_HANDSHAKE,
|
|
513
|
+
'TLS 1.3 ServerHello has no key_share extension; PSK-only resumption is not supported',
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const { group, keyExchange } = decodeKeyShareEntry(ext, 'ServerHello');
|
|
517
|
+
const mine = keyShares.find((k) => k.group === group);
|
|
518
|
+
if (!mine) {
|
|
519
|
+
throw new TlsError(
|
|
520
|
+
codes.TLS_HANDSHAKE,
|
|
521
|
+
`server chose key_share group ${hex16(group)} for which no share was offered`,
|
|
522
|
+
{ group },
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return { group, keyExchange, privateKey: mine.privateKey };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* HelloRetryRequest: the server names one group and expects a fresh ClientHello.
|
|
530
|
+
* @param {ServerHello} serverHello
|
|
531
|
+
* @param {{ offeredGroups: number[] }} offer
|
|
532
|
+
* @returns {{ group: number, cookie: Uint8Array | null }}
|
|
533
|
+
*/
|
|
534
|
+
export function parseHelloRetryRequest(serverHello, { offeredGroups }) {
|
|
535
|
+
const ext = serverHello.extensions.get(EXTENSION.key_share);
|
|
536
|
+
if (!ext) {
|
|
537
|
+
throw new TlsError(codes.TLS_HANDSHAKE, 'HelloRetryRequest has no key_share extension');
|
|
538
|
+
}
|
|
539
|
+
const group = decodeKeyShareHrr(ext);
|
|
540
|
+
if (!offeredGroups.includes(group)) {
|
|
541
|
+
throw new TlsError(
|
|
542
|
+
codes.TLS_HANDSHAKE,
|
|
543
|
+
`HelloRetryRequest asked for group ${hex16(group)}, which was not in supported_groups`,
|
|
544
|
+
{ group },
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
requireSupportedGroup(group, 'HelloRetryRequest');
|
|
548
|
+
const cookie = serverHello.extensions.get(EXTENSION.cookie) ?? null;
|
|
549
|
+
return { group, cookie };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ------------------------------------------------------------------ NewSessionTicket
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* NewSessionTicket (RFC 8446 s4.6.1), the post-handshake message a resumption PSK is minted
|
|
556
|
+
* from. Strict on structure — a truncated field, trailing bytes, or a zero-length ticket ends
|
|
557
|
+
* the connection, because a peer whose post-handshake messages do not parse cannot be trusted
|
|
558
|
+
* to frame the application data either — but faithful to s4.6.1 on extensions: early_data is
|
|
559
|
+
* validated in shape and its value RECORDED but never acted on (0-RTT is deliberately not
|
|
560
|
+
* implemented; see the driver), and unrecognized extensions are ignored, which s4.6.1 makes
|
|
561
|
+
* mandatory ("Clients MUST ignore unrecognized extensions").
|
|
562
|
+
*
|
|
563
|
+
* Lifetime semantics (zero means discard, 604800 s is the cap a client may honour) are POLICY,
|
|
564
|
+
* applied by the ticket store; this function reports what the wire said.
|
|
565
|
+
*
|
|
566
|
+
* @param {Uint8Array} body
|
|
567
|
+
* @returns {{ lifetimeSec: number, ageAdd: number, nonce: Uint8Array, ticket: Uint8Array,
|
|
568
|
+
* maxEarlyDataSize: number | null }}
|
|
569
|
+
*/
|
|
570
|
+
export function parseNewSessionTicket(body) {
|
|
571
|
+
const c = new Cursor(body, 'NewSessionTicket');
|
|
572
|
+
const lifetimeSec = c.u32('ticket_lifetime');
|
|
573
|
+
const ageAdd = c.u32('ticket_age_add');
|
|
574
|
+
const nonce = c.vector(1, 'ticket_nonce');
|
|
575
|
+
const ticket = c.vector(2, 'ticket');
|
|
576
|
+
const exts = decodeExtensionBlock(c.vector(2, 'extensions'), 'NewSessionTicket');
|
|
577
|
+
c.end('NewSessionTicket');
|
|
578
|
+
if (ticket.byteLength === 0) {
|
|
579
|
+
// opaque ticket<1..2^16-1>: zero is outside the vector's floor, and an empty identity could
|
|
580
|
+
// never be offered back (encodePreSharedKey refuses it), so it is dead on arrival.
|
|
581
|
+
throw new TlsError(codes.TLS_TICKET, 'NewSessionTicket carries a zero-length ticket');
|
|
582
|
+
}
|
|
583
|
+
let maxEarlyDataSize = null;
|
|
584
|
+
for (const [type, data] of exts) {
|
|
585
|
+
if (type === EXTENSION.early_data) {
|
|
586
|
+
if (data.byteLength !== 4) {
|
|
587
|
+
throw new TlsError(codes.TLS_TICKET,
|
|
588
|
+
`NewSessionTicket early_data extension is ${data.byteLength} bytes; ` +
|
|
589
|
+
'max_early_data_size is a uint32 (RFC 8446 s4.2.10)',
|
|
590
|
+
{ length: data.byteLength });
|
|
591
|
+
}
|
|
592
|
+
maxEarlyDataSize = new Cursor(data, 'early_data').u32('max_early_data_size');
|
|
593
|
+
}
|
|
594
|
+
// Anything else: ignored by requirement of s4.6.1. Duplicates were already fatal above.
|
|
595
|
+
}
|
|
596
|
+
return { lifetimeSec, ageAdd, nonce, ticket, maxEarlyDataSize };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ------------------------------------------------------------------ Certificate
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* The CertificateStatus body of RFC 6066 s8: `status_type(1) || opaque OCSPResponse<1..2^24-1>`.
|
|
603
|
+
* Two carriers share this exact shape — the TLS 1.2 CertificateStatus handshake message, and the
|
|
604
|
+
* extension_data of a TLS 1.3 status_request CertificateEntry extension (RFC 8446 s4.4.2.1) —
|
|
605
|
+
* which is why it is one parser and not two.
|
|
606
|
+
*
|
|
607
|
+
* @param {Uint8Array} body
|
|
608
|
+
* @param {string} where named in errors
|
|
609
|
+
* @returns {Uint8Array} the DER OCSPResponse, exactly as sent; its meaning is the trust layer's
|
|
610
|
+
* problem, not this layer's
|
|
611
|
+
*/
|
|
612
|
+
export function parseCertificateStatus(body, where) {
|
|
613
|
+
const c = new Cursor(body, where);
|
|
614
|
+
const statusType = c.u8('status_type');
|
|
615
|
+
if (statusType !== 1) {
|
|
616
|
+
throw new TlsError(
|
|
617
|
+
codes.TLS_HANDSHAKE,
|
|
618
|
+
`${where} carries status_type ${statusType}; only ocsp(1) is defined (RFC 6066 s8) and ` +
|
|
619
|
+
'nothing else can be consumed',
|
|
620
|
+
{ statusType },
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
const response = c.vector(3, 'OCSPResponse');
|
|
624
|
+
c.end(where);
|
|
625
|
+
if (response.byteLength === 0) {
|
|
626
|
+
// opaque OCSPResponse<1..2^24-1>: the zero length is outside the vector's floor, and an
|
|
627
|
+
// empty "response" pretending to be a staple must not read as one.
|
|
628
|
+
throw new TlsError(codes.TLS_HANDSHAKE, `${where} carries a zero-length OCSPResponse`);
|
|
629
|
+
}
|
|
630
|
+
return response;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* TLS 1.3 Certificate (RFC 8446 s4.4.2): context, then entries carrying per-cert extensions.
|
|
635
|
+
*
|
|
636
|
+
* Entry extensions are policed, not skipped: RFC 8446 s4.4.2 allows a server to send only
|
|
637
|
+
* extensions the ClientHello offered, and s4.2 confines each type to specific messages — for
|
|
638
|
+
* CertificateEntry that is status_request and signed_certificate_timestamp. An extension we
|
|
639
|
+
* cannot attribute to our own offer is either a server confusion or a smuggling attempt, and
|
|
640
|
+
* both end the handshake.
|
|
641
|
+
*
|
|
642
|
+
* Only the LEAF's stapled OCSP response is returned. A server may staple for intermediates too;
|
|
643
|
+
* those staples are validated structurally (they must still be well-formed CertificateStatus)
|
|
644
|
+
* but not consumed — this package checks revocation of the identity it is authenticating, and
|
|
645
|
+
* inventing partial intermediate coverage would imply a guarantee it does not give.
|
|
646
|
+
*
|
|
647
|
+
* @param {Uint8Array} body
|
|
648
|
+
* @param {{ offeredExtensions?: Set<number> }} [opts] extension types our ClientHello offered.
|
|
649
|
+
* Omitting it means "nothing was offered", the fail-closed reading.
|
|
650
|
+
* @returns {{ chain: Uint8Array[], ocspResponse: Uint8Array | null }} DER certificates in wire
|
|
651
|
+
* order (leaf first), plus the leaf's stapled DER OCSPResponse if the server sent one
|
|
652
|
+
*/
|
|
653
|
+
export function parseCertificate13(body, { offeredExtensions = new Set() } = {}) {
|
|
654
|
+
const c = new Cursor(body, 'Certificate');
|
|
655
|
+
const context = c.vector(1, 'certificate_request_context');
|
|
656
|
+
if (context.byteLength !== 0) {
|
|
657
|
+
throw new TlsError(
|
|
658
|
+
codes.TLS_HANDSHAKE,
|
|
659
|
+
`server Certificate has a ${context.byteLength}-byte certificate_request_context; must be empty`,
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
const list = c.sub(3, 'certificate_list');
|
|
663
|
+
const chain = [];
|
|
664
|
+
let ocspResponse = null;
|
|
665
|
+
while (!list.done) {
|
|
666
|
+
const der = list.vector(3, 'cert_data');
|
|
667
|
+
if (der.byteLength === 0) {
|
|
668
|
+
throw new TlsError(codes.TLS_HANDSHAKE, 'Certificate entry has zero-length cert_data');
|
|
669
|
+
}
|
|
670
|
+
const entryLabel = `CertificateEntry ${chain.length}`;
|
|
671
|
+
const exts = decodeExtensionBlock(list.vector(2, 'certificate extensions'), entryLabel);
|
|
672
|
+
rejectUnofferedExtensions(exts, offeredExtensions, entryLabel);
|
|
673
|
+
for (const [type, data] of exts) {
|
|
674
|
+
if (type === EXTENSION.status_request) {
|
|
675
|
+
const staple = parseCertificateStatus(data, `${entryLabel} status_request`);
|
|
676
|
+
if (chain.length === 0) ocspResponse = staple;
|
|
677
|
+
} else if (type !== EXTENSION.signed_certificate_timestamp) {
|
|
678
|
+
// Offered in the hello, but RFC 8446 s4.2 does not admit it in a Certificate message:
|
|
679
|
+
// the server answered a question in the wrong room, which s4.2 makes fatal
|
|
680
|
+
// (illegal_parameter), not ignorable.
|
|
681
|
+
throw new TlsError(
|
|
682
|
+
codes.TLS_HANDSHAKE,
|
|
683
|
+
`${entryLabel} carries extension ${hex16(type)}, which does not belong in a ` +
|
|
684
|
+
'Certificate message (RFC 8446 s4.2)',
|
|
685
|
+
{ extension: type },
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
chain.push(der);
|
|
690
|
+
}
|
|
691
|
+
c.end('Certificate');
|
|
692
|
+
if (chain.length === 0) {
|
|
693
|
+
throw new CertificateError(codes.CERT_CHAIN_INCOMPLETE, 'server sent an empty certificate_list');
|
|
694
|
+
}
|
|
695
|
+
return { chain, ocspResponse };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* TLS 1.2 Certificate (RFC 5246 s7.4.2): a bare list, no context and no per-cert extensions.
|
|
700
|
+
* @param {Uint8Array} body
|
|
701
|
+
* @returns {Uint8Array[]} DER certificates in wire order, leaf first
|
|
702
|
+
*/
|
|
703
|
+
export function parseCertificate12(body) {
|
|
704
|
+
const c = new Cursor(body, 'Certificate');
|
|
705
|
+
const list = c.sub(3, 'certificate_list');
|
|
706
|
+
const chain = [];
|
|
707
|
+
while (!list.done) {
|
|
708
|
+
const der = list.vector(3, 'certificate');
|
|
709
|
+
if (der.byteLength === 0) {
|
|
710
|
+
throw new TlsError(codes.TLS_HANDSHAKE, 'Certificate entry has zero length');
|
|
711
|
+
}
|
|
712
|
+
chain.push(der);
|
|
713
|
+
}
|
|
714
|
+
c.end('Certificate');
|
|
715
|
+
if (chain.length === 0) {
|
|
716
|
+
throw new CertificateError(codes.CERT_CHAIN_INCOMPLETE, 'server sent an empty certificate_list');
|
|
717
|
+
}
|
|
718
|
+
return chain;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* @param {Uint8Array} body
|
|
723
|
+
* @returns {{ algorithm: number, signature: Uint8Array }}
|
|
724
|
+
*/
|
|
725
|
+
export function parseCertificateVerify(body) {
|
|
726
|
+
const c = new Cursor(body, 'CertificateVerify');
|
|
727
|
+
const algorithm = c.u16('signature algorithm');
|
|
728
|
+
const signature = c.vector(2, 'signature');
|
|
729
|
+
c.end('CertificateVerify');
|
|
730
|
+
return { algorithm, signature };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* RFC 8446 s4.4.3: 64 spaces, a context string, a zero byte, then the transcript hash.
|
|
735
|
+
* @param {Uint8Array} transcriptHash
|
|
736
|
+
* @param {boolean} [isServer]
|
|
737
|
+
* @returns {Uint8Array}
|
|
738
|
+
*/
|
|
739
|
+
export function certificateVerifyContent(transcriptHash, isServer = true) {
|
|
740
|
+
const label = isServer ? 'TLS 1.3, server CertificateVerify' : 'TLS 1.3, client CertificateVerify';
|
|
741
|
+
return concat([new Uint8Array(64).fill(0x20), utf8(label), Uint8Array.from([0]), transcriptHash]);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Verify a handshake signature with WebCrypto.
|
|
746
|
+
* `spki` is the DER SubjectPublicKeyInfo lifted straight out of the leaf certificate, so the key
|
|
747
|
+
* used to check the signature is provably the key the trust layer validated.
|
|
748
|
+
*
|
|
749
|
+
* @param {object} args
|
|
750
|
+
* @param {number} args.scheme signature scheme id from the wire
|
|
751
|
+
* @param {Uint8Array} args.spki DER SubjectPublicKeyInfo of the validated leaf
|
|
752
|
+
* @param {Uint8Array} args.signature as received: DER for ECDSA, raw otherwise
|
|
753
|
+
* @param {Uint8Array} args.content the exact bytes the signature must cover
|
|
754
|
+
* @returns {Promise<true>} every failure throws; there is no false
|
|
755
|
+
*/
|
|
756
|
+
export async function verifyHandshakeSignature({ scheme, spki, signature, content }) {
|
|
757
|
+
const params = SIG_SCHEME_PARAMS[scheme];
|
|
758
|
+
if (!params) {
|
|
759
|
+
throw new TlsUnsupportedError(
|
|
760
|
+
codes.TLS_SIGALG_UNSUPPORTED,
|
|
761
|
+
`server signed the handshake with ${describeSigScheme(scheme)}, which is not implemented; ` +
|
|
762
|
+
`offered ${SUPPORTED_SIG_SCHEMES.map(describeSigScheme).join(', ')}`,
|
|
763
|
+
{ scheme },
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
let key;
|
|
767
|
+
try {
|
|
768
|
+
key = await crypto.subtle.importKey('spki', spki, params.import, false, ['verify']);
|
|
769
|
+
} catch (cause) {
|
|
770
|
+
throw new TlsError(
|
|
771
|
+
codes.TLS_HANDSHAKE,
|
|
772
|
+
`certificate public key cannot be used with ${describeSigScheme(scheme)}: ${cause?.message}`,
|
|
773
|
+
{ scheme },
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// TLS carries ECDSA signatures as a DER ECDSA-Sig-Value; WebCrypto accepts only the fixed-width
|
|
778
|
+
// r||s concatenation. Omitting this conversion produces a client that verifies its own
|
|
779
|
+
// signatures happily and fails against every real ECDSA server — which is most of them.
|
|
780
|
+
const sigBytes =
|
|
781
|
+
params.format === 'ecdsa-der'
|
|
782
|
+
? ecdsaDerToRaw(signature, params.curveOrderLen, (why) =>
|
|
783
|
+
new TlsError(
|
|
784
|
+
codes.TLS_HANDSHAKE,
|
|
785
|
+
`handshake signature (${describeSigScheme(scheme)}) is not a well-formed ` +
|
|
786
|
+
`ECDSA-Sig-Value: ${why}`,
|
|
787
|
+
{ scheme },
|
|
788
|
+
))
|
|
789
|
+
: signature;
|
|
790
|
+
|
|
791
|
+
const ok = await crypto.subtle.verify(params.verify, key, sigBytes, content);
|
|
792
|
+
if (!ok) {
|
|
793
|
+
throw new TlsError(
|
|
794
|
+
codes.TLS_HANDSHAKE,
|
|
795
|
+
`handshake signature (${describeSigScheme(scheme)}) does not verify against the certificate public key`,
|
|
796
|
+
{ scheme },
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ------------------------------------------------------------------ TLS 1.2 ServerKeyExchange
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* RFC 8422 s5.4. Only named-curve ECDHE is accepted: explicit curves are a decade-dead feature
|
|
806
|
+
* and finite-field DHE would need bignum arithmetic WebCrypto does not expose.
|
|
807
|
+
*
|
|
808
|
+
* @param {Uint8Array} body
|
|
809
|
+
* @returns {{ group: number, publicKey: Uint8Array, signatureAlgorithm: number,
|
|
810
|
+
* signature: Uint8Array, signedParams: Uint8Array }} `signedParams` is the exact byte range
|
|
811
|
+
* the server's signature covers (curve_type through the public key)
|
|
812
|
+
*/
|
|
813
|
+
export function parseServerKeyExchangeEcdhe(body) {
|
|
814
|
+
const c = new Cursor(body, 'ServerKeyExchange');
|
|
815
|
+
const curveType = c.u8('curve_type');
|
|
816
|
+
if (curveType !== 3) {
|
|
817
|
+
throw new TlsUnsupportedError(
|
|
818
|
+
codes.TLS_GROUP_UNSUPPORTED,
|
|
819
|
+
`server used ECCurveType ${curveType} in ServerKeyExchange; only named_curve (3) is implemented`,
|
|
820
|
+
{ curveType },
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
const group = c.u16('named_curve');
|
|
824
|
+
const publicKey = c.vector(1, 'ECPoint');
|
|
825
|
+
const signatureAlgorithm = c.u16('SignatureAndHashAlgorithm');
|
|
826
|
+
const signature = c.vector(2, 'signature');
|
|
827
|
+
c.end('ServerKeyExchange');
|
|
828
|
+
// The signed blob is everything from curve_type through the public key.
|
|
829
|
+
const signedParams = body.subarray(0, 4 + publicKey.byteLength);
|
|
830
|
+
return { group, publicKey, signatureAlgorithm, signature, signedParams };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* TLS 1.2 signs client_random || server_random || ServerECDHParams.
|
|
835
|
+
* @param {Uint8Array} clientRandom
|
|
836
|
+
* @param {Uint8Array} serverRandom
|
|
837
|
+
* @param {Uint8Array} signedParams
|
|
838
|
+
* @returns {Uint8Array}
|
|
839
|
+
*/
|
|
840
|
+
export function serverKeyExchangeContent(clientRandom, serverRandom, signedParams) {
|
|
841
|
+
return concat([clientRandom, serverRandom, signedParams]);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* @param {Uint8Array} publicKey
|
|
846
|
+
* @returns {Uint8Array} framed ClientKeyExchange message
|
|
847
|
+
*/
|
|
848
|
+
export function buildClientKeyExchange(publicKey) {
|
|
849
|
+
return handshakeMessage(HANDSHAKE_TYPE.client_key_exchange, vector(1, publicKey));
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* @param {Uint8Array} verifyData
|
|
854
|
+
* @returns {Uint8Array} framed Finished message
|
|
855
|
+
*/
|
|
856
|
+
export function buildFinished(verifyData) {
|
|
857
|
+
return handshakeMessage(HANDSHAKE_TYPE.finished, verifyData);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Compare a peer Finished against ours. Constant-time in intent: verify_data is derived from
|
|
862
|
+
* secrets the peer must already know, so a timing leak is not a decryption oracle, but there is
|
|
863
|
+
* no reason to leak the prefix length either.
|
|
864
|
+
* @param {Uint8Array} received
|
|
865
|
+
* @param {Uint8Array} expected
|
|
866
|
+
* @returns {true} a mismatch throws; there is no false
|
|
867
|
+
*/
|
|
868
|
+
export function checkFinished(received, expected) {
|
|
869
|
+
if (!timingSafeEqual(received, expected)) {
|
|
870
|
+
throw new TlsError(
|
|
871
|
+
codes.TLS_HANDSHAKE,
|
|
872
|
+
'peer Finished verify_data does not match; the handshake transcript differs, which means ' +
|
|
873
|
+
'the connection was tampered with or the peer is not speaking the negotiated parameters',
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
return true;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* The negotiated ALPN protocol, refusing anything we did not offer.
|
|
881
|
+
* @param {Map<number, Uint8Array>} extensions
|
|
882
|
+
* @param {string[]} offeredAlpn
|
|
883
|
+
* @param {string} where
|
|
884
|
+
* @returns {string | null} null when the server declined ALPN entirely
|
|
885
|
+
*/
|
|
886
|
+
export function checkAlpn(extensions, offeredAlpn, where) {
|
|
887
|
+
const ext = extensions.get(EXTENSION.alpn);
|
|
888
|
+
if (!ext) return null; // absent means the server declined; HTTP/1.1 is the default anyway
|
|
889
|
+
const selected = decodeAlpn(ext);
|
|
890
|
+
if (!offeredAlpn.includes(selected)) {
|
|
891
|
+
throw new TlsError(
|
|
892
|
+
codes.TLS_ALPN,
|
|
893
|
+
`server selected ALPN protocol "${selected}" in ${where}, which was not offered ` +
|
|
894
|
+
`(offered ${offeredAlpn.map((p) => `"${p}"`).join(', ')})`,
|
|
895
|
+
{ selected, offeredAlpn },
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return selected;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export { GROUP_PARAMS };
|