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,145 @@
|
|
|
1
|
+
// The resumption ticket store: per-Client, keyed like the connection pool, single-use.
|
|
2
|
+
//
|
|
3
|
+
// A ticket is a credential. Presenting one tells the server "treat this connection as a
|
|
4
|
+
// continuation of the session you issued it on", and the server will — without sending a
|
|
5
|
+
// certificate, without this client re-validating anything. So the one property this store must
|
|
6
|
+
// never lose is that a ticket can only ever be OFFERED under the exact configuration it was
|
|
7
|
+
// OBTAINED under. The pool solved the identical problem for sockets: poolKey() folds scheme,
|
|
8
|
+
// host, port, proxy, trust policy and TLS options into the key precisely so a connection
|
|
9
|
+
// validated under one policy cannot serve a request that asked for another. Tickets are keyed
|
|
10
|
+
// by the caller with that same key, for that same reason. Keying by hostname alone is the
|
|
11
|
+
// canonical mistake here: it lets a request that asked for certificate pinning resume — and
|
|
12
|
+
// silently skip the pin check — on the strength of a session that was never pinned at all.
|
|
13
|
+
//
|
|
14
|
+
// Three smaller policies, each deliberate:
|
|
15
|
+
//
|
|
16
|
+
// * Single use. A ticket is consumed by take() whether or not the resumption succeeds.
|
|
17
|
+
// RFC 8446 appendix C.4 wants tickets used at most once (reuse lets a passive observer
|
|
18
|
+
// correlate connections), servers commonly enforce it, and a second offer of a spent ticket
|
|
19
|
+
// would burn a round trip to be declined. Servers that want chains of resumption issue a
|
|
20
|
+
// fresh ticket on every connection, including resumed ones.
|
|
21
|
+
// * Lifetimes are honoured on both ends: a ticket_lifetime of zero is discarded on arrival
|
|
22
|
+
// (s4.6.1 says exactly that), everything is capped at the protocol's seven-day ceiling
|
|
23
|
+
// (clients MUST NOT cache longer, whatever the server claimed), and take() re-checks age at
|
|
24
|
+
// the moment of use, so a ticket that expired while stored is dropped, not offered.
|
|
25
|
+
// * The clock is injected. Ages are wall-clock quantities, and on the target runtime the
|
|
26
|
+
// clock is frozen within a synchronous slice — tests need to move time, and nothing else in
|
|
27
|
+
// the TLS layer reads a clock at all.
|
|
28
|
+
|
|
29
|
+
import { TlsError, codes } from '../errors.js';
|
|
30
|
+
import { hashLength } from './keyschedule.js';
|
|
31
|
+
|
|
32
|
+
/** RFC 8446 s4.6.1: servers MUST NOT exceed this, and clients MUST NOT cache beyond it. */
|
|
33
|
+
const MAX_LIFETIME_SEC = 604800; // seven days
|
|
34
|
+
|
|
35
|
+
export class TicketStore {
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} [opts]
|
|
38
|
+
* @param {number} [opts.maxPerKey] tickets retained per key, default 2 — the number a typical
|
|
39
|
+
* server flight issues; older tickets are evicted first
|
|
40
|
+
* @param {() => number} [opts.now] epoch-ms source, injectable for tests
|
|
41
|
+
*/
|
|
42
|
+
constructor({ maxPerKey = 2, now = () => Date.now() } = {}) {
|
|
43
|
+
/** @type {Map<string, Array<StoredTicket>>} */
|
|
44
|
+
this._byKey = new Map();
|
|
45
|
+
this._maxPerKey = maxPerKey;
|
|
46
|
+
this._now = now;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {object} StoredTicket
|
|
51
|
+
* @property {Uint8Array} identity
|
|
52
|
+
* @property {Uint8Array} psk
|
|
53
|
+
* @property {import('./keyschedule.js').ScheduleHash} hash
|
|
54
|
+
* @property {number} ageAdd
|
|
55
|
+
* @property {number} lifetimeMs already clamped to the seven-day ceiling
|
|
56
|
+
* @property {number} receivedAtMs
|
|
57
|
+
* @property {object} peer
|
|
58
|
+
* @property {number} cipherSuite
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/** Total tickets currently held, for tests and diagnostics. */
|
|
62
|
+
get size() {
|
|
63
|
+
let n = 0;
|
|
64
|
+
for (const list of this._byKey.values()) n += list.length;
|
|
65
|
+
return n;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Store a captured ticket under `key`. Returns whether it was retained: a zero (or negative)
|
|
70
|
+
* lifetime is a server instruction to discard immediately and is honoured silently — that is
|
|
71
|
+
* the server's prerogative, not an error — while a structurally unusable capture (no PSK, no
|
|
72
|
+
* identity, unknown hash) throws, because the only producer is this package's own driver and
|
|
73
|
+
* a malformed capture is a bug, not an input.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} key the pool key of the connection the ticket arrived on
|
|
76
|
+
* @param {import('./connect.js').CapturedTicket} captured
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
put(key, captured) {
|
|
80
|
+
const { identity, psk, hash, lifetimeSec, ageAdd } = captured;
|
|
81
|
+
hashLength(hash); // throws on anything but SHA-256/SHA-384
|
|
82
|
+
if (!(identity instanceof Uint8Array) || identity.byteLength === 0 ||
|
|
83
|
+
!(psk instanceof Uint8Array) || psk.byteLength === 0) {
|
|
84
|
+
throw new TlsError(codes.TLS_TICKET,
|
|
85
|
+
'a captured ticket needs a non-empty identity and PSK; refusing to store a blank credential');
|
|
86
|
+
}
|
|
87
|
+
if (!Number.isInteger(lifetimeSec) || !Number.isInteger(ageAdd)) {
|
|
88
|
+
throw new TlsError(codes.TLS_TICKET,
|
|
89
|
+
`ticket lifetime ${lifetimeSec} / age_add ${ageAdd} are not integers`,
|
|
90
|
+
{ lifetimeSec, ageAdd });
|
|
91
|
+
}
|
|
92
|
+
if (lifetimeSec <= 0) return false; // s4.6.1: zero means discard immediately
|
|
93
|
+
const lifetimeMs = Math.min(lifetimeSec, MAX_LIFETIME_SEC) * 1000;
|
|
94
|
+
const list = this._byKey.get(key) ?? [];
|
|
95
|
+
list.push({
|
|
96
|
+
identity, psk, hash, ageAdd, lifetimeMs,
|
|
97
|
+
receivedAtMs: this._now(),
|
|
98
|
+
peer: captured.peer ?? null,
|
|
99
|
+
cipherSuite: captured.cipherSuite,
|
|
100
|
+
});
|
|
101
|
+
while (list.length > this._maxPerKey) list.shift();
|
|
102
|
+
this._byKey.set(key, list);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Take the freshest usable ticket for `key` as a ready-to-offer PSK, or null. The ticket is
|
|
108
|
+
* removed either way it goes from here — single use — and expired tickets encountered on the
|
|
109
|
+
* way are dropped rather than offered: a server checks obfuscated_ticket_age against the
|
|
110
|
+
* lifetime it granted, and offering a stale ticket is a round trip spent being refused.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} key MUST be built from the same inputs as the connection's pool key; this
|
|
113
|
+
* equality is the entire trust story of resumption (see the module comment)
|
|
114
|
+
* @returns {import('./connect.js').ResumptionOffer | null}
|
|
115
|
+
*/
|
|
116
|
+
take(key) {
|
|
117
|
+
const list = this._byKey.get(key);
|
|
118
|
+
if (!list) return null;
|
|
119
|
+
while (list.length > 0) {
|
|
120
|
+
const t = /** @type {StoredTicket} */ (list.pop());
|
|
121
|
+
const age = this._now() - t.receivedAtMs;
|
|
122
|
+
if (age >= t.lifetimeMs) continue; // expired in storage: drop it, try the next-newest
|
|
123
|
+
if (list.length === 0) this._byKey.delete(key);
|
|
124
|
+
return {
|
|
125
|
+
identity: t.identity,
|
|
126
|
+
psk: t.psk,
|
|
127
|
+
hash: t.hash,
|
|
128
|
+
peer: t.peer,
|
|
129
|
+
// s4.2.11.1: obfuscated_ticket_age = age-in-ms + ticket_age_add, mod 2^32, current at
|
|
130
|
+
// the moment each hello is built (a HelloRetryRequest builds a second one later).
|
|
131
|
+
obfuscatedTicketAge: () => {
|
|
132
|
+
const ms = Math.max(0, this._now() - t.receivedAtMs);
|
|
133
|
+
return (ms + t.ageAdd) % 0x100000000;
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
this._byKey.delete(key);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Drop everything; a closed Client must not keep credentials alive. */
|
|
142
|
+
clear() {
|
|
143
|
+
this._byKey.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Running handshake transcript hash (RFC 8446 s4.4.1).
|
|
2
|
+
//
|
|
3
|
+
// WebCrypto has no streaming digest, so the raw handshake messages are buffered and re-digested
|
|
4
|
+
// on every hash() call. That tradeoff is deliberate: the transcript covers handshake messages
|
|
5
|
+
// only, which a real handshake bounds at a few hundred KB of certificates, and hash() is called
|
|
6
|
+
// a handful of times per connection — buffering costs one transcript-sized allocation, while a
|
|
7
|
+
// hand-rolled incremental SHA-2 in JS would be a fresh piece of security-critical code to get
|
|
8
|
+
// wrong. The buffer is still capped so a peer streaming an absurd certificate chain cannot make
|
|
9
|
+
// us hold unbounded memory; the record layer's per-message cap fires first in practice.
|
|
10
|
+
|
|
11
|
+
import { TlsError, codes } from '../errors.js';
|
|
12
|
+
import { concat, u8, u24 } from '../util/bytes.js';
|
|
13
|
+
import { HANDSHAKE_TYPE } from './constants.js';
|
|
14
|
+
import { hashLength } from './keyschedule.js';
|
|
15
|
+
|
|
16
|
+
export class Transcript {
|
|
17
|
+
/**
|
|
18
|
+
* @param {import('./keyschedule.js').ScheduleHash} hash fixed once the cipher suite is known
|
|
19
|
+
* @param {{ maxBytes?: number }} [opts] transcript buffer cap, default 1 MiB
|
|
20
|
+
*/
|
|
21
|
+
constructor(hash, { maxBytes = 1 << 20 } = {}) {
|
|
22
|
+
hashLength(hash); // validate eagerly: a typo'd hash name must not surface at first hash()
|
|
23
|
+
this._hash = hash;
|
|
24
|
+
/** @type {Uint8Array[]} */
|
|
25
|
+
this._chunks = [];
|
|
26
|
+
this._len = 0;
|
|
27
|
+
this._maxBytes = maxBytes;
|
|
28
|
+
/** @type {Uint8Array | null} digest cache, invalidated by update() */
|
|
29
|
+
this._cached = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get bytesBuffered() {
|
|
33
|
+
return this._len;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Append raw handshake message bytes (including the 4-byte message header — the transcript
|
|
38
|
+
* is over complete Handshake structs, never record framing).
|
|
39
|
+
* @param {Uint8Array} bytes
|
|
40
|
+
*/
|
|
41
|
+
update(bytes) {
|
|
42
|
+
if (bytes.byteLength === 0) return;
|
|
43
|
+
if (this._len + bytes.byteLength > this._maxBytes) {
|
|
44
|
+
throw new TlsError(codes.TLS_HANDSHAKE,
|
|
45
|
+
`handshake transcript would exceed ${this._maxBytes} bytes`,
|
|
46
|
+
{ buffered: this._len, adding: bytes.byteLength, limit: this._maxBytes });
|
|
47
|
+
}
|
|
48
|
+
// Copy: callers hand us subarrays aliasing record buffers they will reuse.
|
|
49
|
+
this._chunks.push(bytes.slice());
|
|
50
|
+
this._len += bytes.byteLength;
|
|
51
|
+
this._cached = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Digest of everything appended so far. Does not consume; call as often as needed.
|
|
56
|
+
* @returns {Promise<Uint8Array>}
|
|
57
|
+
*/
|
|
58
|
+
async hash() {
|
|
59
|
+
if (!this._cached) {
|
|
60
|
+
this._cached = new Uint8Array(
|
|
61
|
+
await crypto.subtle.digest(this._hash, concat(this._chunks, this._len)),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return this._cached;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Digest of everything appended so far PLUS `extra`, without appending it.
|
|
69
|
+
*
|
|
70
|
+
* Exists for exactly one caller: the PSK binder after a HelloRetryRequest, which is an HMAC
|
|
71
|
+
* over Transcript-Hash(message_hash(CH1) || HRR || Truncate(CH2)) (RFC 8446 s4.2.11.2). The
|
|
72
|
+
* truncated ClientHello2 must be hashed as a continuation of the real transcript but must
|
|
73
|
+
* never BECOME part of it — the transcript proper gets the full ClientHello2 with its binder,
|
|
74
|
+
* and folding the truncated form in even transiently would leave a window where the two
|
|
75
|
+
* bookkeepings disagree.
|
|
76
|
+
* @param {Uint8Array} extra
|
|
77
|
+
* @returns {Promise<Uint8Array>}
|
|
78
|
+
*/
|
|
79
|
+
async hashWith(extra) {
|
|
80
|
+
return new Uint8Array(
|
|
81
|
+
await crypto.subtle.digest(this._hash, concat([...this._chunks, extra], this._len + extra.byteLength)),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* HelloRetryRequest transcript substitution (RFC 8446 s4.4.1): when a ServerHello is an HRR,
|
|
87
|
+
* the transcript restarts as a synthetic handshake message
|
|
88
|
+
*
|
|
89
|
+
* message_hash(0xFE) || uint24 Hash.length || Hash(ClientHello1)
|
|
90
|
+
*
|
|
91
|
+
* so that a stateless server need only remember the hash of the first ClientHello. Call this
|
|
92
|
+
* after ClientHello1 is the only message in the transcript, before appending the HRR itself.
|
|
93
|
+
* @returns {Promise<void>}
|
|
94
|
+
*/
|
|
95
|
+
async replaceWithMessageHash() {
|
|
96
|
+
const digest = await this.hash();
|
|
97
|
+
this._chunks = [concat([u8(HANDSHAKE_TYPE.message_hash), u24(digest.byteLength), digest])];
|
|
98
|
+
this._len = this._chunks[0].byteLength;
|
|
99
|
+
this._cached = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/tls/wire.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// TLS wire primitives: length-prefixed vectors, in both directions.
|
|
2
|
+
//
|
|
3
|
+
// TLS is almost entirely nested length-prefixed vectors, and nearly every historical parsing CVE
|
|
4
|
+
// in TLS stacks comes from trusting one of those lengths. So the reader here has exactly one
|
|
5
|
+
// policy: a length that does not fit inside its parent is an error, and a structure with bytes
|
|
6
|
+
// left over when it should be exhausted is an error. Never "read what you can".
|
|
7
|
+
|
|
8
|
+
import { TlsError, codes } from '../errors.js';
|
|
9
|
+
import { concat, u8, u16, u24 } from '../util/bytes.js';
|
|
10
|
+
|
|
11
|
+
/** Sequential reader over a byte range with hard bounds. */
|
|
12
|
+
export class Cursor {
|
|
13
|
+
/**
|
|
14
|
+
* @param {Uint8Array} bytes
|
|
15
|
+
* @param {string} what named in every error so a failure says which structure was malformed
|
|
16
|
+
*/
|
|
17
|
+
constructor(bytes, what = 'structure') {
|
|
18
|
+
this.bytes = bytes;
|
|
19
|
+
this.pos = 0;
|
|
20
|
+
this.what = what;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get remaining() {
|
|
24
|
+
return this.bytes.byteLength - this.pos;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get done() {
|
|
28
|
+
return this.pos >= this.bytes.byteLength;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {number} n
|
|
33
|
+
* @param {string} field
|
|
34
|
+
*/
|
|
35
|
+
_need(n, field) {
|
|
36
|
+
if (n < 0 || this.remaining < n) {
|
|
37
|
+
throw new TlsError(
|
|
38
|
+
codes.TLS_HANDSHAKE,
|
|
39
|
+
`${this.what}: needed ${n} bytes for ${field} but only ${this.remaining} remain ` +
|
|
40
|
+
`at offset ${this.pos} of ${this.bytes.byteLength}`,
|
|
41
|
+
{ what: this.what, field, needed: n, remaining: this.remaining, offset: this.pos },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} [field]
|
|
48
|
+
* @returns {number}
|
|
49
|
+
*/
|
|
50
|
+
u8(field = 'uint8') {
|
|
51
|
+
this._need(1, field);
|
|
52
|
+
return this.bytes[this.pos++];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} [field]
|
|
57
|
+
* @returns {number}
|
|
58
|
+
*/
|
|
59
|
+
u16(field = 'uint16') {
|
|
60
|
+
this._need(2, field);
|
|
61
|
+
const v = (this.bytes[this.pos] << 8) | this.bytes[this.pos + 1];
|
|
62
|
+
this.pos += 2;
|
|
63
|
+
return v;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} [field]
|
|
68
|
+
* @returns {number}
|
|
69
|
+
*/
|
|
70
|
+
u24(field = 'uint24') {
|
|
71
|
+
this._need(3, field);
|
|
72
|
+
const v =
|
|
73
|
+
(this.bytes[this.pos] << 16) | (this.bytes[this.pos + 1] << 8) | this.bytes[this.pos + 2];
|
|
74
|
+
this.pos += 3;
|
|
75
|
+
return v;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {string} [field]
|
|
80
|
+
* @returns {number}
|
|
81
|
+
*/
|
|
82
|
+
u32(field = 'uint32') {
|
|
83
|
+
this._need(4, field);
|
|
84
|
+
const b = this.bytes;
|
|
85
|
+
const v =
|
|
86
|
+
((b[this.pos] << 24) | (b[this.pos + 1] << 16) | (b[this.pos + 2] << 8) | b[this.pos + 3]) >>>
|
|
87
|
+
0;
|
|
88
|
+
this.pos += 4;
|
|
89
|
+
return v;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fixed-length opaque bytes. Returns a view into the original buffer, never a copy.
|
|
94
|
+
* @param {number} n
|
|
95
|
+
* @param {string} [field]
|
|
96
|
+
* @returns {Uint8Array}
|
|
97
|
+
*/
|
|
98
|
+
take(n, field = 'opaque') {
|
|
99
|
+
this._need(n, field);
|
|
100
|
+
const out = this.bytes.subarray(this.pos, this.pos + n);
|
|
101
|
+
this.pos += n;
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A vector whose length is carried in `lenBytes` (1, 2 or 3) leading octets.
|
|
107
|
+
* @param {1 | 2 | 3} lenBytes
|
|
108
|
+
* @param {string} [field]
|
|
109
|
+
* @returns {Uint8Array}
|
|
110
|
+
*/
|
|
111
|
+
vector(lenBytes, field = 'vector') {
|
|
112
|
+
const n = lenBytes === 1 ? this.u8(`${field} length`)
|
|
113
|
+
: lenBytes === 2 ? this.u16(`${field} length`)
|
|
114
|
+
: this.u24(`${field} length`);
|
|
115
|
+
return this.take(n, field);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Like `vector`, but hands back a Cursor so nested structures inherit the bound.
|
|
120
|
+
* @param {1 | 2 | 3} lenBytes
|
|
121
|
+
* @param {string} [field]
|
|
122
|
+
* @returns {Cursor}
|
|
123
|
+
*/
|
|
124
|
+
sub(lenBytes, field = 'vector') {
|
|
125
|
+
return new Cursor(this.vector(lenBytes, field), `${this.what}.${field}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Assert nothing is left. Trailing data inside a length-delimited structure means our idea of
|
|
130
|
+
* the structure and the peer's disagree, which is exactly when to stop rather than guess.
|
|
131
|
+
* @param {string} [field]
|
|
132
|
+
*/
|
|
133
|
+
end(field = 'structure') {
|
|
134
|
+
if (!this.done) {
|
|
135
|
+
throw new TlsError(
|
|
136
|
+
codes.TLS_HANDSHAKE,
|
|
137
|
+
`${this.what}: ${this.remaining} trailing bytes after ${field}`,
|
|
138
|
+
{ what: this.what, field, trailing: this.remaining },
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Accumulating writer. Kept dumb: correctness of lengths comes from `vector()` below. */
|
|
145
|
+
export class Builder {
|
|
146
|
+
constructor() {
|
|
147
|
+
/** @type {Uint8Array[]} */
|
|
148
|
+
this.parts = [];
|
|
149
|
+
this.length = 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @param {Uint8Array} bytes
|
|
154
|
+
* @returns {this}
|
|
155
|
+
*/
|
|
156
|
+
push(bytes) {
|
|
157
|
+
if (bytes.byteLength) {
|
|
158
|
+
this.parts.push(bytes);
|
|
159
|
+
this.length += bytes.byteLength;
|
|
160
|
+
}
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @param {number} n @returns {this} */
|
|
165
|
+
u8(n) {
|
|
166
|
+
return this.push(u8(n));
|
|
167
|
+
}
|
|
168
|
+
/** @param {number} n @returns {this} */
|
|
169
|
+
u16(n) {
|
|
170
|
+
return this.push(u16(n));
|
|
171
|
+
}
|
|
172
|
+
/** @param {number} n @returns {this} */
|
|
173
|
+
u24(n) {
|
|
174
|
+
return this.push(u24(n));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Write `body` prefixed by its length in `lenBytes` octets. Taking the body as bytes rather
|
|
179
|
+
* than back-patching a placeholder means a length can never drift from what follows it.
|
|
180
|
+
* @param {1 | 2 | 3} lenBytes
|
|
181
|
+
* @param {Uint8Array} body
|
|
182
|
+
* @returns {this}
|
|
183
|
+
*/
|
|
184
|
+
vector(lenBytes, body) {
|
|
185
|
+
const n = body.byteLength;
|
|
186
|
+
const max = lenBytes === 1 ? 0xff : lenBytes === 2 ? 0xffff : 0xffffff;
|
|
187
|
+
if (n > max) {
|
|
188
|
+
throw new TlsError(
|
|
189
|
+
codes.TLS_HANDSHAKE,
|
|
190
|
+
`vector of ${n} bytes does not fit in a ${lenBytes}-byte length prefix (max ${max})`,
|
|
191
|
+
{ length: n, lenBytes, max },
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (lenBytes === 1) this.u8(n);
|
|
195
|
+
else if (lenBytes === 2) this.u16(n);
|
|
196
|
+
else this.u24(n);
|
|
197
|
+
return this.push(body);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** @returns {Uint8Array} */
|
|
201
|
+
build() {
|
|
202
|
+
return concat(this.parts, this.length);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Convenience: build a length-prefixed vector standalone.
|
|
208
|
+
* @param {1 | 2 | 3} lenBytes
|
|
209
|
+
* @param {Uint8Array} body
|
|
210
|
+
* @returns {Uint8Array}
|
|
211
|
+
*/
|
|
212
|
+
export function vector(lenBytes, body) {
|
|
213
|
+
return new Builder().vector(lenBytes, body).build();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Encode a handshake message: 1-byte type, 3-byte length, body.
|
|
218
|
+
* @param {number} type
|
|
219
|
+
* @param {Uint8Array} body
|
|
220
|
+
* @returns {Uint8Array}
|
|
221
|
+
*/
|
|
222
|
+
export function handshakeMessage(type, body) {
|
|
223
|
+
return new Builder().u8(type).vector(3, body).build();
|
|
224
|
+
}
|