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,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An ephemeral key share: the public half as sent in key_share, plus the private key the
|
|
3
|
+
* eventual ServerHello selection will feed into deriveSharedSecret.
|
|
4
|
+
* @typedef {object} KeyShare
|
|
5
|
+
* @property {number} group
|
|
6
|
+
* @property {Uint8Array} keyExchange raw public key, the exact bytes on the wire
|
|
7
|
+
* @property {CryptoKey} privateKey non-extractable
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Generate an ephemeral key share for one group.
|
|
11
|
+
* `generateKeyPair` is injectable so a recorded handshake can be replayed with the exact private
|
|
12
|
+
* key that produced it.
|
|
13
|
+
*
|
|
14
|
+
* @param {number} group
|
|
15
|
+
* @param {import('./connect.js').TlsDeps} [deps]
|
|
16
|
+
* @returns {Promise<KeyShare>}
|
|
17
|
+
*/
|
|
18
|
+
export function generateKeyShare(group: number, { generateKeyPair }?: import("./connect.js").TlsDeps): Promise<KeyShare>;
|
|
19
|
+
/**
|
|
20
|
+
* ECDH/X25519 shared secret. The peer's key is imported in raw form, which is where a malformed
|
|
21
|
+
* point is caught: WebCrypto rejects a point that is not on the curve, so we do not have to
|
|
22
|
+
* implement that check ourselves — but a wrong LENGTH would be accepted by some implementations,
|
|
23
|
+
* so it is checked here first.
|
|
24
|
+
*
|
|
25
|
+
* @param {number} group
|
|
26
|
+
* @param {CryptoKey} privateKey our ephemeral private key for the group
|
|
27
|
+
* @param {Uint8Array} peerKey the server's raw public key from its key_share
|
|
28
|
+
* @returns {Promise<Uint8Array>} throws on any degenerate or malformed peer key
|
|
29
|
+
*/
|
|
30
|
+
export function deriveSharedSecret(group: number, privateKey: CryptoKey, peerKey: Uint8Array): Promise<Uint8Array>;
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} ClientHelloOptions
|
|
33
|
+
* @property {string} hostname SNI, unless it is an IP literal (then no SNI is sent)
|
|
34
|
+
* @property {Array<{ group: number, keyExchange: Uint8Array }>} keyShares public halves to
|
|
35
|
+
* offer; empty for a 1.2-only hello, whose wire form must not carry the extension at all
|
|
36
|
+
* @property {Uint8Array} [random] fixed ClientHello.random, for reproducible handshakes
|
|
37
|
+
* @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise
|
|
38
|
+
* @property {number[]} [ciphers] default: the union for the offered versions, 1.3 first
|
|
39
|
+
* @property {number[]} [groups] supported_groups, default SUPPORTED_GROUPS
|
|
40
|
+
* @property {number[]} [sigSchemes] default SUPPORTED_SIG_SCHEMES
|
|
41
|
+
* @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
|
|
42
|
+
* @property {number[]} [versions] default [TLS13, TLS12]
|
|
43
|
+
* @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
|
|
44
|
+
* @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
|
|
45
|
+
* offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
|
|
46
|
+
* the real binder over `message.subarray(0, truncatedLength)` and patch it in at
|
|
47
|
+
* `binderOffset` before the hello touches the wire — a zero binder on the wire is a hello
|
|
48
|
+
* every honest server must reject.
|
|
49
|
+
* @property {(n: number) => Uint8Array} [randomBytes] injectable randomness
|
|
50
|
+
*/
|
|
51
|
+
/**
|
|
52
|
+
* The built hello plus everything later steps need to police the server's answer against what
|
|
53
|
+
* was actually offered — negotiation checks must run against this record, never against the
|
|
54
|
+
* defaults they might have come from.
|
|
55
|
+
* @typedef {object} ClientHello
|
|
56
|
+
* @property {Uint8Array} message framed handshake message, ready for the record layer
|
|
57
|
+
* @property {Uint8Array} clientRandom
|
|
58
|
+
* @property {Uint8Array} legacySessionId
|
|
59
|
+
* @property {number[]} offeredCiphers
|
|
60
|
+
* @property {number[]} offeredGroups
|
|
61
|
+
* @property {number[]} offeredSigSchemes
|
|
62
|
+
* @property {Set<number>} offeredExtensions extension types present in the hello
|
|
63
|
+
* @property {string[]} offeredAlpn
|
|
64
|
+
* @property {number} [binderOffset] psk only: where the binder's bytes sit in `message`
|
|
65
|
+
* @property {number} [truncatedLength] psk only: how many leading bytes of `message` the binder
|
|
66
|
+
* transcript covers (RFC 8446 s4.2.11.2 truncation — everything except the binders list)
|
|
67
|
+
*/
|
|
68
|
+
/**
|
|
69
|
+
* Build a ClientHello. Returns the framed handshake message plus the metadata the rest of the
|
|
70
|
+
* handshake needs to police the server's answer.
|
|
71
|
+
*
|
|
72
|
+
* @param {ClientHelloOptions} opts
|
|
73
|
+
* @returns {ClientHello}
|
|
74
|
+
*/
|
|
75
|
+
export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extraExtensions, psk, randomBytes, }: ClientHelloOptions): ClientHello;
|
|
76
|
+
/**
|
|
77
|
+
* Patch the real binder over the placeholder `buildClientHello` emitted. Separate from the
|
|
78
|
+
* builder because the binder is derived FROM the built message (truncated), so there is no
|
|
79
|
+
* ordering in which one function could do both.
|
|
80
|
+
* @param {ClientHello} hello a hello built with a psk offer
|
|
81
|
+
* @param {Uint8Array} binder
|
|
82
|
+
*/
|
|
83
|
+
export function setPskBinder(hello: ClientHello, binder: Uint8Array): void;
|
|
84
|
+
/**
|
|
85
|
+
* A parsed ServerHello. `isHelloRetryRequest` is decided by the random alone (RFC 8446 s4.1.3);
|
|
86
|
+
* everything else is exactly what the wire carried, judged later by the negotiate* functions.
|
|
87
|
+
* @typedef {object} ServerHello
|
|
88
|
+
* @property {number} legacyVersion
|
|
89
|
+
* @property {Uint8Array} random
|
|
90
|
+
* @property {Uint8Array} legacySessionIdEcho
|
|
91
|
+
* @property {number} cipherSuite
|
|
92
|
+
* @property {Map<number, Uint8Array>} extensions
|
|
93
|
+
* @property {boolean} isHelloRetryRequest
|
|
94
|
+
*/
|
|
95
|
+
/**
|
|
96
|
+
* @param {Uint8Array} body
|
|
97
|
+
* @returns {ServerHello} throws on malformed encoding or a compression method other than null
|
|
98
|
+
*/
|
|
99
|
+
export function parseServerHello(body: Uint8Array): ServerHello;
|
|
100
|
+
/**
|
|
101
|
+
* Decide the negotiated version, and refuse every shape of downgrade.
|
|
102
|
+
*
|
|
103
|
+
* The subtle one is the sentinel check (RFC 8446 s4.1.3): a server that supports TLS 1.3 but was
|
|
104
|
+
* pushed down to 1.2 by an attacker stripping our supported_versions plants a known value in the
|
|
105
|
+
* last 8 bytes of its random. A 1.3-capable client that ignores it is exactly the client the
|
|
106
|
+
* attack targets.
|
|
107
|
+
*
|
|
108
|
+
* @param {ServerHello} serverHello
|
|
109
|
+
* @param {{ offeredVersions: number[] }} offer
|
|
110
|
+
* @returns {number} the negotiated version; every downgrade shape throws instead
|
|
111
|
+
*/
|
|
112
|
+
export function negotiateVersion(serverHello: ServerHello, { offeredVersions }: {
|
|
113
|
+
offeredVersions: number[];
|
|
114
|
+
}): number;
|
|
115
|
+
/**
|
|
116
|
+
* @param {ServerHello} serverHello
|
|
117
|
+
* @param {{ offeredCiphers: number[], version: number }} offer the negotiated version re-checks
|
|
118
|
+
* the suite's family, so a union offer cannot run a 1.3 suite under 1.2 or the reverse
|
|
119
|
+
* @returns {{ suite: number, params: import('./constants.js').CipherParams }}
|
|
120
|
+
*/
|
|
121
|
+
export function negotiateCipher(serverHello: ServerHello, { offeredCiphers, version }: {
|
|
122
|
+
offeredCiphers: number[];
|
|
123
|
+
version: number;
|
|
124
|
+
}): {
|
|
125
|
+
suite: number;
|
|
126
|
+
params: import("./constants.js").CipherParams;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* RFC 8446 s4.1.3: the server must echo legacy_session_id verbatim. A mismatch means the
|
|
130
|
+
* ServerHello does not belong to our ClientHello.
|
|
131
|
+
* @param {ServerHello} serverHello
|
|
132
|
+
* @param {Uint8Array} legacySessionId
|
|
133
|
+
* @returns {void} throws TlsError on mismatch
|
|
134
|
+
*/
|
|
135
|
+
export function checkSessionIdEcho(serverHello: ServerHello, legacySessionId: Uint8Array): void;
|
|
136
|
+
/**
|
|
137
|
+
* The server's chosen key share, validated against what we actually offered.
|
|
138
|
+
* @param {ServerHello} serverHello
|
|
139
|
+
* @param {KeyShare[]} keyShares the shares we generated for the hello
|
|
140
|
+
* @returns {{ group: number, keyExchange: Uint8Array, privateKey: CryptoKey }} the server's
|
|
141
|
+
* group and public key, paired with OUR private key for it
|
|
142
|
+
*/
|
|
143
|
+
export function selectServerKeyShare(serverHello: ServerHello, keyShares: KeyShare[]): {
|
|
144
|
+
group: number;
|
|
145
|
+
keyExchange: Uint8Array;
|
|
146
|
+
privateKey: CryptoKey;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* HelloRetryRequest: the server names one group and expects a fresh ClientHello.
|
|
150
|
+
* @param {ServerHello} serverHello
|
|
151
|
+
* @param {{ offeredGroups: number[] }} offer
|
|
152
|
+
* @returns {{ group: number, cookie: Uint8Array | null }}
|
|
153
|
+
*/
|
|
154
|
+
export function parseHelloRetryRequest(serverHello: ServerHello, { offeredGroups }: {
|
|
155
|
+
offeredGroups: number[];
|
|
156
|
+
}): {
|
|
157
|
+
group: number;
|
|
158
|
+
cookie: Uint8Array | null;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* NewSessionTicket (RFC 8446 s4.6.1), the post-handshake message a resumption PSK is minted
|
|
162
|
+
* from. Strict on structure — a truncated field, trailing bytes, or a zero-length ticket ends
|
|
163
|
+
* the connection, because a peer whose post-handshake messages do not parse cannot be trusted
|
|
164
|
+
* to frame the application data either — but faithful to s4.6.1 on extensions: early_data is
|
|
165
|
+
* validated in shape and its value RECORDED but never acted on (0-RTT is deliberately not
|
|
166
|
+
* implemented; see the driver), and unrecognized extensions are ignored, which s4.6.1 makes
|
|
167
|
+
* mandatory ("Clients MUST ignore unrecognized extensions").
|
|
168
|
+
*
|
|
169
|
+
* Lifetime semantics (zero means discard, 604800 s is the cap a client may honour) are POLICY,
|
|
170
|
+
* applied by the ticket store; this function reports what the wire said.
|
|
171
|
+
*
|
|
172
|
+
* @param {Uint8Array} body
|
|
173
|
+
* @returns {{ lifetimeSec: number, ageAdd: number, nonce: Uint8Array, ticket: Uint8Array,
|
|
174
|
+
* maxEarlyDataSize: number | null }}
|
|
175
|
+
*/
|
|
176
|
+
export function parseNewSessionTicket(body: Uint8Array): {
|
|
177
|
+
lifetimeSec: number;
|
|
178
|
+
ageAdd: number;
|
|
179
|
+
nonce: Uint8Array;
|
|
180
|
+
ticket: Uint8Array;
|
|
181
|
+
maxEarlyDataSize: number | null;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* The CertificateStatus body of RFC 6066 s8: `status_type(1) || opaque OCSPResponse<1..2^24-1>`.
|
|
185
|
+
* Two carriers share this exact shape — the TLS 1.2 CertificateStatus handshake message, and the
|
|
186
|
+
* extension_data of a TLS 1.3 status_request CertificateEntry extension (RFC 8446 s4.4.2.1) —
|
|
187
|
+
* which is why it is one parser and not two.
|
|
188
|
+
*
|
|
189
|
+
* @param {Uint8Array} body
|
|
190
|
+
* @param {string} where named in errors
|
|
191
|
+
* @returns {Uint8Array} the DER OCSPResponse, exactly as sent; its meaning is the trust layer's
|
|
192
|
+
* problem, not this layer's
|
|
193
|
+
*/
|
|
194
|
+
export function parseCertificateStatus(body: Uint8Array, where: string): Uint8Array;
|
|
195
|
+
/**
|
|
196
|
+
* TLS 1.3 Certificate (RFC 8446 s4.4.2): context, then entries carrying per-cert extensions.
|
|
197
|
+
*
|
|
198
|
+
* Entry extensions are policed, not skipped: RFC 8446 s4.4.2 allows a server to send only
|
|
199
|
+
* extensions the ClientHello offered, and s4.2 confines each type to specific messages — for
|
|
200
|
+
* CertificateEntry that is status_request and signed_certificate_timestamp. An extension we
|
|
201
|
+
* cannot attribute to our own offer is either a server confusion or a smuggling attempt, and
|
|
202
|
+
* both end the handshake.
|
|
203
|
+
*
|
|
204
|
+
* Only the LEAF's stapled OCSP response is returned. A server may staple for intermediates too;
|
|
205
|
+
* those staples are validated structurally (they must still be well-formed CertificateStatus)
|
|
206
|
+
* but not consumed — this package checks revocation of the identity it is authenticating, and
|
|
207
|
+
* inventing partial intermediate coverage would imply a guarantee it does not give.
|
|
208
|
+
*
|
|
209
|
+
* @param {Uint8Array} body
|
|
210
|
+
* @param {{ offeredExtensions?: Set<number> }} [opts] extension types our ClientHello offered.
|
|
211
|
+
* Omitting it means "nothing was offered", the fail-closed reading.
|
|
212
|
+
* @returns {{ chain: Uint8Array[], ocspResponse: Uint8Array | null }} DER certificates in wire
|
|
213
|
+
* order (leaf first), plus the leaf's stapled DER OCSPResponse if the server sent one
|
|
214
|
+
*/
|
|
215
|
+
export function parseCertificate13(body: Uint8Array, { offeredExtensions }?: {
|
|
216
|
+
offeredExtensions?: Set<number>;
|
|
217
|
+
}): {
|
|
218
|
+
chain: Uint8Array[];
|
|
219
|
+
ocspResponse: Uint8Array | null;
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* TLS 1.2 Certificate (RFC 5246 s7.4.2): a bare list, no context and no per-cert extensions.
|
|
223
|
+
* @param {Uint8Array} body
|
|
224
|
+
* @returns {Uint8Array[]} DER certificates in wire order, leaf first
|
|
225
|
+
*/
|
|
226
|
+
export function parseCertificate12(body: Uint8Array): Uint8Array[];
|
|
227
|
+
/**
|
|
228
|
+
* @param {Uint8Array} body
|
|
229
|
+
* @returns {{ algorithm: number, signature: Uint8Array }}
|
|
230
|
+
*/
|
|
231
|
+
export function parseCertificateVerify(body: Uint8Array): {
|
|
232
|
+
algorithm: number;
|
|
233
|
+
signature: Uint8Array;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* RFC 8446 s4.4.3: 64 spaces, a context string, a zero byte, then the transcript hash.
|
|
237
|
+
* @param {Uint8Array} transcriptHash
|
|
238
|
+
* @param {boolean} [isServer]
|
|
239
|
+
* @returns {Uint8Array}
|
|
240
|
+
*/
|
|
241
|
+
export function certificateVerifyContent(transcriptHash: Uint8Array, isServer?: boolean): Uint8Array;
|
|
242
|
+
/**
|
|
243
|
+
* Verify a handshake signature with WebCrypto.
|
|
244
|
+
* `spki` is the DER SubjectPublicKeyInfo lifted straight out of the leaf certificate, so the key
|
|
245
|
+
* used to check the signature is provably the key the trust layer validated.
|
|
246
|
+
*
|
|
247
|
+
* @param {object} args
|
|
248
|
+
* @param {number} args.scheme signature scheme id from the wire
|
|
249
|
+
* @param {Uint8Array} args.spki DER SubjectPublicKeyInfo of the validated leaf
|
|
250
|
+
* @param {Uint8Array} args.signature as received: DER for ECDSA, raw otherwise
|
|
251
|
+
* @param {Uint8Array} args.content the exact bytes the signature must cover
|
|
252
|
+
* @returns {Promise<true>} every failure throws; there is no false
|
|
253
|
+
*/
|
|
254
|
+
export function verifyHandshakeSignature({ scheme, spki, signature, content }: {
|
|
255
|
+
scheme: number;
|
|
256
|
+
spki: Uint8Array;
|
|
257
|
+
signature: Uint8Array;
|
|
258
|
+
content: Uint8Array;
|
|
259
|
+
}): Promise<true>;
|
|
260
|
+
/**
|
|
261
|
+
* RFC 8422 s5.4. Only named-curve ECDHE is accepted: explicit curves are a decade-dead feature
|
|
262
|
+
* and finite-field DHE would need bignum arithmetic WebCrypto does not expose.
|
|
263
|
+
*
|
|
264
|
+
* @param {Uint8Array} body
|
|
265
|
+
* @returns {{ group: number, publicKey: Uint8Array, signatureAlgorithm: number,
|
|
266
|
+
* signature: Uint8Array, signedParams: Uint8Array }} `signedParams` is the exact byte range
|
|
267
|
+
* the server's signature covers (curve_type through the public key)
|
|
268
|
+
*/
|
|
269
|
+
export function parseServerKeyExchangeEcdhe(body: Uint8Array): {
|
|
270
|
+
group: number;
|
|
271
|
+
publicKey: Uint8Array;
|
|
272
|
+
signatureAlgorithm: number;
|
|
273
|
+
signature: Uint8Array;
|
|
274
|
+
signedParams: Uint8Array;
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* TLS 1.2 signs client_random || server_random || ServerECDHParams.
|
|
278
|
+
* @param {Uint8Array} clientRandom
|
|
279
|
+
* @param {Uint8Array} serverRandom
|
|
280
|
+
* @param {Uint8Array} signedParams
|
|
281
|
+
* @returns {Uint8Array}
|
|
282
|
+
*/
|
|
283
|
+
export function serverKeyExchangeContent(clientRandom: Uint8Array, serverRandom: Uint8Array, signedParams: Uint8Array): Uint8Array;
|
|
284
|
+
/**
|
|
285
|
+
* @param {Uint8Array} publicKey
|
|
286
|
+
* @returns {Uint8Array} framed ClientKeyExchange message
|
|
287
|
+
*/
|
|
288
|
+
export function buildClientKeyExchange(publicKey: Uint8Array): Uint8Array;
|
|
289
|
+
/**
|
|
290
|
+
* @param {Uint8Array} verifyData
|
|
291
|
+
* @returns {Uint8Array} framed Finished message
|
|
292
|
+
*/
|
|
293
|
+
export function buildFinished(verifyData: Uint8Array): Uint8Array;
|
|
294
|
+
/**
|
|
295
|
+
* Compare a peer Finished against ours. Constant-time in intent: verify_data is derived from
|
|
296
|
+
* secrets the peer must already know, so a timing leak is not a decryption oracle, but there is
|
|
297
|
+
* no reason to leak the prefix length either.
|
|
298
|
+
* @param {Uint8Array} received
|
|
299
|
+
* @param {Uint8Array} expected
|
|
300
|
+
* @returns {true} a mismatch throws; there is no false
|
|
301
|
+
*/
|
|
302
|
+
export function checkFinished(received: Uint8Array, expected: Uint8Array): true;
|
|
303
|
+
/**
|
|
304
|
+
* The negotiated ALPN protocol, refusing anything we did not offer.
|
|
305
|
+
* @param {Map<number, Uint8Array>} extensions
|
|
306
|
+
* @param {string[]} offeredAlpn
|
|
307
|
+
* @param {string} where
|
|
308
|
+
* @returns {string | null} null when the server declined ALPN entirely
|
|
309
|
+
*/
|
|
310
|
+
export function checkAlpn(extensions: Map<number, Uint8Array>, offeredAlpn: string[], where: string): string | null;
|
|
311
|
+
export { GROUP_PARAMS };
|
|
312
|
+
/**
|
|
313
|
+
* An ephemeral key share: the public half as sent in key_share, plus the private key the
|
|
314
|
+
* eventual ServerHello selection will feed into deriveSharedSecret.
|
|
315
|
+
*/
|
|
316
|
+
export type KeyShare = {
|
|
317
|
+
group: number;
|
|
318
|
+
/**
|
|
319
|
+
* raw public key, the exact bytes on the wire
|
|
320
|
+
*/
|
|
321
|
+
keyExchange: Uint8Array;
|
|
322
|
+
/**
|
|
323
|
+
* non-extractable
|
|
324
|
+
*/
|
|
325
|
+
privateKey: CryptoKey;
|
|
326
|
+
};
|
|
327
|
+
export type ClientHelloOptions = {
|
|
328
|
+
/**
|
|
329
|
+
* SNI, unless it is an IP literal (then no SNI is sent)
|
|
330
|
+
*/
|
|
331
|
+
hostname: string;
|
|
332
|
+
/**
|
|
333
|
+
* public halves to
|
|
334
|
+
* offer; empty for a 1.2-only hello, whose wire form must not carry the extension at all
|
|
335
|
+
*/
|
|
336
|
+
keyShares: Array<{
|
|
337
|
+
group: number;
|
|
338
|
+
keyExchange: Uint8Array;
|
|
339
|
+
}>;
|
|
340
|
+
/**
|
|
341
|
+
* fixed ClientHello.random, for reproducible handshakes
|
|
342
|
+
*/
|
|
343
|
+
random?: Uint8Array<ArrayBufferLike> | undefined;
|
|
344
|
+
/**
|
|
345
|
+
* fixed legacy_session_id, likewise
|
|
346
|
+
*/
|
|
347
|
+
legacySessionId?: Uint8Array<ArrayBufferLike> | undefined;
|
|
348
|
+
/**
|
|
349
|
+
* default: the union for the offered versions, 1.3 first
|
|
350
|
+
*/
|
|
351
|
+
ciphers?: number[] | undefined;
|
|
352
|
+
/**
|
|
353
|
+
* supported_groups, default SUPPORTED_GROUPS
|
|
354
|
+
*/
|
|
355
|
+
groups?: number[] | undefined;
|
|
356
|
+
/**
|
|
357
|
+
* default SUPPORTED_SIG_SCHEMES
|
|
358
|
+
*/
|
|
359
|
+
sigSchemes?: number[] | undefined;
|
|
360
|
+
/**
|
|
361
|
+
* default ['http/1.1']; empty array omits the extension
|
|
362
|
+
*/
|
|
363
|
+
alpn?: string[] | undefined;
|
|
364
|
+
/**
|
|
365
|
+
* default [TLS13, TLS12]
|
|
366
|
+
*/
|
|
367
|
+
versions?: number[] | undefined;
|
|
368
|
+
/**
|
|
369
|
+
* pre-encoded, sent verbatim (the HRR cookie)
|
|
370
|
+
*/
|
|
371
|
+
extraExtensions?: Uint8Array<ArrayBufferLike>[] | undefined;
|
|
372
|
+
/**
|
|
373
|
+
* offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
|
|
374
|
+
* the real binder over `message.subarray(0, truncatedLength)` and patch it in at
|
|
375
|
+
* `binderOffset` before the hello touches the wire — a zero binder on the wire is a hello
|
|
376
|
+
* every honest server must reject.
|
|
377
|
+
*/
|
|
378
|
+
psk?: {
|
|
379
|
+
identity: Uint8Array;
|
|
380
|
+
obfuscatedTicketAge: number;
|
|
381
|
+
binderLen: number;
|
|
382
|
+
} | undefined;
|
|
383
|
+
/**
|
|
384
|
+
* injectable randomness
|
|
385
|
+
*/
|
|
386
|
+
randomBytes?: ((n: number) => Uint8Array) | undefined;
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* The built hello plus everything later steps need to police the server's answer against what
|
|
390
|
+
* was actually offered — negotiation checks must run against this record, never against the
|
|
391
|
+
* defaults they might have come from.
|
|
392
|
+
*/
|
|
393
|
+
export type ClientHello = {
|
|
394
|
+
/**
|
|
395
|
+
* framed handshake message, ready for the record layer
|
|
396
|
+
*/
|
|
397
|
+
message: Uint8Array;
|
|
398
|
+
clientRandom: Uint8Array;
|
|
399
|
+
legacySessionId: Uint8Array;
|
|
400
|
+
offeredCiphers: number[];
|
|
401
|
+
offeredGroups: number[];
|
|
402
|
+
offeredSigSchemes: number[];
|
|
403
|
+
/**
|
|
404
|
+
* extension types present in the hello
|
|
405
|
+
*/
|
|
406
|
+
offeredExtensions: Set<number>;
|
|
407
|
+
offeredAlpn: string[];
|
|
408
|
+
/**
|
|
409
|
+
* psk only: where the binder's bytes sit in `message`
|
|
410
|
+
*/
|
|
411
|
+
binderOffset?: number | undefined;
|
|
412
|
+
/**
|
|
413
|
+
* psk only: how many leading bytes of `message` the binder
|
|
414
|
+
* transcript covers (RFC 8446 s4.2.11.2 truncation — everything except the binders list)
|
|
415
|
+
*/
|
|
416
|
+
truncatedLength?: number | undefined;
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* A parsed ServerHello. `isHelloRetryRequest` is decided by the random alone (RFC 8446 s4.1.3);
|
|
420
|
+
* everything else is exactly what the wire carried, judged later by the negotiate* functions.
|
|
421
|
+
*/
|
|
422
|
+
export type ServerHello = {
|
|
423
|
+
legacyVersion: number;
|
|
424
|
+
random: Uint8Array;
|
|
425
|
+
legacySessionIdEcho: Uint8Array;
|
|
426
|
+
cipherSuite: number;
|
|
427
|
+
extensions: Map<number, Uint8Array>;
|
|
428
|
+
isHelloRetryRequest: boolean;
|
|
429
|
+
};
|
|
430
|
+
import { GROUP_PARAMS } from './constants.js';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a TLS 1.3 handshake over a byte duplex and return the plaintext duplex above it.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} args
|
|
5
|
+
* @param {import('./connect.js').ByteDuplex} args.transport
|
|
6
|
+
* @param {string} args.hostname the identity the certificate must prove, and the SNI sent
|
|
7
|
+
* @param {VerifyPeer} args.verifyPeer
|
|
8
|
+
* Must throw to reject. Resolves with the validated leaf; its SPKI is the only key this
|
|
9
|
+
* handshake will accept a CertificateVerify signature from.
|
|
10
|
+
* @param {import('./connect.js').TlsOptions} [args.options] `versions` is ignored: this entry
|
|
11
|
+
* pins the offer to [TLS 1.3]
|
|
12
|
+
* @param {import('./connect.js').TlsDeps} [args.deps]
|
|
13
|
+
* @returns {Promise<import('./connect.js').TlsSession>}
|
|
14
|
+
*/
|
|
15
|
+
export function handshakeTls13({ transport, hostname, verifyPeer, options, deps }: {
|
|
16
|
+
transport: import("./connect.js").ByteDuplex;
|
|
17
|
+
hostname: string;
|
|
18
|
+
verifyPeer: VerifyPeer;
|
|
19
|
+
options?: import("./connect.js").TlsOptions | undefined;
|
|
20
|
+
deps?: import("./connect.js").TlsDeps | undefined;
|
|
21
|
+
}): Promise<import("./connect.js").TlsSession>;
|
|
22
|
+
/**
|
|
23
|
+
* Continue a TLS 1.3 handshake from the first ServerHello (which may be a HelloRetryRequest).
|
|
24
|
+
* Called by connect.js once negotiation routed the connection here.
|
|
25
|
+
* @param {HandshakeContext} ctx
|
|
26
|
+
* @returns {Promise<import('./connect.js').TlsSession>}
|
|
27
|
+
*/
|
|
28
|
+
export function continueTls13(ctx: HandshakeContext): Promise<import("./connect.js").TlsSession>;
|
|
29
|
+
/**
|
|
30
|
+
* Only one key share is offered by default. A second costs a key generation and 30-odd bytes for
|
|
31
|
+
* a group the server is unlikely to prefer; a HelloRetryRequest recovers the rare case at the
|
|
32
|
+
* cost of one round trip.
|
|
33
|
+
*/
|
|
34
|
+
export const DEFAULT_OFFER_GROUPS: number[];
|
|
35
|
+
/**
|
|
36
|
+
* The injected trust decision. Must throw to reject; resolves with the validated leaf, whose
|
|
37
|
+
* SPKI is the only key a driver will accept a handshake signature from. `details.ocspResponse`
|
|
38
|
+
* is the peer's stapled DER OCSPResponse when it sent one — delivered here, at the same moment
|
|
39
|
+
* as the chain, because revocation is part of deciding whether to believe the certificate and
|
|
40
|
+
* must be settled before anything of ours goes on the wire.
|
|
41
|
+
*/
|
|
42
|
+
export type VerifyPeer = (chain: Uint8Array[], hostname: string, details?: {
|
|
43
|
+
ocspResponse: Uint8Array | null;
|
|
44
|
+
}) => Promise<{
|
|
45
|
+
spki: {
|
|
46
|
+
spkiDer: Uint8Array;
|
|
47
|
+
};
|
|
48
|
+
}>;
|
|
49
|
+
/**
|
|
50
|
+
* Driver context assembled by connect.js after the ServerHello routed the connection: the
|
|
51
|
+
* record layer, the transcript (created under the negotiated suite's hash, ClientHello already
|
|
52
|
+
* folded in), the ClientHello metadata, the parsed ServerHello with its raw bytes, and the
|
|
53
|
+
* offer that produced them. Both continue* drivers consume exactly this shape.
|
|
54
|
+
*/
|
|
55
|
+
export type HandshakeContext = {
|
|
56
|
+
record: import("./record.js").RecordLayer;
|
|
57
|
+
transcript: import("./transcript.js").Transcript;
|
|
58
|
+
hello: import("./handshake-messages.js").ClientHello;
|
|
59
|
+
serverHello: import("./handshake-messages.js").ServerHello;
|
|
60
|
+
rawServerHello: Uint8Array;
|
|
61
|
+
suite: number;
|
|
62
|
+
params: import("./constants.js").CipherParams;
|
|
63
|
+
hostname: string;
|
|
64
|
+
verifyPeer: VerifyPeer;
|
|
65
|
+
options: import("./connect.js").TlsOptions;
|
|
66
|
+
deps: import("./connect.js").TlsDeps;
|
|
67
|
+
offer: {
|
|
68
|
+
versions: number[];
|
|
69
|
+
ciphers: number[];
|
|
70
|
+
groups: number[];
|
|
71
|
+
offerGroups: number[];
|
|
72
|
+
alpn: string[];
|
|
73
|
+
keyShares: import("./handshake-messages.js").KeyShare[];
|
|
74
|
+
psk: OfferedPsk | null;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The resumption PSK as prepared by connect.js: the caller's offer plus the secrets derived
|
|
79
|
+
* from it once (Early Secret, binder key) so neither hello build nor acceptance re-derives.
|
|
80
|
+
*/
|
|
81
|
+
export type OfferedPsk = {
|
|
82
|
+
identity: Uint8Array;
|
|
83
|
+
psk: Uint8Array;
|
|
84
|
+
hash: import("./keyschedule.js").ScheduleHash;
|
|
85
|
+
obfuscatedTicketAge: () => number;
|
|
86
|
+
peer: object | null;
|
|
87
|
+
earlySecret: Uint8Array;
|
|
88
|
+
binderKey: Uint8Array;
|
|
89
|
+
binderLen: number;
|
|
90
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one refusal that must fire regardless of which message was expected. Exported for
|
|
3
|
+
* connect.js, whose ServerHello wait is the earliest point a 1.2 server can spring one.
|
|
4
|
+
* @returns {never}
|
|
5
|
+
*/
|
|
6
|
+
export function refuseHelloRequest(): never;
|
|
7
|
+
/**
|
|
8
|
+
* Run a TLS 1.2 handshake over a byte duplex and return the plaintext duplex above it.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} args
|
|
11
|
+
* @param {import('./connect.js').ByteDuplex} args.transport
|
|
12
|
+
* @param {string} args.hostname the identity the certificate must prove, and the SNI sent
|
|
13
|
+
* @param {import('./handshake.js').VerifyPeer} args.verifyPeer
|
|
14
|
+
* Must throw to reject. Resolves with the validated leaf; its SPKI is the only key this
|
|
15
|
+
* handshake will accept a ServerKeyExchange signature from.
|
|
16
|
+
* @param {import('./connect.js').TlsOptions} [args.options] `versions` is ignored: this entry
|
|
17
|
+
* pins the offer to [TLS 1.2]
|
|
18
|
+
* @param {import('./connect.js').TlsDeps} [args.deps]
|
|
19
|
+
* @returns {Promise<import('./connect.js').TlsSession>}
|
|
20
|
+
*/
|
|
21
|
+
export function handshakeTls12({ transport, hostname, verifyPeer, options, deps }: {
|
|
22
|
+
transport: import("./connect.js").ByteDuplex;
|
|
23
|
+
hostname: string;
|
|
24
|
+
verifyPeer: import("./handshake.js").VerifyPeer;
|
|
25
|
+
options?: import("./connect.js").TlsOptions | undefined;
|
|
26
|
+
deps?: import("./connect.js").TlsDeps | undefined;
|
|
27
|
+
}): Promise<import("./connect.js").TlsSession>;
|
|
28
|
+
/**
|
|
29
|
+
* Continue a TLS 1.2 handshake from the ServerHello. Called by connect.js once negotiation
|
|
30
|
+
* routed the connection here; the record layer arrives already pinned to 1.2 semantics and the
|
|
31
|
+
* transcript already runs under the negotiated suite's PRF hash.
|
|
32
|
+
* @param {import('./handshake.js').HandshakeContext} ctx
|
|
33
|
+
* @returns {Promise<import('./connect.js').TlsSession>}
|
|
34
|
+
*/
|
|
35
|
+
export function continueTls12(ctx: import("./handshake.js").HandshakeContext): Promise<import("./connect.js").TlsSession>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { connectTls } from "./connect.js";
|
|
2
|
+
export { handshakeTls13 } from "./handshake.js";
|
|
3
|
+
export { handshakeTls12 } from "./handshake12.js";
|
|
4
|
+
export { RecordLayer } from "./record.js";
|
|
5
|
+
export { Transcript } from "./transcript.js";
|
|
6
|
+
export { TicketStore } from "./tickets.js";
|
|
7
|
+
export { createAead, buildNonce } from "./aead.js";
|
|
8
|
+
export { ALPN_HTTP11, CIPHER, CIPHER_NAME, CIPHER_PARAMS, GROUP, GROUP_NAME, SIG_SCHEME, SIG_SCHEME_NAME, SUPPORTED_GROUPS, SUPPORTED_SIG_SCHEMES, TLS12, TLS12_CIPHERS, TLS13, TLS13_CIPHERS, VERSION_NAME } from "./constants.js";
|
|
9
|
+
export { buildClientHello, deriveSharedSecret, generateKeyShare, negotiateCipher, negotiateVersion, parseServerHello, verifyHandshakeSignature } from "./handshake-messages.js";
|