@joclaim/tls 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -0
- package/lib/crypto/common.d.ts +3 -0
- package/lib/crypto/common.js +26 -0
- package/lib/crypto/index.d.ts +3 -0
- package/lib/crypto/index.js +4 -0
- package/lib/crypto/insecure-rand.d.ts +1 -0
- package/lib/crypto/insecure-rand.js +9 -0
- package/lib/crypto/pure-js.d.ts +2 -0
- package/lib/crypto/pure-js.js +144 -0
- package/lib/crypto/webcrypto.d.ts +3 -0
- package/lib/crypto/webcrypto.js +310 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/make-tls-client.d.ts +74 -0
- package/lib/make-tls-client.js +657 -0
- package/lib/scripts/build-jsc.d.ts +1 -0
- package/lib/scripts/build-jsc.js +20 -0
- package/lib/scripts/ca-template.d.ts +5 -0
- package/lib/scripts/ca-template.js +6 -0
- package/lib/scripts/fallbacks/crypto.d.ts +4 -0
- package/lib/scripts/fallbacks/crypto.js +2 -0
- package/lib/scripts/handshake.d.ts +1 -0
- package/lib/scripts/handshake.js +61 -0
- package/lib/scripts/jsc.d.ts +28 -0
- package/lib/scripts/jsc.js +92 -0
- package/lib/scripts/update-ca-certs.d.ts +1 -0
- package/lib/scripts/update-ca-certs.js +29 -0
- package/lib/types/crypto.d.ts +62 -0
- package/lib/types/crypto.js +1 -0
- package/lib/types/index.d.ts +15 -0
- package/lib/types/index.js +4 -0
- package/lib/types/logger.d.ts +6 -0
- package/lib/types/logger.js +1 -0
- package/lib/types/tls.d.ts +141 -0
- package/lib/types/tls.js +1 -0
- package/lib/types/x509.d.ts +32 -0
- package/lib/types/x509.js +1 -0
- package/lib/utils/additional-root-cas.d.ts +1 -0
- package/lib/utils/additional-root-cas.js +197 -0
- package/lib/utils/client-hello.d.ts +23 -0
- package/lib/utils/client-hello.js +167 -0
- package/lib/utils/constants.d.ts +239 -0
- package/lib/utils/constants.js +244 -0
- package/lib/utils/decryption-utils.d.ts +64 -0
- package/lib/utils/decryption-utils.js +166 -0
- package/lib/utils/finish-messages.d.ts +11 -0
- package/lib/utils/finish-messages.js +49 -0
- package/lib/utils/generics.d.ts +35 -0
- package/lib/utils/generics.js +146 -0
- package/lib/utils/index.d.ts +18 -0
- package/lib/utils/index.js +18 -0
- package/lib/utils/key-share.d.ts +13 -0
- package/lib/utils/key-share.js +72 -0
- package/lib/utils/key-update.d.ts +2 -0
- package/lib/utils/key-update.js +14 -0
- package/lib/utils/logger.d.ts +2 -0
- package/lib/utils/logger.js +15 -0
- package/lib/utils/make-queue.d.ts +3 -0
- package/lib/utils/make-queue.js +22 -0
- package/lib/utils/mozilla-root-cas.d.ts +5 -0
- package/lib/utils/mozilla-root-cas.js +4459 -0
- package/lib/utils/packets.d.ts +51 -0
- package/lib/utils/packets.js +148 -0
- package/lib/utils/parse-alert.d.ts +7 -0
- package/lib/utils/parse-alert.js +28 -0
- package/lib/utils/parse-certificate.d.ts +29 -0
- package/lib/utils/parse-certificate.js +188 -0
- package/lib/utils/parse-client-hello.d.ts +11 -0
- package/lib/utils/parse-client-hello.js +39 -0
- package/lib/utils/parse-extensions.d.ts +11 -0
- package/lib/utils/parse-extensions.js +74 -0
- package/lib/utils/parse-server-hello.d.ts +10 -0
- package/lib/utils/parse-server-hello.js +52 -0
- package/lib/utils/session-ticket.d.ts +17 -0
- package/lib/utils/session-ticket.js +51 -0
- package/lib/utils/wrapped-record.d.ts +25 -0
- package/lib/utils/wrapped-record.js +191 -0
- package/lib/utils/x509.d.ts +5 -0
- package/lib/utils/x509.js +124 -0
- package/package.json +82 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import { crypto } from "./crypto/index.js";
|
|
2
|
+
import { packClientHello } from "./utils/client-hello.js";
|
|
3
|
+
import { CONTENT_TYPE_MAP, MAX_ENC_PACKET_SIZE, PACKET_TYPE, SUPPORTED_CIPHER_SUITE_MAP, SUPPORTED_NAMED_CURVE_MAP, SUPPORTED_NAMED_CURVES, SUPPORTED_RECORD_TYPE_MAP } from "./utils/constants.js";
|
|
4
|
+
import { computeSharedKeys, computeSharedKeysTls12, computeUpdatedTrafficMasterSecret, deriveTrafficKeysForSide } from "./utils/decryption-utils.js";
|
|
5
|
+
import { generateFinishTls12, packClientFinishTls12, packFinishMessagePacket, verifyFinishMessage } from "./utils/finish-messages.js";
|
|
6
|
+
import { areUint8ArraysEqual, chunkUint8Array, concatenateUint8Arrays, toHexStringWithWhitespace } from "./utils/generics.js";
|
|
7
|
+
import { createRsaPreMasterSecret, packClientCurveKeyShare, packClientRsaKeyShare, processServerKeyShare } from "./utils/key-share.js";
|
|
8
|
+
import { packKeyUpdateRecord } from "./utils/key-update.js";
|
|
9
|
+
import { logger as LOGGER } from "./utils/logger.js";
|
|
10
|
+
import { makeQueue } from "./utils/make-queue.js";
|
|
11
|
+
import { makeMessageProcessor, packPacketHeader, packWithLength, readWithLength } from "./utils/packets.js";
|
|
12
|
+
import { parseTlsAlert } from "./utils/parse-alert.js";
|
|
13
|
+
import { getSignatureDataTls12, getSignatureDataTls13, parseCertificates, parseServerCertificateVerify, verifyCertificateChain, verifyCertificateSignature } from "./utils/parse-certificate.js";
|
|
14
|
+
import { parseServerExtensions } from "./utils/parse-extensions.js";
|
|
15
|
+
import { parseServerHello } from "./utils/parse-server-hello.js";
|
|
16
|
+
import { getPskFromTicket, parseSessionTicket } from "./utils/session-ticket.js";
|
|
17
|
+
import { decryptWrappedRecord, encryptWrappedRecord } from "./utils/wrapped-record.js";
|
|
18
|
+
const RECORD_LENGTH_BYTES = 3;
|
|
19
|
+
export function makeTLSClient({ host, verifyServerCertificate = true, rootCAs, logger: _logger, cipherSuites, namedCurves = SUPPORTED_NAMED_CURVES, supportedProtocolVersions, signatureAlgorithms, applicationLayerProtocols, fetchCertificateBytes, write, onRead, onApplicationData, onSessionTicket, onTlsEnd, onHandshake, onRecvCertificates }) {
|
|
20
|
+
const logger = _logger || LOGGER;
|
|
21
|
+
const processor = makeMessageProcessor(logger);
|
|
22
|
+
const { enqueue: enqueueServerPacket } = makeQueue();
|
|
23
|
+
const keyPairs = {};
|
|
24
|
+
let handshakeDone = false;
|
|
25
|
+
let ended = false;
|
|
26
|
+
let sessionId = new Uint8Array();
|
|
27
|
+
let handshakeMsgs = [];
|
|
28
|
+
let cipherSuite = undefined;
|
|
29
|
+
let earlySecret = undefined;
|
|
30
|
+
let keys = undefined;
|
|
31
|
+
let recordSendCount = 0;
|
|
32
|
+
let recordRecvCount = 0;
|
|
33
|
+
let keyType = undefined;
|
|
34
|
+
let connTlsVersion = undefined;
|
|
35
|
+
let clientRandom = undefined;
|
|
36
|
+
let serverRandom = undefined;
|
|
37
|
+
let cipherSpecChanged = false;
|
|
38
|
+
let selectedAlpn;
|
|
39
|
+
let certificates;
|
|
40
|
+
let handshakePacketStream = new Uint8Array();
|
|
41
|
+
let clientCertificateRequested = false;
|
|
42
|
+
let certificatesVerified = false;
|
|
43
|
+
const processPacketUnsafe = async ({ type, packet: { content, header } }) => {
|
|
44
|
+
if (ended) {
|
|
45
|
+
logger.warn('connection closed, ignoring packet');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let contentType;
|
|
49
|
+
let ctx = { type: 'plaintext' };
|
|
50
|
+
// if the cipher spec has changed,
|
|
51
|
+
// the data will be encrypted, so
|
|
52
|
+
// we need to decrypt the packet
|
|
53
|
+
if (cipherSpecChanged || type === PACKET_TYPE.WRAPPED_RECORD) {
|
|
54
|
+
logger.trace('recv wrapped record');
|
|
55
|
+
const macKey = 'serverMacKey' in keys
|
|
56
|
+
? keys.serverMacKey
|
|
57
|
+
: undefined;
|
|
58
|
+
const decrypted = await decryptWrappedRecord(content, {
|
|
59
|
+
key: keys.serverEncKey,
|
|
60
|
+
iv: keys.serverIv,
|
|
61
|
+
recordHeader: header,
|
|
62
|
+
recordNumber: recordRecvCount,
|
|
63
|
+
cipherSuite: cipherSuite,
|
|
64
|
+
version: connTlsVersion,
|
|
65
|
+
macKey,
|
|
66
|
+
});
|
|
67
|
+
if (connTlsVersion === 'TLS1_3') {
|
|
68
|
+
// TLS 1.3 has an extra byte suffixed
|
|
69
|
+
// this denotes the content type of the
|
|
70
|
+
// packet
|
|
71
|
+
const contentTypeNum = decrypted
|
|
72
|
+
.plaintext[decrypted.plaintext.length - 1];
|
|
73
|
+
contentType = Object.entries(CONTENT_TYPE_MAP)
|
|
74
|
+
.find(([, val]) => val === contentTypeNum)?.[0];
|
|
75
|
+
}
|
|
76
|
+
ctx = {
|
|
77
|
+
type: 'ciphertext',
|
|
78
|
+
encKey: keys.serverEncKey,
|
|
79
|
+
fixedIv: keys.serverIv,
|
|
80
|
+
iv: decrypted.iv,
|
|
81
|
+
recordNumber: recordRecvCount,
|
|
82
|
+
macKey,
|
|
83
|
+
ciphertext: content,
|
|
84
|
+
plaintext: decrypted.plaintext,
|
|
85
|
+
contentType,
|
|
86
|
+
};
|
|
87
|
+
content = decrypted.plaintext;
|
|
88
|
+
if (contentType) {
|
|
89
|
+
content = content.slice(0, -1);
|
|
90
|
+
}
|
|
91
|
+
logger.trace({
|
|
92
|
+
recordRecvCount,
|
|
93
|
+
contentType,
|
|
94
|
+
length: content.length,
|
|
95
|
+
}, 'decrypted wrapped record');
|
|
96
|
+
recordRecvCount += 1;
|
|
97
|
+
}
|
|
98
|
+
onRead?.({ content, header }, ctx);
|
|
99
|
+
if (type === PACKET_TYPE.WRAPPED_RECORD
|
|
100
|
+
|| type === PACKET_TYPE.HELLO) {
|
|
101
|
+
// do nothing -- pass through
|
|
102
|
+
}
|
|
103
|
+
else if (type === PACKET_TYPE.CHANGE_CIPHER_SPEC) {
|
|
104
|
+
logger.debug('received change cipher spec');
|
|
105
|
+
cipherSpecChanged = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
else if (type === PACKET_TYPE.ALERT) {
|
|
109
|
+
await handleAlert(content);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
logger.warn({
|
|
114
|
+
type: type.toString(16),
|
|
115
|
+
chunk: toHexStringWithWhitespace(content)
|
|
116
|
+
}, 'cannot process message');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await processRecord({
|
|
121
|
+
content,
|
|
122
|
+
contentType: contentType
|
|
123
|
+
? CONTENT_TYPE_MAP[contentType]
|
|
124
|
+
: undefined,
|
|
125
|
+
header,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
logger.error({ err }, 'error processing record');
|
|
130
|
+
end(err);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const processPacket = (pkt) => (enqueueServerPacket(processPacketUnsafe, pkt));
|
|
134
|
+
async function processRecord({ content: record, contentType, header, }) {
|
|
135
|
+
contentType ??= header[0];
|
|
136
|
+
if (contentType === CONTENT_TYPE_MAP.HANDSHAKE) {
|
|
137
|
+
handshakePacketStream = concatenateUint8Arrays([handshakePacketStream, record]);
|
|
138
|
+
let data;
|
|
139
|
+
while (data = readPacket()) {
|
|
140
|
+
const { type, content } = data;
|
|
141
|
+
switch (type) {
|
|
142
|
+
case SUPPORTED_RECORD_TYPE_MAP.SERVER_HELLO:
|
|
143
|
+
logger.trace('received server hello');
|
|
144
|
+
const hello = await parseServerHello(content);
|
|
145
|
+
if (!hello.supportsPsk && earlySecret) {
|
|
146
|
+
throw new Error('Server does not support PSK');
|
|
147
|
+
}
|
|
148
|
+
cipherSuite = hello.cipherSuite;
|
|
149
|
+
connTlsVersion = hello.serverTlsVersion;
|
|
150
|
+
serverRandom = hello.serverRandom;
|
|
151
|
+
setAlpn(hello.extensions?.ALPN);
|
|
152
|
+
const cipherSuiteData = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite];
|
|
153
|
+
logger.debug({ cipherSuite, connTlsVersion, selectedAlpn }, 'processed server hello');
|
|
154
|
+
if (hello.publicKeyType && hello.publicKey) {
|
|
155
|
+
await processServerPubKey({
|
|
156
|
+
publicKeyType: hello.publicKeyType,
|
|
157
|
+
publicKey: hello.publicKey
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else if ('isRsaEcdh' in cipherSuiteData && cipherSuiteData.isRsaEcdh) {
|
|
161
|
+
keyType = 'RSA';
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case SUPPORTED_RECORD_TYPE_MAP.ENCRYPTED_EXTENSIONS:
|
|
165
|
+
const extData = parseServerExtensions(content);
|
|
166
|
+
logger.debug({
|
|
167
|
+
len: content.length,
|
|
168
|
+
extData
|
|
169
|
+
}, 'received encrypted extensions');
|
|
170
|
+
setAlpn(extData?.ALPN);
|
|
171
|
+
break;
|
|
172
|
+
case SUPPORTED_RECORD_TYPE_MAP.HELLO_RETRY_REQUEST:
|
|
173
|
+
throw new Error('Hello retry not supported. Please re-establish connection');
|
|
174
|
+
case SUPPORTED_RECORD_TYPE_MAP.CERTIFICATE:
|
|
175
|
+
logger.trace({ len: content.length }, 'received certificate');
|
|
176
|
+
const result = parseCertificates(content, { version: connTlsVersion });
|
|
177
|
+
certificates = result.certificates;
|
|
178
|
+
logger.debug({ len: certificates.length }, 'parsed certificates');
|
|
179
|
+
if (verifyServerCertificate && !certificatesVerified) {
|
|
180
|
+
await verifyCertificateChain(certificates, host, logger, fetchCertificateBytes, rootCAs);
|
|
181
|
+
logger.debug('verified certificate chain');
|
|
182
|
+
certificatesVerified = true;
|
|
183
|
+
}
|
|
184
|
+
onRecvCertificates?.({ certificates });
|
|
185
|
+
break;
|
|
186
|
+
case SUPPORTED_RECORD_TYPE_MAP.CERTIFICATE_VERIFY:
|
|
187
|
+
logger.debug({ len: content.length }, 'received certificate verify');
|
|
188
|
+
const signature = parseServerCertificateVerify(content);
|
|
189
|
+
logger.debug({ alg: signature.algorithm }, 'parsed certificate verify');
|
|
190
|
+
if (!certificates?.length) {
|
|
191
|
+
throw new Error('No certificates received');
|
|
192
|
+
}
|
|
193
|
+
const signatureData = await getSignatureDataTls13(handshakeMsgs.slice(0, -1), cipherSuite);
|
|
194
|
+
await verifyCertificateSignature({
|
|
195
|
+
...signature,
|
|
196
|
+
publicKey: certificates[0].getPublicKey(),
|
|
197
|
+
signatureData,
|
|
198
|
+
});
|
|
199
|
+
break;
|
|
200
|
+
case SUPPORTED_RECORD_TYPE_MAP.FINISHED:
|
|
201
|
+
await processServerFinish(content);
|
|
202
|
+
break;
|
|
203
|
+
case SUPPORTED_RECORD_TYPE_MAP.KEY_UPDATE:
|
|
204
|
+
const newMasterSecret = await computeUpdatedTrafficMasterSecret(keys.serverSecret, cipherSuite);
|
|
205
|
+
const newKeys = await deriveTrafficKeysForSide(newMasterSecret, cipherSuite);
|
|
206
|
+
keys = {
|
|
207
|
+
...keys,
|
|
208
|
+
serverSecret: newMasterSecret,
|
|
209
|
+
serverEncKey: newKeys.encKey,
|
|
210
|
+
serverIv: newKeys.iv,
|
|
211
|
+
};
|
|
212
|
+
recordRecvCount = 0;
|
|
213
|
+
logger.debug('updated server traffic keys');
|
|
214
|
+
break;
|
|
215
|
+
case SUPPORTED_RECORD_TYPE_MAP.SESSION_TICKET:
|
|
216
|
+
if (connTlsVersion === 'TLS1_3') {
|
|
217
|
+
logger.debug({ len: record.length }, 'received session ticket');
|
|
218
|
+
const ticket = parseSessionTicket(content);
|
|
219
|
+
onSessionTicket?.(ticket);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
logger.warn('ignoring received session ticket in TLS 1.2');
|
|
223
|
+
}
|
|
224
|
+
break;
|
|
225
|
+
case SUPPORTED_RECORD_TYPE_MAP.CERTIFICATE_REQUEST:
|
|
226
|
+
logger.debug('received client certificate request');
|
|
227
|
+
clientCertificateRequested = true;
|
|
228
|
+
break;
|
|
229
|
+
case SUPPORTED_RECORD_TYPE_MAP.SERVER_KEY_SHARE:
|
|
230
|
+
logger.trace('received server key share');
|
|
231
|
+
if (!certificates?.length) {
|
|
232
|
+
throw new Error('No certificates received');
|
|
233
|
+
}
|
|
234
|
+
// extract pub key & signature of pub key with cert
|
|
235
|
+
const keyShare = await processServerKeyShare(content);
|
|
236
|
+
logger.debug({
|
|
237
|
+
publicKeyType: keyShare.publicKeyType,
|
|
238
|
+
signatureAlgorithm: keyShare.signatureAlgorithm,
|
|
239
|
+
}, 'got server key share');
|
|
240
|
+
// compute signature data
|
|
241
|
+
const signatureData12 = await getSignatureDataTls12({
|
|
242
|
+
clientRandom: clientRandom,
|
|
243
|
+
serverRandom: serverRandom,
|
|
244
|
+
curveType: keyShare.publicKeyType,
|
|
245
|
+
publicKey: keyShare.publicKey,
|
|
246
|
+
});
|
|
247
|
+
// verify signature
|
|
248
|
+
await verifyCertificateSignature({
|
|
249
|
+
signature: keyShare.signatureBytes,
|
|
250
|
+
algorithm: keyShare.signatureAlgorithm,
|
|
251
|
+
publicKey: certificates[0].getPublicKey(),
|
|
252
|
+
signatureData: signatureData12,
|
|
253
|
+
});
|
|
254
|
+
logger.debug('verified server key share signature');
|
|
255
|
+
if (verifyServerCertificate && !certificatesVerified) {
|
|
256
|
+
await verifyCertificateChain(certificates, host, logger, fetchCertificateBytes, rootCAs);
|
|
257
|
+
logger.debug('verified certificate chain');
|
|
258
|
+
certificatesVerified = true;
|
|
259
|
+
}
|
|
260
|
+
// compute shared keys
|
|
261
|
+
await processServerPubKey(keyShare);
|
|
262
|
+
break;
|
|
263
|
+
case SUPPORTED_RECORD_TYPE_MAP.SERVER_HELLO_DONE:
|
|
264
|
+
logger.debug('server hello done');
|
|
265
|
+
if (!keyType) {
|
|
266
|
+
// need to execute client key share
|
|
267
|
+
throw new Error('Key exchange without key-type not supported');
|
|
268
|
+
}
|
|
269
|
+
let clientKeyShare;
|
|
270
|
+
if (keyType === 'RSA') {
|
|
271
|
+
if (keys) {
|
|
272
|
+
throw new Error('Keys already computed, despite RSA key type');
|
|
273
|
+
}
|
|
274
|
+
const { preMasterSecret, encrypted } = await createRsaPreMasterSecret(certificates[0], connTlsVersion);
|
|
275
|
+
clientKeyShare = await packClientRsaKeyShare(encrypted);
|
|
276
|
+
keys = await computeSharedKeysTls12({
|
|
277
|
+
preMasterSecret: preMasterSecret,
|
|
278
|
+
clientRandom: clientRandom,
|
|
279
|
+
serverRandom: serverRandom,
|
|
280
|
+
cipherSuite: cipherSuite,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
clientKeyShare
|
|
285
|
+
= await packClientCurveKeyShare(keyPairs[keyType].pubKey);
|
|
286
|
+
}
|
|
287
|
+
await writePacket({ type: 'HELLO', data: clientKeyShare });
|
|
288
|
+
handshakeMsgs.push(clientKeyShare);
|
|
289
|
+
await writeChangeCipherSpec();
|
|
290
|
+
const finishMsg = await packClientFinishTls12({
|
|
291
|
+
secret: keys.masterSecret,
|
|
292
|
+
handshakeMessages: handshakeMsgs,
|
|
293
|
+
cipherSuite: cipherSuite,
|
|
294
|
+
});
|
|
295
|
+
await writeEncryptedPacket({ data: finishMsg, type: 'HELLO' });
|
|
296
|
+
handshakeMsgs.push(finishMsg);
|
|
297
|
+
break;
|
|
298
|
+
default:
|
|
299
|
+
logger.warn({ type: type.toString(16) }, 'cannot process record');
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function readPacket() {
|
|
304
|
+
if (!handshakePacketStream.length) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const type = handshakePacketStream[0];
|
|
308
|
+
const content = readWithLength(handshakePacketStream.slice(1), RECORD_LENGTH_BYTES);
|
|
309
|
+
if (!content) {
|
|
310
|
+
logger.warn('missing bytes from packet');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const totalLength = 1 + RECORD_LENGTH_BYTES + content.length;
|
|
314
|
+
if (!handshakeDone) {
|
|
315
|
+
handshakeMsgs.push(handshakePacketStream.slice(0, totalLength));
|
|
316
|
+
}
|
|
317
|
+
handshakePacketStream = handshakePacketStream.slice(totalLength);
|
|
318
|
+
return { type, content };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else if (contentType === CONTENT_TYPE_MAP.APPLICATION_DATA) {
|
|
322
|
+
logger.trace({ len: record.length }, 'received application data');
|
|
323
|
+
onApplicationData?.(record);
|
|
324
|
+
}
|
|
325
|
+
else if (contentType === CONTENT_TYPE_MAP.ALERT) {
|
|
326
|
+
await handleAlert(record);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
logger.warn({ record: record, contentType: contentType?.toString(16) }, 'cannot process record');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function setAlpn(alpn) {
|
|
333
|
+
selectedAlpn = alpn || applicationLayerProtocols?.[0];
|
|
334
|
+
if (selectedAlpn && !applicationLayerProtocols?.includes(selectedAlpn)) {
|
|
335
|
+
throw new Error(`Server selected unsupported ALPN: "${selectedAlpn}"`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function handleAlert(content) {
|
|
339
|
+
if (ended) {
|
|
340
|
+
logger.warn('connection closed, ignoring alert');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const { level, description } = parseTlsAlert(content);
|
|
344
|
+
const msg = (description === 'HANDSHAKE_FAILURE' || description === 'PROTOCOL_VERSION'
|
|
345
|
+
? 'Unsupported TLS version'
|
|
346
|
+
: 'received alert');
|
|
347
|
+
logger[level === 'WARNING' ? 'warn' : 'error']({ level, description }, msg);
|
|
348
|
+
if (level === 'FATAL'
|
|
349
|
+
|| description === 'CLOSE_NOTIFY') {
|
|
350
|
+
end(level === 'FATAL'
|
|
351
|
+
? new Error(`Fatal alert: ${description}`)
|
|
352
|
+
: undefined);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async function sendClientCertificate() {
|
|
356
|
+
if (clientCertificateRequested) {
|
|
357
|
+
const clientZeroCert = concatenateUint8Arrays([
|
|
358
|
+
new Uint8Array([SUPPORTED_RECORD_TYPE_MAP.CERTIFICATE, 0x00]),
|
|
359
|
+
packWithLength(new Uint8Array([0, 0, 0, 0]))
|
|
360
|
+
]);
|
|
361
|
+
logger.trace({ cert: toHexStringWithWhitespace(clientZeroCert) }, 'sending zero certs');
|
|
362
|
+
await writeEncryptedPacket({
|
|
363
|
+
type: 'WRAPPED_RECORD',
|
|
364
|
+
data: clientZeroCert,
|
|
365
|
+
contentType: 'HANDSHAKE'
|
|
366
|
+
});
|
|
367
|
+
handshakeMsgs.push(clientZeroCert);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function processServerFinish(serverFinish) {
|
|
371
|
+
logger.debug('received server finish');
|
|
372
|
+
if (!certificatesVerified
|
|
373
|
+
// when using a PSK, the server does not send certificates
|
|
374
|
+
&& !earlySecret
|
|
375
|
+
&& verifyServerCertificate) {
|
|
376
|
+
throw new Error('Finish received before certificate verification');
|
|
377
|
+
}
|
|
378
|
+
if (connTlsVersion === 'TLS1_2') {
|
|
379
|
+
await processServerFinishTls12(serverFinish);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
await processServerFinishTls13(serverFinish);
|
|
383
|
+
}
|
|
384
|
+
handshakeDone = true;
|
|
385
|
+
onHandshake?.();
|
|
386
|
+
}
|
|
387
|
+
async function processServerFinishTls12(serverFinish) {
|
|
388
|
+
const genServerFinish = await generateFinishTls12('server', {
|
|
389
|
+
handshakeMessages: handshakeMsgs.slice(0, -1),
|
|
390
|
+
secret: keys.masterSecret,
|
|
391
|
+
cipherSuite: cipherSuite,
|
|
392
|
+
});
|
|
393
|
+
if (!areUint8ArraysEqual(genServerFinish, serverFinish)) {
|
|
394
|
+
throw new Error('Server finish does not match');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function processServerFinishTls13(serverFinish) {
|
|
398
|
+
// derive server keys now to streamline handshake messages handling
|
|
399
|
+
const serverKeys = await computeSharedKeys({
|
|
400
|
+
// we only use handshake messages till the server finish
|
|
401
|
+
hellos: handshakeMsgs,
|
|
402
|
+
cipherSuite: cipherSuite,
|
|
403
|
+
secretType: 'ap',
|
|
404
|
+
masterSecret: keys.masterSecret,
|
|
405
|
+
});
|
|
406
|
+
// the server hash computation does not include
|
|
407
|
+
// the server finish, so we need to exclude it
|
|
408
|
+
const handshakeMsgsForServerHash = handshakeMsgs.slice(0, -1);
|
|
409
|
+
await verifyFinishMessage(serverFinish, {
|
|
410
|
+
secret: keys.serverSecret,
|
|
411
|
+
handshakeMessages: handshakeMsgsForServerHash,
|
|
412
|
+
cipherSuite: cipherSuite
|
|
413
|
+
});
|
|
414
|
+
logger.debug('server finish verified');
|
|
415
|
+
// this might add an extra message to handshakeMsgs and affect handshakeHash
|
|
416
|
+
await sendClientCertificate();
|
|
417
|
+
const clientFinish = await packFinishMessagePacket({
|
|
418
|
+
secret: keys.clientSecret,
|
|
419
|
+
handshakeMessages: handshakeMsgs,
|
|
420
|
+
cipherSuite: cipherSuite
|
|
421
|
+
});
|
|
422
|
+
logger.trace({ finish: toHexStringWithWhitespace(clientFinish) }, 'sending client finish');
|
|
423
|
+
await writeEncryptedPacket({
|
|
424
|
+
type: 'WRAPPED_RECORD',
|
|
425
|
+
data: clientFinish,
|
|
426
|
+
contentType: 'HANDSHAKE'
|
|
427
|
+
});
|
|
428
|
+
// add the client finish to the handshake messages
|
|
429
|
+
handshakeMsgs.push(clientFinish);
|
|
430
|
+
// switch to using the provider keys
|
|
431
|
+
keys = serverKeys;
|
|
432
|
+
// also the send/recv counters are reset
|
|
433
|
+
// once we switch to the provider keys
|
|
434
|
+
recordSendCount = 0;
|
|
435
|
+
recordRecvCount = 0;
|
|
436
|
+
}
|
|
437
|
+
async function processServerPubKey(data) {
|
|
438
|
+
keyType = data.publicKeyType;
|
|
439
|
+
const { keyPair, algorithm } = await getKeyPair(data.publicKeyType);
|
|
440
|
+
const sharedSecret = await crypto.calculateSharedSecret(algorithm, keyPair.privKey, data.publicKey);
|
|
441
|
+
if (connTlsVersion === 'TLS1_2') {
|
|
442
|
+
keys = await computeSharedKeysTls12({
|
|
443
|
+
preMasterSecret: sharedSecret,
|
|
444
|
+
clientRandom: clientRandom,
|
|
445
|
+
serverRandom: serverRandom,
|
|
446
|
+
cipherSuite: cipherSuite,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
keys = await computeSharedKeys({
|
|
451
|
+
hellos: handshakeMsgs,
|
|
452
|
+
cipherSuite: cipherSuite,
|
|
453
|
+
secretType: 'hs',
|
|
454
|
+
masterSecret: sharedSecret,
|
|
455
|
+
earlySecret,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
logger.debug({ keyType }, 'computed shared keys');
|
|
459
|
+
}
|
|
460
|
+
async function writeChangeCipherSpec() {
|
|
461
|
+
logger.debug('sending change cipher spec');
|
|
462
|
+
const changeCipherSpecData = new Uint8Array([1]);
|
|
463
|
+
await writePacket({
|
|
464
|
+
type: 'CHANGE_CIPHER_SPEC',
|
|
465
|
+
data: changeCipherSpecData
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
async function writeEncryptedPacket(opts) {
|
|
469
|
+
logger.trace({ ...opts, data: toHexStringWithWhitespace(opts.data) }, 'writing enc packet');
|
|
470
|
+
const macKey = 'clientMacKey' in keys
|
|
471
|
+
? keys.clientMacKey
|
|
472
|
+
: undefined;
|
|
473
|
+
let plaintext = opts.data;
|
|
474
|
+
if (connTlsVersion === 'TLS1_3'
|
|
475
|
+
&& typeof opts.contentType !== 'undefined') {
|
|
476
|
+
plaintext = concatenateUint8Arrays([
|
|
477
|
+
plaintext,
|
|
478
|
+
new Uint8Array([CONTENT_TYPE_MAP[opts.contentType]])
|
|
479
|
+
]);
|
|
480
|
+
}
|
|
481
|
+
const { ciphertext, iv } = await encryptWrappedRecord(plaintext, {
|
|
482
|
+
key: keys.clientEncKey,
|
|
483
|
+
iv: keys.clientIv,
|
|
484
|
+
recordNumber: recordSendCount,
|
|
485
|
+
cipherSuite: cipherSuite,
|
|
486
|
+
macKey,
|
|
487
|
+
recordHeaderOpts: {
|
|
488
|
+
type: opts.type,
|
|
489
|
+
version: opts.version
|
|
490
|
+
},
|
|
491
|
+
version: connTlsVersion,
|
|
492
|
+
});
|
|
493
|
+
const header = packPacketHeader(ciphertext.length, opts);
|
|
494
|
+
await write({ header, content: ciphertext }, {
|
|
495
|
+
type: 'ciphertext',
|
|
496
|
+
encKey: keys.clientEncKey,
|
|
497
|
+
fixedIv: keys.clientIv,
|
|
498
|
+
iv,
|
|
499
|
+
recordNumber: recordSendCount,
|
|
500
|
+
macKey,
|
|
501
|
+
ciphertext,
|
|
502
|
+
plaintext,
|
|
503
|
+
contentType: opts.contentType,
|
|
504
|
+
});
|
|
505
|
+
recordSendCount += 1;
|
|
506
|
+
}
|
|
507
|
+
async function writePacket(opts) {
|
|
508
|
+
logger.trace({ ...opts, data: toHexStringWithWhitespace(opts.data) }, 'writing packet');
|
|
509
|
+
const header = packPacketHeader(opts.data.length, opts);
|
|
510
|
+
await write({ header, content: opts.data }, { type: 'plaintext' });
|
|
511
|
+
}
|
|
512
|
+
async function end(error) {
|
|
513
|
+
await enqueueServerPacket(() => { });
|
|
514
|
+
logger.trace({ err: error }, 'ended tls connection');
|
|
515
|
+
ended = true;
|
|
516
|
+
handshakeDone = false;
|
|
517
|
+
handshakeMsgs = [];
|
|
518
|
+
keys = undefined;
|
|
519
|
+
recordSendCount = 0;
|
|
520
|
+
recordRecvCount = 0;
|
|
521
|
+
earlySecret = undefined;
|
|
522
|
+
cipherSuite = undefined;
|
|
523
|
+
keyType = undefined;
|
|
524
|
+
clientRandom = undefined;
|
|
525
|
+
serverRandom = undefined;
|
|
526
|
+
processor.reset();
|
|
527
|
+
onTlsEnd?.(error);
|
|
528
|
+
}
|
|
529
|
+
async function getKeyPair(keyType) {
|
|
530
|
+
const algorithm = SUPPORTED_NAMED_CURVE_MAP[keyType].algorithm;
|
|
531
|
+
keyPairs[keyType] ??= await crypto.generateKeyPair(algorithm);
|
|
532
|
+
return { algorithm, keyPair: keyPairs[keyType] };
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
getMetadata() {
|
|
536
|
+
return { cipherSuite, keyType, version: connTlsVersion, selectedAlpn };
|
|
537
|
+
},
|
|
538
|
+
hasEnded() {
|
|
539
|
+
return ended;
|
|
540
|
+
},
|
|
541
|
+
/**
|
|
542
|
+
* Get the current traffic keys
|
|
543
|
+
*/
|
|
544
|
+
getKeys() {
|
|
545
|
+
if (!keys) {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
return { ...keys, recordSendCount, recordRecvCount };
|
|
549
|
+
},
|
|
550
|
+
/**
|
|
551
|
+
* Session ID used to connect to the server
|
|
552
|
+
*/
|
|
553
|
+
getSessionId() {
|
|
554
|
+
return sessionId;
|
|
555
|
+
},
|
|
556
|
+
isHandshakeDone() {
|
|
557
|
+
return handshakeDone;
|
|
558
|
+
},
|
|
559
|
+
getPskFromTicket(ticket) {
|
|
560
|
+
return getPskFromTicket(ticket, {
|
|
561
|
+
masterKey: keys.masterSecret,
|
|
562
|
+
hellos: handshakeMsgs,
|
|
563
|
+
cipherSuite: cipherSuite,
|
|
564
|
+
});
|
|
565
|
+
},
|
|
566
|
+
/**
|
|
567
|
+
* Start the handshake with the server
|
|
568
|
+
*/
|
|
569
|
+
async startHandshake(opts) {
|
|
570
|
+
if (handshakeDone) {
|
|
571
|
+
throw new Error('Handshake already done');
|
|
572
|
+
}
|
|
573
|
+
sessionId = crypto.randomBytes(32);
|
|
574
|
+
ended = false;
|
|
575
|
+
clientRandom = opts?.random || crypto.randomBytes(32);
|
|
576
|
+
const clientHello = await packClientHello({
|
|
577
|
+
host,
|
|
578
|
+
keysToShare: await Promise.all(namedCurves
|
|
579
|
+
.map(async (keyType) => {
|
|
580
|
+
const { keyPair } = await getKeyPair(keyType);
|
|
581
|
+
return {
|
|
582
|
+
type: keyType,
|
|
583
|
+
key: keyPair.pubKey,
|
|
584
|
+
};
|
|
585
|
+
})),
|
|
586
|
+
random: clientRandom,
|
|
587
|
+
sessionId,
|
|
588
|
+
psk: opts?.psk,
|
|
589
|
+
cipherSuites,
|
|
590
|
+
supportedProtocolVersions,
|
|
591
|
+
signatureAlgorithms,
|
|
592
|
+
applicationLayerProtocols,
|
|
593
|
+
});
|
|
594
|
+
handshakeMsgs.push(clientHello);
|
|
595
|
+
if (opts?.psk) {
|
|
596
|
+
earlySecret = opts.psk.earlySecret;
|
|
597
|
+
}
|
|
598
|
+
await writePacket({
|
|
599
|
+
type: 'HELLO',
|
|
600
|
+
data: clientHello,
|
|
601
|
+
});
|
|
602
|
+
},
|
|
603
|
+
/**
|
|
604
|
+
* Handle bytes received from the server.
|
|
605
|
+
* Could be a complete or partial TLS packet
|
|
606
|
+
*/
|
|
607
|
+
async handleReceivedBytes(data) {
|
|
608
|
+
await Promise.all(Array.from(processor.onData(data))
|
|
609
|
+
.map(processPacket));
|
|
610
|
+
},
|
|
611
|
+
/**
|
|
612
|
+
* Handle a complete TLS packet received
|
|
613
|
+
* from the server
|
|
614
|
+
*/
|
|
615
|
+
handleReceivedPacket: processPacket,
|
|
616
|
+
/**
|
|
617
|
+
* Utilise the KeyUpdate handshake message to update
|
|
618
|
+
* the traffic keys. Available only in TLS 1.3
|
|
619
|
+
* @param requestUpdateFromServer should the server be requested to
|
|
620
|
+
* update its keys as well
|
|
621
|
+
*/
|
|
622
|
+
async updateTrafficKeys(requestUpdateFromServer = false) {
|
|
623
|
+
const packet = packKeyUpdateRecord(requestUpdateFromServer
|
|
624
|
+
? 'UPDATE_REQUESTED'
|
|
625
|
+
: 'UPDATE_NOT_REQUESTED');
|
|
626
|
+
await writeEncryptedPacket({
|
|
627
|
+
data: packet,
|
|
628
|
+
type: 'WRAPPED_RECORD',
|
|
629
|
+
contentType: 'HANDSHAKE'
|
|
630
|
+
});
|
|
631
|
+
const newMasterSecret = await computeUpdatedTrafficMasterSecret(keys.clientSecret, cipherSuite);
|
|
632
|
+
const newKeys = await deriveTrafficKeysForSide(newMasterSecret, cipherSuite);
|
|
633
|
+
keys = {
|
|
634
|
+
...keys,
|
|
635
|
+
clientSecret: newMasterSecret,
|
|
636
|
+
clientEncKey: newKeys.encKey,
|
|
637
|
+
clientIv: newKeys.iv,
|
|
638
|
+
};
|
|
639
|
+
recordSendCount = 0;
|
|
640
|
+
logger.info('updated client traffic keys');
|
|
641
|
+
},
|
|
642
|
+
async write(data) {
|
|
643
|
+
if (!handshakeDone) {
|
|
644
|
+
throw new Error('Handshake not done');
|
|
645
|
+
}
|
|
646
|
+
const chunks = chunkUint8Array(data, MAX_ENC_PACKET_SIZE);
|
|
647
|
+
for (const chunk of chunks) {
|
|
648
|
+
await writeEncryptedPacket({
|
|
649
|
+
data: chunk,
|
|
650
|
+
type: 'WRAPPED_RECORD',
|
|
651
|
+
contentType: 'APPLICATION_DATA'
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
end,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as esbuild from 'esbuild';
|
|
2
|
+
const rslt = await esbuild.build({
|
|
3
|
+
entryPoints: ['./src/scripts/jsc.ts'],
|
|
4
|
+
bundle: true,
|
|
5
|
+
platform: 'browser',
|
|
6
|
+
outfile: 'out/jsc-bridge.mjs',
|
|
7
|
+
format: 'esm',
|
|
8
|
+
tsconfig: 'tsconfig.json',
|
|
9
|
+
legalComments: 'none',
|
|
10
|
+
metafile: true, // Enable metafile generation
|
|
11
|
+
treeShaking: true,
|
|
12
|
+
alias: {
|
|
13
|
+
'@noble/hashes/crypto': './src/scripts/fallbacks/crypto.ts',
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
if (process.argv.includes('--analyze')) {
|
|
17
|
+
// Analyze the metafile
|
|
18
|
+
const analysis = await esbuild.analyzeMetafile(rslt.metafile);
|
|
19
|
+
console.log(analysis);
|
|
20
|
+
}
|