@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,51 @@
|
|
|
1
|
+
import type { Logger, TLSPacketWithType, TLSProtocolVersion } from '../types/index.ts';
|
|
2
|
+
import { PACKET_TYPE } from './constants.ts';
|
|
3
|
+
type PacketType = keyof typeof PACKET_TYPE;
|
|
4
|
+
export type PacketHeaderOptions = {
|
|
5
|
+
type: PacketType;
|
|
6
|
+
/**
|
|
7
|
+
* TLS version to use in the header packet
|
|
8
|
+
* */
|
|
9
|
+
version?: TLSProtocolVersion;
|
|
10
|
+
};
|
|
11
|
+
export type PacketOptions = PacketHeaderOptions & {
|
|
12
|
+
data: Uint8Array;
|
|
13
|
+
};
|
|
14
|
+
export declare function packPacketHeader(dataLength: number, { type, version }: PacketHeaderOptions): Uint8Array<ArrayBufferLike>;
|
|
15
|
+
export declare function packPacket(opts: PacketOptions): Uint8Array<ArrayBufferLike>;
|
|
16
|
+
/**
|
|
17
|
+
* Packs data prefixed with the length of the data;
|
|
18
|
+
* Length encoded UInt24 big endian
|
|
19
|
+
*/
|
|
20
|
+
export declare function packWith3ByteLength(data: Uint8Array): Uint8Array<ArrayBufferLike>;
|
|
21
|
+
export declare function readWithLength(data: Uint8Array, lengthBytes?: number): Uint8Array<ArrayBuffer> | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Read a prefix of the data, that is prefixed with the length of
|
|
24
|
+
* said data. Throws an error if the data is not long enough
|
|
25
|
+
*
|
|
26
|
+
* @param data total data to read from
|
|
27
|
+
* @param lengthBytes number of bytes to read the length from.
|
|
28
|
+
* Default is 2 bytes
|
|
29
|
+
*/
|
|
30
|
+
export declare function expectReadWithLength(data: Uint8Array, lengthBytes?: number): Uint8Array<ArrayBuffer>;
|
|
31
|
+
/**
|
|
32
|
+
* Packs data prefixed with the length of the data;
|
|
33
|
+
* Length encoded UInt16 big endian
|
|
34
|
+
*/
|
|
35
|
+
export declare function packWithLength(data: Uint8Array): Uint8Array<ArrayBuffer>;
|
|
36
|
+
/**
|
|
37
|
+
* Processes an incoming stream of TLS packets
|
|
38
|
+
*/
|
|
39
|
+
export declare function makeMessageProcessor(logger: Logger): {
|
|
40
|
+
getPendingBuffer(): Uint8Array<ArrayBufferLike>;
|
|
41
|
+
/**
|
|
42
|
+
* @param packet TLS packet;
|
|
43
|
+
* can be multiple packets concatenated
|
|
44
|
+
* or incomplete packet
|
|
45
|
+
* or a single packet
|
|
46
|
+
* @param onChunk handle a complete packet
|
|
47
|
+
*/
|
|
48
|
+
onData(packet: Uint8Array): Generator<TLSPacketWithType, void, unknown>;
|
|
49
|
+
reset(): void;
|
|
50
|
+
};
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { PACKET_TYPE, TLS_PROTOCOL_VERSION_MAP } from "./constants.js";
|
|
2
|
+
import { concatenateUint8Arrays, uint8ArrayToDataView } from "./generics.js";
|
|
3
|
+
export function packPacketHeader(dataLength, { type, version = 'TLS1_2' }) {
|
|
4
|
+
const lengthBuffer = new Uint8Array(2);
|
|
5
|
+
const dataView = uint8ArrayToDataView(lengthBuffer);
|
|
6
|
+
dataView.setUint16(0, dataLength);
|
|
7
|
+
return concatenateUint8Arrays([
|
|
8
|
+
new Uint8Array([PACKET_TYPE[type]]),
|
|
9
|
+
TLS_PROTOCOL_VERSION_MAP[version],
|
|
10
|
+
lengthBuffer
|
|
11
|
+
]);
|
|
12
|
+
}
|
|
13
|
+
export function packPacket(opts) {
|
|
14
|
+
return concatenateUint8Arrays([
|
|
15
|
+
packPacketHeader(opts.data.length, opts),
|
|
16
|
+
opts.data
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Packs data prefixed with the length of the data;
|
|
21
|
+
* Length encoded UInt24 big endian
|
|
22
|
+
*/
|
|
23
|
+
export function packWith3ByteLength(data) {
|
|
24
|
+
return concatenateUint8Arrays([
|
|
25
|
+
new Uint8Array([0x00]),
|
|
26
|
+
packWithLength(data)
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
export function readWithLength(data, lengthBytes = 2) {
|
|
30
|
+
const dataView = uint8ArrayToDataView(data);
|
|
31
|
+
const length = lengthBytes === 1
|
|
32
|
+
? dataView.getUint8(0)
|
|
33
|
+
: dataView.getUint16(lengthBytes === 3 ? 1 : 0);
|
|
34
|
+
if (data.length < lengthBytes + length) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return data.slice(lengthBytes, lengthBytes + length);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read a prefix of the data, that is prefixed with the length of
|
|
41
|
+
* said data. Throws an error if the data is not long enough
|
|
42
|
+
*
|
|
43
|
+
* @param data total data to read from
|
|
44
|
+
* @param lengthBytes number of bytes to read the length from.
|
|
45
|
+
* Default is 2 bytes
|
|
46
|
+
*/
|
|
47
|
+
export function expectReadWithLength(data, lengthBytes = 2) {
|
|
48
|
+
const result = readWithLength(data, lengthBytes);
|
|
49
|
+
if (!result) {
|
|
50
|
+
throw new Error(`Expected packet to have at least ${data.length + lengthBytes} bytes, got ${data.length}`);
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Packs data prefixed with the length of the data;
|
|
56
|
+
* Length encoded UInt16 big endian
|
|
57
|
+
*/
|
|
58
|
+
export function packWithLength(data) {
|
|
59
|
+
const buffer = new Uint8Array(2 + data.length);
|
|
60
|
+
const dataView = uint8ArrayToDataView(buffer);
|
|
61
|
+
dataView.setUint16(0, data.length);
|
|
62
|
+
buffer.set(data, 2);
|
|
63
|
+
return buffer;
|
|
64
|
+
}
|
|
65
|
+
// const SUPPORTED_PROTO_VERSIONS = [
|
|
66
|
+
// LEGACY_PROTOCOL_VERSION,
|
|
67
|
+
// CURRENT_PROTOCOL_VERSION,
|
|
68
|
+
// ]
|
|
69
|
+
/**
|
|
70
|
+
* Processes an incoming stream of TLS packets
|
|
71
|
+
*/
|
|
72
|
+
export function makeMessageProcessor(logger) {
|
|
73
|
+
let currentMessageType = undefined;
|
|
74
|
+
let currentMessageHeader = undefined;
|
|
75
|
+
let buffer = new Uint8Array(0);
|
|
76
|
+
let bytesLeft = 0;
|
|
77
|
+
return {
|
|
78
|
+
getPendingBuffer() {
|
|
79
|
+
return buffer;
|
|
80
|
+
},
|
|
81
|
+
/**
|
|
82
|
+
* @param packet TLS packet;
|
|
83
|
+
* can be multiple packets concatenated
|
|
84
|
+
* or incomplete packet
|
|
85
|
+
* or a single packet
|
|
86
|
+
* @param onChunk handle a complete packet
|
|
87
|
+
*/
|
|
88
|
+
*onData(packet) {
|
|
89
|
+
buffer = concatenateUint8Arrays([buffer, packet]);
|
|
90
|
+
while (buffer.length) {
|
|
91
|
+
// if we already aren't processing a packet
|
|
92
|
+
// this is the first byte
|
|
93
|
+
if (!currentMessageType) {
|
|
94
|
+
if (buffer.length < 5) {
|
|
95
|
+
// we don't have enough bytes to process the header
|
|
96
|
+
// wait for more bytes
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
// bytes[0] tells us which packet type we're processing
|
|
100
|
+
// bytes[1:2] tell us the protocol version
|
|
101
|
+
// bytes[3:4] tell us the length of the packet
|
|
102
|
+
const packTypeNum = buffer[0];
|
|
103
|
+
currentMessageType = packTypeNum;
|
|
104
|
+
// get the number of bytes we need to process
|
|
105
|
+
// to complete the packet
|
|
106
|
+
const buffDataView = uint8ArrayToDataView(buffer);
|
|
107
|
+
bytesLeft = buffDataView.getUint16(3);
|
|
108
|
+
currentMessageHeader = buffer.slice(0, 5);
|
|
109
|
+
// const protoVersion = currentMessageHeader.slice(1, 3)
|
|
110
|
+
// const isSupportedVersion = SUPPORTED_PROTO_VERSIONS
|
|
111
|
+
// .some((v) => areUint8ArraysEqual(v, protoVersion))
|
|
112
|
+
// if(!isSupportedVersion) {
|
|
113
|
+
// throw new Error(`Unsupported protocol version (${protoVersion})`)
|
|
114
|
+
// }
|
|
115
|
+
// remove the packet header
|
|
116
|
+
buffer = buffer.slice(5);
|
|
117
|
+
logger.trace({ bytesLeft, recvBuffer: buffer.length, type: currentMessageType }, 'starting processing packet');
|
|
118
|
+
}
|
|
119
|
+
if (buffer.length < bytesLeft) {
|
|
120
|
+
// we don't have enough bytes to process the packet
|
|
121
|
+
// wait for more bytes
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
const body = buffer.slice(0, bytesLeft);
|
|
125
|
+
logger.trace({ type: currentMessageType }, 'got complete packet');
|
|
126
|
+
const pktWithType = {
|
|
127
|
+
type: currentMessageType,
|
|
128
|
+
packet: {
|
|
129
|
+
header: currentMessageHeader,
|
|
130
|
+
content: body
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
currentMessageType = undefined;
|
|
134
|
+
// if the current chunk we have still has bytes left
|
|
135
|
+
// then that means we have another packet in the chunk
|
|
136
|
+
// this will be processed in the next iteration of the loop
|
|
137
|
+
buffer = buffer.slice(body.length);
|
|
138
|
+
yield pktWithType;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
reset() {
|
|
142
|
+
currentMessageType = undefined;
|
|
143
|
+
currentMessageHeader = undefined;
|
|
144
|
+
buffer = new Uint8Array(0);
|
|
145
|
+
bytesLeft = 0;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a TLS alert message
|
|
3
|
+
*/
|
|
4
|
+
export declare function parseTlsAlert(buffer: Uint8Array): {
|
|
5
|
+
level: "WARNING" | "FATAL";
|
|
6
|
+
description: "CLOSE_NOTIFY" | "UNEXPECTED_MESSAGE" | "BAD_RECORD_MAC" | "RECORD_OVERFLOW" | "HANDSHAKE_FAILURE" | "BAD_CERTIFICATE" | "UNSUPPORTED_CERTIFICATE" | "CERTIFICATE_REVOKED" | "CERTIFICATE_EXPIRED" | "CERTIFICATE_UNKNOWN" | "ILLEGAL_PARAMETER" | "UNKNOWN_CA" | "ACCESS_DENIED" | "DECODE_ERROR" | "DECRYPT_ERROR" | "PROTOCOL_VERSION" | "INSUFFICIENT_SECURITY" | "INTERNAL_ERROR" | "INAPPROPRIATE_FALLBACK" | "USER_CANCELED" | "MISSING_EXTENSION" | "UNSUPPORTED_EXTENSION" | "UNRECOGNIZED_NAME" | "BAD_CERTIFICATE_STATUS_RESPONSE" | "UNKNOWN_PSK_IDENTITY" | "CERTIFICATE_REQUIRED" | "NO_APPLICATION_PROTOCOL" | "DECRYPTION_FAILED_RESERVED" | "DECOMPRESSION_FAILURE" | "NO_CERTIFICATE_RESERVED" | "EXPORT_RESTRICTION_RESERVED" | "NO_RENEGOTIATION";
|
|
7
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ALERT_DESCRIPTION, ALERT_LEVEL } from "./constants.js";
|
|
2
|
+
import { uint8ArrayToDataView } from "./generics.js";
|
|
3
|
+
const ALERT_LEVEL_ENTRIES = Object
|
|
4
|
+
.entries(ALERT_LEVEL);
|
|
5
|
+
const ALERT_DESCRIPTION_ENTRIES = Object
|
|
6
|
+
.entries(ALERT_DESCRIPTION);
|
|
7
|
+
/**
|
|
8
|
+
* Parse a TLS alert message
|
|
9
|
+
*/
|
|
10
|
+
export function parseTlsAlert(buffer) {
|
|
11
|
+
const view = uint8ArrayToDataView(buffer);
|
|
12
|
+
const level = view.getUint8(0);
|
|
13
|
+
const description = view.getUint8(1);
|
|
14
|
+
const levelStr = ALERT_LEVEL_ENTRIES
|
|
15
|
+
.find(([, value]) => value === level)?.[0];
|
|
16
|
+
if (!levelStr) {
|
|
17
|
+
throw new Error(`Unknown alert level ${level}`);
|
|
18
|
+
}
|
|
19
|
+
const descriptionStr = ALERT_DESCRIPTION_ENTRIES
|
|
20
|
+
.find(([, value]) => value === description)?.[0];
|
|
21
|
+
if (!descriptionStr) {
|
|
22
|
+
throw new Error(`Unknown alert description ${description}`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
level: levelStr,
|
|
26
|
+
description: descriptionStr
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import './additional-root-cas.js';
|
|
2
|
+
import type { CertificatePublicKey, CipherSuite, Key, Logger, TLSProcessContext, X509Certificate } from '../types/index.ts';
|
|
3
|
+
import { SUPPORTED_NAMED_CURVE_MAP, SUPPORTED_SIGNATURE_ALGS_MAP } from './constants.ts';
|
|
4
|
+
import { defaultFetchCertificateBytes } from './x509.ts';
|
|
5
|
+
type VerifySignatureOptions = {
|
|
6
|
+
signature: Uint8Array;
|
|
7
|
+
algorithm: keyof typeof SUPPORTED_SIGNATURE_ALGS_MAP;
|
|
8
|
+
publicKey: CertificatePublicKey;
|
|
9
|
+
signatureData: Uint8Array;
|
|
10
|
+
};
|
|
11
|
+
export declare function parseCertificates(data: Uint8Array, { version }: TLSProcessContext): {
|
|
12
|
+
certificates: X509Certificate[];
|
|
13
|
+
ctx: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function parseServerCertificateVerify(data: Uint8Array): {
|
|
16
|
+
algorithm: "ECDSA_SECP256R1_SHA256" | "ECDSA_SECP384R1_SHA256" | "RSA_PSS_RSAE_SHA256" | "RSA_PKCS1_SHA256" | "RSA_PKCS1_SHA384" | "RSA_PKCS1_SHA512";
|
|
17
|
+
signature: Uint8Array<ArrayBuffer>;
|
|
18
|
+
};
|
|
19
|
+
export declare function verifyCertificateSignature({ signature, algorithm, publicKey, signatureData, }: VerifySignatureOptions): Promise<void>;
|
|
20
|
+
export declare function getSignatureDataTls13(hellos: Uint8Array[] | Uint8Array, cipherSuite: CipherSuite): Promise<Uint8Array<ArrayBufferLike>>;
|
|
21
|
+
type Tls12SignatureDataOpts = {
|
|
22
|
+
clientRandom: Uint8Array;
|
|
23
|
+
serverRandom: Uint8Array;
|
|
24
|
+
curveType: keyof typeof SUPPORTED_NAMED_CURVE_MAP;
|
|
25
|
+
publicKey: Key;
|
|
26
|
+
};
|
|
27
|
+
export declare function getSignatureDataTls12({ clientRandom, serverRandom, curveType, publicKey, }: Tls12SignatureDataOpts): Promise<Uint8Array<ArrayBufferLike>>;
|
|
28
|
+
export declare function verifyCertificateChain(chain: X509Certificate[], host: string, logger: Logger, fetchCertificateBytes?: typeof defaultFetchCertificateBytes, additionalRootCAs?: X509Certificate[]): Promise<void>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import './additional-root-cas.js';
|
|
2
|
+
import { crypto } from "../crypto/index.js";
|
|
3
|
+
import { SUPPORTED_NAMED_CURVE_MAP, SUPPORTED_SIGNATURE_ALGS, SUPPORTED_SIGNATURE_ALGS_MAP } from "./constants.js";
|
|
4
|
+
import { getHash } from "./decryption-utils.js";
|
|
5
|
+
import { areUint8ArraysEqual, asciiToUint8Array, concatenateUint8Arrays } from "./generics.js";
|
|
6
|
+
import { MOZILLA_ROOT_CA_LIST } from "./mozilla-root-cas.js";
|
|
7
|
+
import { expectReadWithLength, packWithLength } from "./packets.js";
|
|
8
|
+
import { defaultFetchCertificateBytes, loadX509FromDer, loadX509FromPem } from "./x509.js";
|
|
9
|
+
const CERT_VERIFY_TXT = asciiToUint8Array('TLS 1.3, server CertificateVerify');
|
|
10
|
+
let ROOT_CAS;
|
|
11
|
+
export function parseCertificates(data, { version }) {
|
|
12
|
+
// context, kina irrelevant
|
|
13
|
+
const ctx = version === 'TLS1_3' ? read(1)[0] : 0;
|
|
14
|
+
// the data itself
|
|
15
|
+
data = readWLength(3);
|
|
16
|
+
const certificates = [];
|
|
17
|
+
while (data.length) {
|
|
18
|
+
// the certificate data
|
|
19
|
+
const cert = readWLength(3);
|
|
20
|
+
const certObj = loadX509FromDer(cert);
|
|
21
|
+
certificates.push(certObj);
|
|
22
|
+
if (version === 'TLS1_3') {
|
|
23
|
+
// extensions
|
|
24
|
+
readWLength(2);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { certificates, ctx };
|
|
28
|
+
function read(bytes) {
|
|
29
|
+
const result = data.slice(0, bytes);
|
|
30
|
+
data = data.slice(bytes);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function readWLength(bytesLength = 2) {
|
|
34
|
+
const content = expectReadWithLength(data, bytesLength);
|
|
35
|
+
data = data.slice(content.length + bytesLength);
|
|
36
|
+
return content;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function parseServerCertificateVerify(data) {
|
|
40
|
+
// data = readWLength(2)
|
|
41
|
+
const algorithmBytes = read(2);
|
|
42
|
+
const algorithm = SUPPORTED_SIGNATURE_ALGS.find(alg => (areUint8ArraysEqual(SUPPORTED_SIGNATURE_ALGS_MAP[alg]
|
|
43
|
+
.identifier, algorithmBytes)));
|
|
44
|
+
if (!algorithm) {
|
|
45
|
+
throw new Error(`Unsupported signature algorithm '${algorithmBytes}'`);
|
|
46
|
+
}
|
|
47
|
+
const signature = readWLength(2);
|
|
48
|
+
return { algorithm, signature };
|
|
49
|
+
function read(bytes) {
|
|
50
|
+
const result = data.slice(0, bytes);
|
|
51
|
+
data = data.slice(bytes);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
function readWLength(bytesLength = 2) {
|
|
55
|
+
const content = expectReadWithLength(data, bytesLength);
|
|
56
|
+
data = data.slice(content.length + bytesLength);
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function verifyCertificateSignature({ signature, algorithm, publicKey, signatureData, }) {
|
|
61
|
+
const { algorithm: cryptoAlg } = SUPPORTED_SIGNATURE_ALGS_MAP[algorithm];
|
|
62
|
+
const pubKey = await crypto.importKey(cryptoAlg, publicKey.buffer, 'public');
|
|
63
|
+
const verified = await crypto.verify(cryptoAlg, {
|
|
64
|
+
data: signatureData,
|
|
65
|
+
signature,
|
|
66
|
+
publicKey: pubKey
|
|
67
|
+
});
|
|
68
|
+
if (!verified) {
|
|
69
|
+
throw new Error(`${algorithm} signature verification failed`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function getSignatureDataTls13(hellos, cipherSuite) {
|
|
73
|
+
const handshakeHash = await getHash(hellos, cipherSuite);
|
|
74
|
+
return concatenateUint8Arrays([
|
|
75
|
+
new Uint8Array(64).fill(0x20),
|
|
76
|
+
CERT_VERIFY_TXT,
|
|
77
|
+
new Uint8Array([0]),
|
|
78
|
+
handshakeHash
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
export async function getSignatureDataTls12({ clientRandom, serverRandom, curveType, publicKey, }) {
|
|
82
|
+
const publicKeyBytes = await crypto.exportKey(publicKey);
|
|
83
|
+
return concatenateUint8Arrays([
|
|
84
|
+
clientRandom,
|
|
85
|
+
serverRandom,
|
|
86
|
+
concatenateUint8Arrays([
|
|
87
|
+
new Uint8Array([3]),
|
|
88
|
+
SUPPORTED_NAMED_CURVE_MAP[curveType].identifier,
|
|
89
|
+
]),
|
|
90
|
+
packWithLength(publicKeyBytes)
|
|
91
|
+
// pub key is packed with 1 byte length
|
|
92
|
+
.slice(1)
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
95
|
+
export async function verifyCertificateChain(chain, host, logger, fetchCertificateBytes = defaultFetchCertificateBytes, additionalRootCAs) {
|
|
96
|
+
const rootCAs = [
|
|
97
|
+
...loadRootCAs(),
|
|
98
|
+
...(additionalRootCAs || [])
|
|
99
|
+
];
|
|
100
|
+
const leaf = chain[0];
|
|
101
|
+
const commonNames = [
|
|
102
|
+
...leaf.getSubjectField('CN'),
|
|
103
|
+
...leaf.getAlternativeDNSNames()
|
|
104
|
+
];
|
|
105
|
+
if (!commonNames.some(cn => matchHostname(host, cn))) {
|
|
106
|
+
throw new Error(`Certificate is not for host ${host}`);
|
|
107
|
+
}
|
|
108
|
+
chain = [...chain]; // clone to allow appending fetched certs
|
|
109
|
+
for (let i = 0; i < chain.length; i++) {
|
|
110
|
+
const cert = chain[i];
|
|
111
|
+
const cn = cert.getSubjectField('CN');
|
|
112
|
+
if (!cert.isWithinValidity()) {
|
|
113
|
+
throw new Error(`Certificate ${cn} (i: ${i}) is outside validity`);
|
|
114
|
+
}
|
|
115
|
+
// look in our chain for issuer
|
|
116
|
+
let issuer = findIssuer(chain.slice(i + 1), cert);
|
|
117
|
+
// if not found, check in our root CAs
|
|
118
|
+
if (!issuer) {
|
|
119
|
+
issuer = findIssuer(rootCAs, cert);
|
|
120
|
+
}
|
|
121
|
+
// if not found, we'll try fetching it via AIA extension
|
|
122
|
+
if (!issuer) {
|
|
123
|
+
const aiaExt = cert.getAIAExtension();
|
|
124
|
+
if (!aiaExt) {
|
|
125
|
+
throw new Error(`Missing issuer for certificate ${cn} (i: ${i})`);
|
|
126
|
+
}
|
|
127
|
+
if (TLS_INTERMEDIATE_CA_CACHE?.[aiaExt]) {
|
|
128
|
+
issuer = TLS_INTERMEDIATE_CA_CACHE[aiaExt];
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
logger.debug({ aiaExt, cn }, 'fetching issuer certificate via AIA extension');
|
|
132
|
+
const bytes = await fetchCertificateBytes(aiaExt);
|
|
133
|
+
issuer = await loadX509FromPem(bytes);
|
|
134
|
+
// we'll need to verify this cert below too
|
|
135
|
+
chain.push(issuer);
|
|
136
|
+
TLS_INTERMEDIATE_CA_CACHE[aiaExt] = issuer;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!issuer.isWithinValidity()) {
|
|
140
|
+
throw new Error(`Issuer Cert ${cn} is not within validity period`);
|
|
141
|
+
}
|
|
142
|
+
if (!(await issuer.verifyIssued(cert))) {
|
|
143
|
+
const icn = issuer.getSubjectField('CN');
|
|
144
|
+
throw new Error(`Verification of ${cn} failed by issuer ${icn} (i: ${i})`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function findIssuer(chain, cert) {
|
|
149
|
+
for (const element of chain) {
|
|
150
|
+
if (element.isIssuer(cert)) {
|
|
151
|
+
return element;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Checks if a hostname matches a common name
|
|
157
|
+
* @param host the hostname, eg. "google.com"
|
|
158
|
+
* @param commonName the common name from the certificate,
|
|
159
|
+
* eg. "*.google.com", "google.com"
|
|
160
|
+
*/
|
|
161
|
+
function matchHostname(host, commonName) {
|
|
162
|
+
// write a regex to match the common name
|
|
163
|
+
// and check if it matches the hostname
|
|
164
|
+
const hostComps = host.split('.');
|
|
165
|
+
const cnComps = commonName.split('.');
|
|
166
|
+
if (cnComps.length !== hostComps.length) {
|
|
167
|
+
// can ignore the first component if it's a wildcard
|
|
168
|
+
if (cnComps[0] === '*'
|
|
169
|
+
&& cnComps.length === hostComps.length + 1) {
|
|
170
|
+
cnComps.shift();
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return hostComps.every((comp, i) => (comp === cnComps[i]
|
|
177
|
+
|| cnComps[i] === '*'));
|
|
178
|
+
}
|
|
179
|
+
function loadRootCAs() {
|
|
180
|
+
if (ROOT_CAS) {
|
|
181
|
+
return ROOT_CAS;
|
|
182
|
+
}
|
|
183
|
+
ROOT_CAS = MOZILLA_ROOT_CA_LIST.map(loadX509FromPem);
|
|
184
|
+
if (typeof TLS_ADDITIONAL_ROOT_CA_LIST !== 'undefined') {
|
|
185
|
+
ROOT_CAS.push(...TLS_ADDITIONAL_ROOT_CA_LIST.map(loadX509FromPem));
|
|
186
|
+
}
|
|
187
|
+
return ROOT_CAS;
|
|
188
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a full client hello message
|
|
3
|
+
*/
|
|
4
|
+
export declare function parseClientHello(data: Uint8Array): {
|
|
5
|
+
version: "TLS1_3" | "TLS1_2";
|
|
6
|
+
serverRandom: Uint8Array<ArrayBuffer>;
|
|
7
|
+
sessionId: Uint8Array<ArrayBuffer>;
|
|
8
|
+
cipherSuitesBytes: Uint8Array<ArrayBuffer>;
|
|
9
|
+
compressionMethodByte: number;
|
|
10
|
+
extensions: Partial<import("../index.ts").SupportedExtensionClientData>;
|
|
11
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SUPPORTED_RECORD_TYPE_MAP } from "./constants.js";
|
|
2
|
+
import { getTlsVersionFromBytes } from "./generics.js";
|
|
3
|
+
import { expectReadWithLength } from "./packets.js";
|
|
4
|
+
import { parseClientExtensions } from "./parse-extensions.js";
|
|
5
|
+
/**
|
|
6
|
+
* Parse a full client hello message
|
|
7
|
+
*/
|
|
8
|
+
export function parseClientHello(data) {
|
|
9
|
+
const packetType = read(1)[0];
|
|
10
|
+
if (packetType !== SUPPORTED_RECORD_TYPE_MAP.CLIENT_HELLO) {
|
|
11
|
+
throw new Error(`Invalid record type for client hello (${packetType})`);
|
|
12
|
+
}
|
|
13
|
+
data = readWLength(3);
|
|
14
|
+
const versionBytes = read(2);
|
|
15
|
+
const version = getTlsVersionFromBytes(versionBytes);
|
|
16
|
+
const serverRandom = read(32);
|
|
17
|
+
const sessionId = readWLength(1);
|
|
18
|
+
const cipherSuitesBytes = readWLength(2);
|
|
19
|
+
const compressionMethodByte = readWLength(1)[0];
|
|
20
|
+
const extensions = parseClientExtensions(data);
|
|
21
|
+
return {
|
|
22
|
+
version,
|
|
23
|
+
serverRandom,
|
|
24
|
+
sessionId,
|
|
25
|
+
cipherSuitesBytes,
|
|
26
|
+
compressionMethodByte,
|
|
27
|
+
extensions,
|
|
28
|
+
};
|
|
29
|
+
function read(bytes) {
|
|
30
|
+
const result = data.slice(0, bytes);
|
|
31
|
+
data = data.slice(bytes);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function readWLength(bytesLength = 2) {
|
|
35
|
+
const content = expectReadWithLength(data, bytesLength);
|
|
36
|
+
data = data.slice(content.length + bytesLength);
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SupportedExtensionClientData, SupportedExtensionServerData } from '../types/index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a length-encoded list of extensions
|
|
4
|
+
* sent by the server
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseServerExtensions(data: Uint8Array): Partial<SupportedExtensionServerData>;
|
|
7
|
+
/**
|
|
8
|
+
* Parse a length-encoded list of extensions
|
|
9
|
+
* sent by the client
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseClientExtensions(data: Uint8Array): Partial<SupportedExtensionClientData>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { SUPPORTED_EXTENSION_MAP, SUPPORTED_EXTENSIONS, SUPPORTED_NAMED_CURVE_MAP, SUPPORTED_NAMED_CURVES } from "./constants.js";
|
|
2
|
+
import { areUint8ArraysEqual, getTlsVersionFromBytes, uint8ArrayToBinaryStr } from "./generics.js";
|
|
3
|
+
import { expectReadWithLength } from "./packets.js";
|
|
4
|
+
/**
|
|
5
|
+
* Parse a length-encoded list of extensions
|
|
6
|
+
* sent by the server
|
|
7
|
+
*/
|
|
8
|
+
export function parseServerExtensions(data) {
|
|
9
|
+
return parseExtensions(data, {
|
|
10
|
+
'ALPN': (extData) => {
|
|
11
|
+
const data = expectReadWithLength(extData);
|
|
12
|
+
const alpnBytes = expectReadWithLength(data, 1);
|
|
13
|
+
return uint8ArrayToBinaryStr(alpnBytes);
|
|
14
|
+
},
|
|
15
|
+
'SUPPORTED_VERSIONS': getTlsVersionFromBytes,
|
|
16
|
+
'PRE_SHARED_KEY': () => ({ supported: true }),
|
|
17
|
+
'KEY_SHARE': (extData) => {
|
|
18
|
+
const typeBytes = extData.slice(0, 2);
|
|
19
|
+
const type = SUPPORTED_NAMED_CURVES
|
|
20
|
+
.find(k => areUint8ArraysEqual(SUPPORTED_NAMED_CURVE_MAP[k].identifier, typeBytes));
|
|
21
|
+
if (!type) {
|
|
22
|
+
throw new Error(`Unsupported key type '${typeBytes}'`);
|
|
23
|
+
}
|
|
24
|
+
const publicKey = expectReadWithLength(extData.slice(2));
|
|
25
|
+
return { type, publicKey };
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse a length-encoded list of extensions
|
|
31
|
+
* sent by the client
|
|
32
|
+
*/
|
|
33
|
+
export function parseClientExtensions(data) {
|
|
34
|
+
return parseExtensions(data, {
|
|
35
|
+
'SERVER_NAME': (extData) => {
|
|
36
|
+
extData = expectReadWithLength(extData);
|
|
37
|
+
const byte = extData[0];
|
|
38
|
+
extData = extData.slice(1);
|
|
39
|
+
const serverNameBytes = expectReadWithLength(extData);
|
|
40
|
+
return {
|
|
41
|
+
type: byte,
|
|
42
|
+
serverName: uint8ArrayToBinaryStr(serverNameBytes)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function parseExtensions(data, parsers) {
|
|
48
|
+
data = readWLength(2);
|
|
49
|
+
const map = {};
|
|
50
|
+
const seenExtensions = new Set();
|
|
51
|
+
while (data.length) {
|
|
52
|
+
const typeByte = read(2)[1];
|
|
53
|
+
const extData = readWLength(2);
|
|
54
|
+
const type = SUPPORTED_EXTENSIONS
|
|
55
|
+
.find(k => SUPPORTED_EXTENSION_MAP[k] === typeByte);
|
|
56
|
+
if (seenExtensions.has(typeByte)) {
|
|
57
|
+
throw new Error(`Duplicate extension '${type}' (${typeByte})`);
|
|
58
|
+
}
|
|
59
|
+
if (type && type in parsers) {
|
|
60
|
+
map[type] = parsers[type](extData);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return map;
|
|
64
|
+
function read(bytes) {
|
|
65
|
+
const result = data.slice(0, bytes);
|
|
66
|
+
data = data.slice(bytes);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function readWLength(bytesLength = 2) {
|
|
70
|
+
const content = expectReadWithLength(data, bytesLength);
|
|
71
|
+
data = data.slice(content.length + bytesLength);
|
|
72
|
+
return content;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function parseServerHello(data: Uint8Array): Promise<{
|
|
2
|
+
publicKey?: unknown;
|
|
3
|
+
publicKeyType?: "X25519" | "SECP256R1" | "SECP384R1" | undefined;
|
|
4
|
+
serverTlsVersion: "TLS1_3" | "TLS1_2";
|
|
5
|
+
serverRandom: Uint8Array<ArrayBuffer>;
|
|
6
|
+
sessionId: Uint8Array<ArrayBuffer>;
|
|
7
|
+
cipherSuite: "TLS_CHACHA20_POLY1305_SHA256" | "TLS_AES_256_GCM_SHA384" | "TLS_AES_128_GCM_SHA256" | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" | "TLS_RSA_WITH_AES_128_GCM_SHA256" | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA";
|
|
8
|
+
supportsPsk: boolean;
|
|
9
|
+
extensions: Partial<import("../index.ts").SupportedExtensionServerData>;
|
|
10
|
+
}>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { crypto } from "../crypto/index.js";
|
|
2
|
+
import { SUPPORTED_CIPHER_SUITE_MAP, SUPPORTED_CIPHER_SUITES, SUPPORTED_NAMED_CURVE_MAP } from "./constants.js";
|
|
3
|
+
import { areUint8ArraysEqual } from "./generics.js";
|
|
4
|
+
import { expectReadWithLength } from "./packets.js";
|
|
5
|
+
import { parseServerExtensions } from "./parse-extensions.js";
|
|
6
|
+
export async function parseServerHello(data) {
|
|
7
|
+
// header TLS version (expected to be 0x0303)
|
|
8
|
+
read(2);
|
|
9
|
+
const serverRandom = read(32);
|
|
10
|
+
const sessionId = readWLength(1);
|
|
11
|
+
const cipherSuiteBytes = read(2);
|
|
12
|
+
const cipherSuite = SUPPORTED_CIPHER_SUITES
|
|
13
|
+
.find(k => areUint8ArraysEqual(SUPPORTED_CIPHER_SUITE_MAP[k].identifier, cipherSuiteBytes));
|
|
14
|
+
if (!cipherSuite) {
|
|
15
|
+
throw new Error(`Unsupported cipher suite '${cipherSuiteBytes}'`);
|
|
16
|
+
}
|
|
17
|
+
const compressionMethod = read(1)[0];
|
|
18
|
+
if (compressionMethod !== 0x00) {
|
|
19
|
+
throw new Error(`Unsupported compression method '${compressionMethod.toString(16)}'`);
|
|
20
|
+
}
|
|
21
|
+
const extensions = parseServerExtensions(data);
|
|
22
|
+
const serverTlsVersion = extensions['SUPPORTED_VERSIONS'] || 'TLS1_2';
|
|
23
|
+
const pubKeyExt = extensions['KEY_SHARE'];
|
|
24
|
+
if (serverTlsVersion === 'TLS1_3'
|
|
25
|
+
&& !pubKeyExt) {
|
|
26
|
+
throw new Error('Missing key share in TLS 1.3');
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
serverTlsVersion,
|
|
30
|
+
serverRandom,
|
|
31
|
+
sessionId,
|
|
32
|
+
cipherSuite,
|
|
33
|
+
supportsPsk: !!extensions['PRE_SHARED_KEY']?.supported,
|
|
34
|
+
extensions,
|
|
35
|
+
...(pubKeyExt
|
|
36
|
+
? {
|
|
37
|
+
publicKey: await crypto.importKey(SUPPORTED_NAMED_CURVE_MAP[pubKeyExt.type].algorithm, pubKeyExt.publicKey, 'public'),
|
|
38
|
+
publicKeyType: pubKeyExt.type,
|
|
39
|
+
}
|
|
40
|
+
: {})
|
|
41
|
+
};
|
|
42
|
+
function read(bytes) {
|
|
43
|
+
const result = data.slice(0, bytes);
|
|
44
|
+
data = data.slice(bytes);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
function readWLength(bytesLength = 2) {
|
|
48
|
+
const content = expectReadWithLength(data, bytesLength);
|
|
49
|
+
data = data.slice(content.length + bytesLength);
|
|
50
|
+
return content;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CipherSuite, TLSSessionTicket } from '../types/index.ts';
|
|
2
|
+
type GetResumableSessionTicketOptions = {
|
|
3
|
+
masterKey: Uint8Array;
|
|
4
|
+
/** hello msgs without record header */
|
|
5
|
+
hellos: Uint8Array[] | Uint8Array;
|
|
6
|
+
cipherSuite: CipherSuite;
|
|
7
|
+
};
|
|
8
|
+
export declare function parseSessionTicket(data: Uint8Array): TLSSessionTicket;
|
|
9
|
+
export declare function getPskFromTicket(ticket: TLSSessionTicket, { masterKey, hellos, cipherSuite }: GetResumableSessionTicketOptions): Promise<{
|
|
10
|
+
identity: Uint8Array<ArrayBufferLike>;
|
|
11
|
+
ticketAge: number;
|
|
12
|
+
finishKey: unknown;
|
|
13
|
+
resumeMasterSecret: Uint8Array<ArrayBuffer>;
|
|
14
|
+
earlySecret: Uint8Array<ArrayBufferLike>;
|
|
15
|
+
cipherSuite: "TLS_CHACHA20_POLY1305_SHA256" | "TLS_AES_256_GCM_SHA384" | "TLS_AES_128_GCM_SHA256" | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" | "TLS_RSA_WITH_AES_128_GCM_SHA256" | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA";
|
|
16
|
+
}>;
|
|
17
|
+
export {};
|