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,507 @@
|
|
|
1
|
+
// TLS 1.2 client handshake driver.
|
|
2
|
+
//
|
|
3
|
+
// This driver exists for servers that never learned TLS 1.3; it is a fallback, not a peer of
|
|
4
|
+
// handshake.js in importance, but it must meet exactly the same bar: on this runtime nothing
|
|
5
|
+
// below us verifies a tunnelled peer, so every guarantee the caller gets is manufactured here.
|
|
6
|
+
// The negotiable surface is deliberately tiny — ECDHE key exchange and AES-GCM only (see
|
|
7
|
+
// constants.js for the reasoning) — and three refusals are absolute:
|
|
8
|
+
//
|
|
9
|
+
// * No renegotiation. A HelloRequest at any point is an error, never a trigger. Renegotiation
|
|
10
|
+
// is the mechanism behind the 2009 prefix-injection MITM (CVE-2009-3555), and a client that
|
|
11
|
+
// only ever dials out has no legitimate use for it.
|
|
12
|
+
// * No resumption. We send a throwaway session id (middlebox camouflage, same as 1.3); a
|
|
13
|
+
// server echoing it announces an abbreviated handshake that skips authentication, which is
|
|
14
|
+
// not a handshake we are willing to have. Every connection pays for the full flight.
|
|
15
|
+
// * No ServerKeyExchange, no connection. Without it there is no ephemeral key: the selected
|
|
16
|
+
// suite would degenerate into static key transport with no forward secrecy, which this
|
|
17
|
+
// package refuses to implement.
|
|
18
|
+
//
|
|
19
|
+
// Two ordering rules are load-bearing, mirroring the 1.3 driver:
|
|
20
|
+
//
|
|
21
|
+
// 1. The certificate chain is validated by the injected trust layer BEFORE the
|
|
22
|
+
// ServerKeyExchange signature is checked against the leaf's key, and both happen before any
|
|
23
|
+
// key material or Finished leaves this side. Signature-then-trust would only prove that
|
|
24
|
+
// whoever holds the socket also holds a key — no evidence at all.
|
|
25
|
+
// 2. TLS 1.2 signs only client_random || server_random || ServerECDHParams — the ServerHello
|
|
26
|
+
// and the chosen suite are NOT covered by that signature, only (eventually) by Finished. So
|
|
27
|
+
// the transcript must be byte-exact at both Finished computations, and when the server
|
|
28
|
+
// supports extended master secret (RFC 7627) the master secret itself is bound to the
|
|
29
|
+
// session hash, closing the triple-handshake gap that the plain randoms-based derivation
|
|
30
|
+
// leaves open. EMS is preferred whenever offered back; a server that declines is accepted,
|
|
31
|
+
// but the downgrade is reported in info.extendedMasterSecret so callers can see it.
|
|
32
|
+
//
|
|
33
|
+
// The ClientHello/ServerHello preamble lives in connect.js, shared with the 1.3 driver so that
|
|
34
|
+
// one hello can offer both versions; continueTls12 below is everything that happens after the
|
|
35
|
+
// ServerHello routed the connection here. handshakeTls12 remains the single-version entry: it is
|
|
36
|
+
// connectTls with the offer pinned to [TLS 1.2], which keeps the hello free of every 1.3
|
|
37
|
+
// extension exactly as this driver always sent it.
|
|
38
|
+
//
|
|
39
|
+
// Verification is injected rather than imported, for the same reasons as the 1.3 driver: this
|
|
40
|
+
// module holds no trust policy, and the handshake must be replayable offline.
|
|
41
|
+
|
|
42
|
+
import { TlsError, TlsUnsupportedError, codes, hex8, hex16 } from '../errors.js';
|
|
43
|
+
import {
|
|
44
|
+
CIPHER_NAME,
|
|
45
|
+
EXTENSION,
|
|
46
|
+
HANDSHAKE_TYPE,
|
|
47
|
+
SIG_SCHEME,
|
|
48
|
+
TLS12,
|
|
49
|
+
} from './constants.js';
|
|
50
|
+
import {
|
|
51
|
+
extendedMasterSecret12,
|
|
52
|
+
keyBlock12,
|
|
53
|
+
masterSecret12,
|
|
54
|
+
verifyData12,
|
|
55
|
+
} from './keyschedule.js';
|
|
56
|
+
import {
|
|
57
|
+
buildClientKeyExchange,
|
|
58
|
+
buildFinished,
|
|
59
|
+
checkAlpn,
|
|
60
|
+
checkFinished,
|
|
61
|
+
deriveSharedSecret,
|
|
62
|
+
generateKeyShare,
|
|
63
|
+
parseCertificate12,
|
|
64
|
+
parseCertificateStatus,
|
|
65
|
+
parseServerKeyExchangeEcdhe,
|
|
66
|
+
serverKeyExchangeContent,
|
|
67
|
+
verifyHandshakeSignature,
|
|
68
|
+
} from './handshake-messages.js';
|
|
69
|
+
import { describeSigScheme, rejectUnofferedExtensions } from './extensions.js';
|
|
70
|
+
import { equal } from '../util/bytes.js';
|
|
71
|
+
import { handshakeMessage, vector } from './wire.js';
|
|
72
|
+
import { connectTls } from './connect.js';
|
|
73
|
+
|
|
74
|
+
const HS_NAME = Object.fromEntries(Object.entries(HANDSHAKE_TYPE).map(([k, v]) => [v, k]));
|
|
75
|
+
|
|
76
|
+
/** HelloRequest has no entry in HANDSHAKE_TYPE because no other module may ever act on one. */
|
|
77
|
+
const HELLO_REQUEST = 0;
|
|
78
|
+
|
|
79
|
+
const describeType = (t) => `${hex8(t)} (${HS_NAME[t] ?? 'unknown'})`;
|
|
80
|
+
const describeSuite = (s) => `${hex16(s)}${CIPHER_NAME[s] ? ` (${CIPHER_NAME[s]})` : ''}`;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Which authentication family a signature scheme proves. The cipher suite name pins the server
|
|
84
|
+
* certificate's key type (ECDHE_RSA vs ECDHE_ECDSA, with EdDSA folded into the ECDSA family by
|
|
85
|
+
* RFC 8422 s5.10), but the ServerKeyExchange signature does not cover the ServerHello — so this
|
|
86
|
+
* check is the only thing tying the signature algorithm to the suite the server claims to be
|
|
87
|
+
* honouring until Finished lands.
|
|
88
|
+
*/
|
|
89
|
+
const SIG_KIND = {
|
|
90
|
+
[SIG_SCHEME.ecdsa_secp256r1_sha256]: 'ecdsa',
|
|
91
|
+
[SIG_SCHEME.ecdsa_secp384r1_sha384]: 'ecdsa',
|
|
92
|
+
[SIG_SCHEME.ecdsa_secp521r1_sha512]: 'ecdsa',
|
|
93
|
+
[SIG_SCHEME.ed25519]: 'ecdsa',
|
|
94
|
+
[SIG_SCHEME.rsa_pkcs1_sha256]: 'rsa',
|
|
95
|
+
[SIG_SCHEME.rsa_pkcs1_sha384]: 'rsa',
|
|
96
|
+
[SIG_SCHEME.rsa_pkcs1_sha512]: 'rsa',
|
|
97
|
+
[SIG_SCHEME.rsa_pss_rsae_sha256]: 'rsa',
|
|
98
|
+
[SIG_SCHEME.rsa_pss_rsae_sha384]: 'rsa',
|
|
99
|
+
[SIG_SCHEME.rsa_pss_rsae_sha512]: 'rsa',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The one refusal that must fire regardless of which message was expected. Exported for
|
|
104
|
+
* connect.js, whose ServerHello wait is the earliest point a 1.2 server can spring one.
|
|
105
|
+
* @returns {never}
|
|
106
|
+
*/
|
|
107
|
+
export function refuseHelloRequest() {
|
|
108
|
+
throw new TlsError(
|
|
109
|
+
codes.TLS_HANDSHAKE,
|
|
110
|
+
'server sent HelloRequest; renegotiation is refused. Renegotiation lets a MITM splice an ' +
|
|
111
|
+
'attacker-controlled prefix onto the authenticated stream (CVE-2009-3555), and this ' +
|
|
112
|
+
'client has no use for it',
|
|
113
|
+
{ type: HELLO_REQUEST },
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Demand a specific handshake message, turning every other outcome into a named failure. */
|
|
118
|
+
function expect(msg, type, where) {
|
|
119
|
+
if (msg === null) {
|
|
120
|
+
throw new TlsError(
|
|
121
|
+
codes.TLS_TRUNCATED,
|
|
122
|
+
`server closed the connection during the handshake while ${where} was expected`,
|
|
123
|
+
{ expected: HS_NAME[type] ?? type },
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (msg.ccs) {
|
|
127
|
+
throw new TlsError(codes.TLS_RECORD, `change_cipher_spec arrived where ${where} was expected`, {
|
|
128
|
+
expected: HS_NAME[type] ?? type,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (msg.type === HELLO_REQUEST) refuseHelloRequest();
|
|
132
|
+
if (msg.type !== type) {
|
|
133
|
+
throw new TlsError(
|
|
134
|
+
codes.TLS_HANDSHAKE,
|
|
135
|
+
`server sent handshake type ${describeType(msg.type)} where ${where} was expected`,
|
|
136
|
+
{ got: msg.type, expected: type },
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return msg;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Demand the server's ChangeCipherSpec, which in 1.2 is a real key-change signal. */
|
|
143
|
+
function expectCcs(msg) {
|
|
144
|
+
if (msg === null) {
|
|
145
|
+
throw new TlsError(
|
|
146
|
+
codes.TLS_TRUNCATED,
|
|
147
|
+
'server closed the connection while its ChangeCipherSpec was expected',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (msg.ccs) return;
|
|
151
|
+
if (msg.type === HELLO_REQUEST) refuseHelloRequest();
|
|
152
|
+
throw new TlsError(
|
|
153
|
+
codes.TLS_HANDSHAKE,
|
|
154
|
+
`server sent handshake type ${describeType(msg.type)} where its ChangeCipherSpec was ` +
|
|
155
|
+
'expected; a Finished before the key change would arrive unprotected',
|
|
156
|
+
{ got: msg.type },
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Run a TLS 1.2 handshake over a byte duplex and return the plaintext duplex above it.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} args
|
|
165
|
+
* @param {import('./connect.js').ByteDuplex} args.transport
|
|
166
|
+
* @param {string} args.hostname the identity the certificate must prove, and the SNI sent
|
|
167
|
+
* @param {import('./handshake.js').VerifyPeer} args.verifyPeer
|
|
168
|
+
* Must throw to reject. Resolves with the validated leaf; its SPKI is the only key this
|
|
169
|
+
* handshake will accept a ServerKeyExchange signature from.
|
|
170
|
+
* @param {import('./connect.js').TlsOptions} [args.options] `versions` is ignored: this entry
|
|
171
|
+
* pins the offer to [TLS 1.2]
|
|
172
|
+
* @param {import('./connect.js').TlsDeps} [args.deps]
|
|
173
|
+
* @returns {Promise<import('./connect.js').TlsSession>}
|
|
174
|
+
*/
|
|
175
|
+
export async function handshakeTls12({ transport, hostname, verifyPeer, options = {}, deps = {} }) {
|
|
176
|
+
if (typeof verifyPeer !== 'function') {
|
|
177
|
+
// Refusing to start is the only safe default. A missing verifier must never read as "skip".
|
|
178
|
+
throw new TlsError(
|
|
179
|
+
codes.CONFIG_INVALID,
|
|
180
|
+
'handshakeTls12 requires a verifyPeer function; there is no unverified mode',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
// The offer is pinned to [TLS 1.2]: no supported_versions, no key_share, no PSK modes. A
|
|
184
|
+
// 1.3-capable ClientHello answered by this driver would mean the version dispatch above us
|
|
185
|
+
// routed wrongly, and the 1.3 downgrade protections (sentinel, supported_versions echo) would
|
|
186
|
+
// silently not apply — so the offer itself must make that state unrepresentable.
|
|
187
|
+
return connectTls({
|
|
188
|
+
transport,
|
|
189
|
+
hostname,
|
|
190
|
+
verifyPeer,
|
|
191
|
+
deps,
|
|
192
|
+
options: { ...options, versions: [TLS12] },
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Continue a TLS 1.2 handshake from the ServerHello. Called by connect.js once negotiation
|
|
198
|
+
* routed the connection here; the record layer arrives already pinned to 1.2 semantics and the
|
|
199
|
+
* transcript already runs under the negotiated suite's PRF hash.
|
|
200
|
+
* @param {import('./handshake.js').HandshakeContext} ctx
|
|
201
|
+
* @returns {Promise<import('./connect.js').TlsSession>}
|
|
202
|
+
*/
|
|
203
|
+
export async function continueTls12(ctx) {
|
|
204
|
+
const { record, transcript, hello, serverHello: sh, rawServerHello, suite, params } = ctx;
|
|
205
|
+
const { hostname, verifyPeer, deps } = ctx;
|
|
206
|
+
const { alpn, groups } = ctx.offer;
|
|
207
|
+
const hash = params.hash;
|
|
208
|
+
|
|
209
|
+
// RFC 5246 s7.4.1.3: a server echoing the client's session id is announcing an abbreviated
|
|
210
|
+
// handshake — ChangeCipherSpec and Finished next, no certificate, keys from a cached master
|
|
211
|
+
// secret we do not hold. Ours is a throwaway id, so an echo is either a broken cache or an
|
|
212
|
+
// attempt to skip authentication; the CCS guard below would catch the flight shape anyway,
|
|
213
|
+
// but naming the cause here beats "change_cipher_spec arrived where Certificate was expected".
|
|
214
|
+
if (
|
|
215
|
+
hello.legacySessionId.byteLength > 0 &&
|
|
216
|
+
equal(sh.legacySessionIdEcho, hello.legacySessionId)
|
|
217
|
+
) {
|
|
218
|
+
throw new TlsError(
|
|
219
|
+
codes.TLS_HANDSHAKE,
|
|
220
|
+
'server echoed our session id, which announces an abbreviated (resumed) handshake; ' +
|
|
221
|
+
'session resumption is not supported and the handshake is never abbreviated',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// RFC 5246 s7.4.1.4: the ServerHello may only carry extensions the ClientHello offered. An
|
|
226
|
+
// unoffered one (session_ticket is the classic) would commit us to protocol behaviour we do
|
|
227
|
+
// not implement, so it is refused before any of its semantics can be presumed.
|
|
228
|
+
rejectUnofferedExtensions(sh.extensions, hello.offeredExtensions, 'ServerHello');
|
|
229
|
+
|
|
230
|
+
// When the ClientHello offered 1.3 as well, the 1.3-only extensions WERE offered, so the
|
|
231
|
+
// check above can no longer catch a server echoing them while negotiating 1.2. RFC 8446
|
|
232
|
+
// s4.1.4 permits key_share in a ServerHello only when 1.3 was selected, and
|
|
233
|
+
// psk_key_exchange_modes may never appear in one at all; a server that answers the 1.3 half
|
|
234
|
+
// of the offer while refusing the 1.3 that gives it meaning is confused at best and splicing
|
|
235
|
+
// two handshakes at worst. (supported_versions cannot reach this driver: negotiateVersion
|
|
236
|
+
// either routed it to 1.3 or refused it outright.)
|
|
237
|
+
for (const type of [EXTENSION.key_share, EXTENSION.psk_key_exchange_modes]) {
|
|
238
|
+
if (sh.extensions.has(type)) {
|
|
239
|
+
throw new TlsError(
|
|
240
|
+
codes.TLS_HANDSHAKE,
|
|
241
|
+
`server echoed the TLS 1.3-only extension ${hex16(type)} in a ServerHello that ` +
|
|
242
|
+
'negotiates TLS 1.2',
|
|
243
|
+
{ extension: type },
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const reneg = sh.extensions.get(EXTENSION.renegotiation_info);
|
|
249
|
+
if (reneg && !(reneg.byteLength === 1 && reneg[0] === 0x00)) {
|
|
250
|
+
// RFC 5746 s3.4: on an initial handshake the server's renegotiated_connection must be
|
|
251
|
+
// empty. Anything else means the server believes this connection is a renegotiation.
|
|
252
|
+
throw new TlsError(
|
|
253
|
+
codes.TLS_HANDSHAKE,
|
|
254
|
+
'server sent a non-empty renegotiation_info, treating this connection as a ' +
|
|
255
|
+
'renegotiation; renegotiation is refused',
|
|
256
|
+
{ length: reneg.byteLength },
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const emsEcho = sh.extensions.get(EXTENSION.extended_master_secret);
|
|
261
|
+
if (emsEcho && emsEcho.byteLength !== 0) {
|
|
262
|
+
throw new TlsError(
|
|
263
|
+
codes.TLS_HANDSHAKE,
|
|
264
|
+
`extended_master_secret extension_data must be empty, got ${emsEcho.byteLength} bytes`,
|
|
265
|
+
{ length: emsEcho.byteLength },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const extendedMasterSecret = emsEcho !== undefined;
|
|
269
|
+
|
|
270
|
+
// RFC 6066 s8: a server that intends to send CertificateStatus announces it by echoing
|
|
271
|
+
// status_request with EMPTY extension_data. The echo is permission to send the message, not a
|
|
272
|
+
// promise to — a server may echo and then have nothing to staple.
|
|
273
|
+
const statusEcho = sh.extensions.get(EXTENSION.status_request);
|
|
274
|
+
if (statusEcho && statusEcho.byteLength !== 0) {
|
|
275
|
+
throw new TlsError(
|
|
276
|
+
codes.TLS_HANDSHAKE,
|
|
277
|
+
`status_request in a ServerHello must have empty extension_data (RFC 6066 s8), got ` +
|
|
278
|
+
`${statusEcho.byteLength} bytes`,
|
|
279
|
+
{ length: statusEcho.byteLength },
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// In 1.2 the ALPN answer lives in the ServerHello — there is no EncryptedExtensions.
|
|
284
|
+
const alpnProtocol = checkAlpn(sh.extensions, alpn, 'ServerHello');
|
|
285
|
+
transcript.update(rawServerHello);
|
|
286
|
+
|
|
287
|
+
// --- server flight -------------------------------------------------------------------------
|
|
288
|
+
const certMsg = expect(
|
|
289
|
+
await record.nextHandshakeMessage(),
|
|
290
|
+
HANDSHAKE_TYPE.certificate,
|
|
291
|
+
'Certificate',
|
|
292
|
+
);
|
|
293
|
+
transcript.update(certMsg.raw);
|
|
294
|
+
const chain = parseCertificate12(certMsg.body);
|
|
295
|
+
|
|
296
|
+
let next = await record.nextHandshakeMessage();
|
|
297
|
+
|
|
298
|
+
// CertificateStatus (RFC 6066 s8): the stapled OCSP response, arriving as its own handshake
|
|
299
|
+
// message immediately after Certificate — TLS 1.2's shape of what 1.3 carries inside the
|
|
300
|
+
// CertificateEntry. Legal only when the ServerHello echoed status_request; a staple nobody
|
|
301
|
+
// announced is a message from outside the negotiated protocol and ends the handshake. It is
|
|
302
|
+
// optional even when announced. The message must be folded into the transcript exactly here,
|
|
303
|
+
// like every other handshake message, or both Finished computations drift.
|
|
304
|
+
let ocspResponse = null;
|
|
305
|
+
if (next !== null && !next.ccs && next.type === HANDSHAKE_TYPE.certificate_status) {
|
|
306
|
+
if (statusEcho === undefined) {
|
|
307
|
+
throw new TlsError(
|
|
308
|
+
codes.TLS_HANDSHAKE,
|
|
309
|
+
'server sent CertificateStatus without echoing status_request in its ServerHello ' +
|
|
310
|
+
'(RFC 6066 s8 requires the echo first)',
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
ocspResponse = parseCertificateStatus(next.body, 'CertificateStatus');
|
|
314
|
+
transcript.update(next.raw);
|
|
315
|
+
next = await record.nextHandshakeMessage();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (
|
|
319
|
+
next !== null &&
|
|
320
|
+
!next.ccs &&
|
|
321
|
+
(next.type === HANDSHAKE_TYPE.certificate_request ||
|
|
322
|
+
next.type === HANDSHAKE_TYPE.server_hello_done)
|
|
323
|
+
) {
|
|
324
|
+
// The flight skipped straight past ServerKeyExchange. For every suite this package offers
|
|
325
|
+
// that message carries the ephemeral key, so its absence is not a variant — it is the
|
|
326
|
+
// server trying to run the suite as static key transport.
|
|
327
|
+
throw new TlsError(
|
|
328
|
+
codes.TLS_HANDSHAKE,
|
|
329
|
+
`server omitted ServerKeyExchange under ${describeSuite(suite)}. Every suite this ` +
|
|
330
|
+
'package offers is ECDHE: without ServerKeyExchange there is no ephemeral key and no ' +
|
|
331
|
+
'forward secrecy, and static-RSA key transport is deliberately not implemented',
|
|
332
|
+
{ cipherSuite: suite, got: next.type },
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const skeMsg = expect(next, HANDSHAKE_TYPE.server_key_exchange, 'ServerKeyExchange');
|
|
336
|
+
transcript.update(skeMsg.raw);
|
|
337
|
+
const ske = parseServerKeyExchangeEcdhe(skeMsg.body);
|
|
338
|
+
if (!groups.includes(ske.group)) {
|
|
339
|
+
throw new TlsError(
|
|
340
|
+
codes.TLS_HANDSHAKE,
|
|
341
|
+
`server chose group ${hex16(ske.group)} in ServerKeyExchange, which was not offered in ` +
|
|
342
|
+
'supported_groups',
|
|
343
|
+
{ group: ske.group },
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
let certificateRequested = false;
|
|
348
|
+
next = await record.nextHandshakeMessage();
|
|
349
|
+
if (next !== null && !next.ccs && next.type === HANDSHAKE_TYPE.certificate_request) {
|
|
350
|
+
// We hold no client certificate, so the body's demands are irrelevant — the answer is an
|
|
351
|
+
// empty Certificate in the client flight either way (RFC 5246 s7.4.6) — but the message
|
|
352
|
+
// still has to be remembered and folded into the transcript at this exact point.
|
|
353
|
+
certificateRequested = true;
|
|
354
|
+
transcript.update(next.raw);
|
|
355
|
+
next = await record.nextHandshakeMessage();
|
|
356
|
+
}
|
|
357
|
+
const done = expect(next, HANDSHAKE_TYPE.server_hello_done, 'ServerHelloDone');
|
|
358
|
+
if (done.body.byteLength !== 0) {
|
|
359
|
+
throw new TlsError(
|
|
360
|
+
codes.TLS_HANDSHAKE,
|
|
361
|
+
`ServerHelloDone must have an empty body, got ${done.body.byteLength} bytes`,
|
|
362
|
+
{ length: done.body.byteLength },
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
transcript.update(done.raw);
|
|
366
|
+
|
|
367
|
+
// --- trust, then the ServerKeyExchange signature -------------------------------------------
|
|
368
|
+
// Trust first. The signature check below is only meaningful once the key performing it has
|
|
369
|
+
// been tied by the trust layer to a chain we accept for this hostname; done the other way
|
|
370
|
+
// round it merely proves that whoever holds the socket also holds a key, which is no evidence
|
|
371
|
+
// at all. Nothing of ours — no key share, no Finished — has been sent yet, so a failure here
|
|
372
|
+
// leaks nothing but the ClientHello.
|
|
373
|
+
const peer = await verifyPeer(chain, hostname, { ocspResponse });
|
|
374
|
+
const spki = peer?.spki?.spkiDer;
|
|
375
|
+
if (!spki) {
|
|
376
|
+
throw new TlsError(
|
|
377
|
+
codes.CONFIG_INVALID,
|
|
378
|
+
'verifyPeer must resolve with the validated leaf certificate, including spki.spkiDer',
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const scheme = ske.signatureAlgorithm;
|
|
383
|
+
if (!hello.offeredSigSchemes.includes(scheme)) {
|
|
384
|
+
// RFC 5246 s7.4.3: the server must sign with an algorithm from our signature_algorithms.
|
|
385
|
+
throw new TlsUnsupportedError(
|
|
386
|
+
codes.TLS_SIGALG_UNSUPPORTED,
|
|
387
|
+
`server signed ServerKeyExchange with ${describeSigScheme(scheme)}, which was not ` +
|
|
388
|
+
'offered in signature_algorithms',
|
|
389
|
+
{ scheme },
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
if (SIG_KIND[scheme] !== params.sig) {
|
|
393
|
+
throw new TlsError(
|
|
394
|
+
codes.TLS_HANDSHAKE,
|
|
395
|
+
`cipher suite ${describeSuite(suite)} authenticates with ${params.sig}, but ` +
|
|
396
|
+
`ServerKeyExchange is signed with ${describeSigScheme(scheme)}`,
|
|
397
|
+
{ cipherSuite: suite, scheme },
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
// The signature goes to verifyHandshakeSignature in the form it arrived in. That function owns
|
|
401
|
+
// the DER-to-P1363 conversion for ECDSA, so both handshake drivers share one implementation of
|
|
402
|
+
// it — converting here as well would hand it an already-converted signature to re-parse.
|
|
403
|
+
await verifyHandshakeSignature({
|
|
404
|
+
scheme,
|
|
405
|
+
spki,
|
|
406
|
+
signature: ske.signature,
|
|
407
|
+
content: serverKeyExchangeContent(hello.clientRandom, sh.random, ske.signedParams),
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// --- key exchange --------------------------------------------------------------------------
|
|
411
|
+
// The server's point is validated (length, uncompressed form, on-curve via the WebCrypto
|
|
412
|
+
// import) before anything of ours goes on the wire.
|
|
413
|
+
const share = await generateKeyShare(ske.group, deps);
|
|
414
|
+
const preMaster = await deriveSharedSecret(ske.group, share.privateKey, ske.publicKey);
|
|
415
|
+
|
|
416
|
+
// --- client flight -------------------------------------------------------------------------
|
|
417
|
+
const clientFlight = [];
|
|
418
|
+
if (certificateRequested) {
|
|
419
|
+
// RFC 5246 s7.4.6: no credentials means a Certificate message with an empty
|
|
420
|
+
// certificate_list — and, because the list is empty, no CertificateVerify afterwards.
|
|
421
|
+
const empty = handshakeMessage(HANDSHAKE_TYPE.certificate, vector(3, new Uint8Array(0)));
|
|
422
|
+
clientFlight.push(empty);
|
|
423
|
+
transcript.update(empty);
|
|
424
|
+
}
|
|
425
|
+
const cke = buildClientKeyExchange(share.keyExchange);
|
|
426
|
+
clientFlight.push(cke);
|
|
427
|
+
transcript.update(cke);
|
|
428
|
+
await record.writeHandshake(clientFlight);
|
|
429
|
+
|
|
430
|
+
// RFC 7627: with EMS the master secret is bound to the session hash — the transcript through
|
|
431
|
+
// ClientKeyExchange, hashed right here, before Finished joins it — so it cannot be replayed
|
|
432
|
+
// onto a different handshake (the triple-handshake attack). Without the extension the RFC 5246
|
|
433
|
+
// derivation over the two randoms is all there is; that acceptance is recorded in
|
|
434
|
+
// info.extendedMasterSecret rather than hidden.
|
|
435
|
+
const master = extendedMasterSecret
|
|
436
|
+
? await extendedMasterSecret12(hash, preMaster, await transcript.hash())
|
|
437
|
+
: await masterSecret12(hash, preMaster, hello.clientRandom, sh.random);
|
|
438
|
+
|
|
439
|
+
// RFC 5246 s6.3 puts MAC keys first in the key_block, but AEAD suites have none — a stray
|
|
440
|
+
// MAC-key slot would silently shift every later slice into the wrong bytes, so the layout is
|
|
441
|
+
// asserted rather than trusted.
|
|
442
|
+
const kb = await keyBlock12(hash, master, hello.clientRandom, sh.random, {
|
|
443
|
+
keyLen: params.keyLen,
|
|
444
|
+
fixedIvLen: params.fixedIvLen,
|
|
445
|
+
});
|
|
446
|
+
if (
|
|
447
|
+
kb.clientWriteMacKey !== undefined ||
|
|
448
|
+
kb.clientWriteKey.byteLength !== params.keyLen ||
|
|
449
|
+
kb.serverWriteKey.byteLength !== params.keyLen ||
|
|
450
|
+
kb.clientWriteIv.byteLength !== params.fixedIvLen ||
|
|
451
|
+
kb.serverWriteIv.byteLength !== params.fixedIvLen
|
|
452
|
+
) {
|
|
453
|
+
throw new TlsError(
|
|
454
|
+
codes.CONFIG_INVALID,
|
|
455
|
+
'key_block layout is wrong for an AEAD suite; a MAC-key slot would shift every slice',
|
|
456
|
+
{ cipherSuite: suite },
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Our ChangeCipherSpec, then Finished under the new keys. The verify_data covers everything
|
|
461
|
+
// through ClientKeyExchange; the Finished message itself then joins the transcript because
|
|
462
|
+
// the server's Finished must cover ours (RFC 5246 s7.4.9).
|
|
463
|
+
await record.writeChangeCipherSpec();
|
|
464
|
+
await record.setSendKeys({ cipher: suite, key: kb.clientWriteKey, iv: kb.clientWriteIv });
|
|
465
|
+
const finished = buildFinished(
|
|
466
|
+
await verifyData12(hash, master, 'client finished', await transcript.hash()),
|
|
467
|
+
);
|
|
468
|
+
transcript.update(finished);
|
|
469
|
+
await record.writeHandshake([finished]);
|
|
470
|
+
|
|
471
|
+
// --- server ChangeCipherSpec and Finished --------------------------------------------------
|
|
472
|
+
// The {ccs: true} event is the record layer telling us the peer switched keys — a real
|
|
473
|
+
// signal in 1.2, unlike the 1.3 compatibility noise — and receive keys are installed only in
|
|
474
|
+
// response to it. The record layer separately guarantees no handshake message spans the
|
|
475
|
+
// change and that a second CCS (renegotiation's opening move) is fatal.
|
|
476
|
+
expectCcs(await record.nextHandshakeMessage());
|
|
477
|
+
await record.setReceiveKeys({ cipher: suite, key: kb.serverWriteKey, iv: kb.serverWriteIv });
|
|
478
|
+
|
|
479
|
+
const sf = expect(await record.nextHandshakeMessage(), HANDSHAKE_TYPE.finished, 'server Finished');
|
|
480
|
+
checkFinished(sf.body, await verifyData12(hash, master, 'server finished', await transcript.hash()));
|
|
481
|
+
|
|
482
|
+
record.markHandshakeComplete();
|
|
483
|
+
// Lazy for the same reasons as the 1.3 driver: direct record-layer consumers never need the
|
|
484
|
+
// platform-stream wrappers, and the warmup replay runs where constructing them is forbidden.
|
|
485
|
+
let duplex = null;
|
|
486
|
+
const lazyDuplex = () => (duplex ??= record.plaintextDuplex());
|
|
487
|
+
return {
|
|
488
|
+
get readable() {
|
|
489
|
+
return lazyDuplex().readable;
|
|
490
|
+
},
|
|
491
|
+
get writable() {
|
|
492
|
+
return lazyDuplex().writable;
|
|
493
|
+
},
|
|
494
|
+
record,
|
|
495
|
+
peer,
|
|
496
|
+
info: {
|
|
497
|
+
version: TLS12,
|
|
498
|
+
cipherSuite: suite,
|
|
499
|
+
group: ske.group,
|
|
500
|
+
alpnProtocol: alpnProtocol ?? null,
|
|
501
|
+
certificateRequested,
|
|
502
|
+
extendedMasterSecret,
|
|
503
|
+
hostname,
|
|
504
|
+
},
|
|
505
|
+
close: () => record.close(),
|
|
506
|
+
};
|
|
507
|
+
}
|
package/src/tls/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The TLS layer as a standalone transport: give it a byte duplex, get a plaintext one back.
|
|
2
|
+
//
|
|
3
|
+
// Exported separately because the layering is the point — anyone with a socket-shaped thing and a
|
|
4
|
+
// trust policy can use this without the HTTP client above it, and every layer in this package is
|
|
5
|
+
// testable over an in-memory pipe for exactly that reason.
|
|
6
|
+
|
|
7
|
+
export { connectTls } from './connect.js';
|
|
8
|
+
export { handshakeTls13 } from './handshake.js';
|
|
9
|
+
export { handshakeTls12 } from './handshake12.js';
|
|
10
|
+
export { RecordLayer } from './record.js';
|
|
11
|
+
export { Transcript } from './transcript.js';
|
|
12
|
+
export { createAead, buildNonce } from './aead.js';
|
|
13
|
+
// Session resumption: the per-client ticket store, exported so a direct connectTls consumer can
|
|
14
|
+
// wire options.psk / options.onSessionTicket with the same lifetime and single-use policy the
|
|
15
|
+
// Client uses. Keying discipline is the caller's burden and is documented on the class.
|
|
16
|
+
export { TicketStore } from './tickets.js';
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
ALPN_HTTP11,
|
|
20
|
+
CIPHER,
|
|
21
|
+
CIPHER_NAME,
|
|
22
|
+
CIPHER_PARAMS,
|
|
23
|
+
GROUP,
|
|
24
|
+
GROUP_NAME,
|
|
25
|
+
SIG_SCHEME,
|
|
26
|
+
SIG_SCHEME_NAME,
|
|
27
|
+
SUPPORTED_GROUPS,
|
|
28
|
+
SUPPORTED_SIG_SCHEMES,
|
|
29
|
+
TLS12,
|
|
30
|
+
TLS12_CIPHERS,
|
|
31
|
+
TLS13,
|
|
32
|
+
TLS13_CIPHERS,
|
|
33
|
+
VERSION_NAME,
|
|
34
|
+
} from './constants.js';
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
buildClientHello,
|
|
38
|
+
deriveSharedSecret,
|
|
39
|
+
generateKeyShare,
|
|
40
|
+
negotiateCipher,
|
|
41
|
+
negotiateVersion,
|
|
42
|
+
parseServerHello,
|
|
43
|
+
verifyHandshakeSignature,
|
|
44
|
+
} from './handshake-messages.js';
|