@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 { crypto } from "../crypto/index.js";
|
|
2
|
+
import { getHash, hkdfExtractAndExpandLabel } from "../utils/decryption-utils.js";
|
|
3
|
+
import { SUPPORTED_CIPHER_SUITE_MAP } from "./constants.js";
|
|
4
|
+
import { uint8ArrayToDataView } from "./generics.js";
|
|
5
|
+
import { expectReadWithLength } from "./packets.js";
|
|
6
|
+
export function parseSessionTicket(data) {
|
|
7
|
+
const lifetimeS = read(4).getUint32(0);
|
|
8
|
+
const ticketAgeAddMs = read(4).getUint32(0);
|
|
9
|
+
const nonce = readWLength(1);
|
|
10
|
+
const ticket = readWLength(2);
|
|
11
|
+
const extensions = readWLength(2);
|
|
12
|
+
const sessionTicket = {
|
|
13
|
+
ticket,
|
|
14
|
+
lifetimeS,
|
|
15
|
+
ticketAgeAddMs,
|
|
16
|
+
nonce,
|
|
17
|
+
expiresAt: new Date(Date.now() + lifetimeS * 1000),
|
|
18
|
+
extensions
|
|
19
|
+
};
|
|
20
|
+
return sessionTicket;
|
|
21
|
+
function read(bytes) {
|
|
22
|
+
const result = data.slice(0, bytes);
|
|
23
|
+
data = data.slice(bytes);
|
|
24
|
+
return uint8ArrayToDataView(result);
|
|
25
|
+
}
|
|
26
|
+
function readWLength(bytesLength = 2) {
|
|
27
|
+
const content = expectReadWithLength(data, bytesLength);
|
|
28
|
+
data = data.slice(content.length + bytesLength);
|
|
29
|
+
return content;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function getPskFromTicket(ticket, { masterKey, hellos, cipherSuite }) {
|
|
33
|
+
const { hashAlgorithm, hashLength } = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite];
|
|
34
|
+
const handshakeHash = await getHash(hellos, cipherSuite);
|
|
35
|
+
const resumeMasterSecret = await hkdfExtractAndExpandLabel(hashAlgorithm, masterKey, 'res master', handshakeHash, hashLength);
|
|
36
|
+
const psk = await hkdfExtractAndExpandLabel(hashAlgorithm, resumeMasterSecret, 'resumption', ticket.nonce, hashLength);
|
|
37
|
+
const emptyHash = await crypto.hash(hashAlgorithm, new Uint8Array());
|
|
38
|
+
const earlySecret = await crypto.extract(hashAlgorithm, hashLength, psk, '');
|
|
39
|
+
const binderKey = await hkdfExtractAndExpandLabel(hashAlgorithm, earlySecret, 'res binder', emptyHash, hashLength);
|
|
40
|
+
// const clientEarlyTrafficSecret = hkdfExtractAndExpandLabel(hashAlgorithm, earlySecret, 'c e traffic', new Uint8Array(), hashLength)
|
|
41
|
+
const finishKey = await hkdfExtractAndExpandLabel(hashAlgorithm, binderKey, 'finished', new Uint8Array(), hashLength);
|
|
42
|
+
const ticketAge = Math.floor(ticket.lifetimeS / 1000 + ticket.ticketAgeAddMs);
|
|
43
|
+
return {
|
|
44
|
+
identity: ticket.ticket,
|
|
45
|
+
ticketAge,
|
|
46
|
+
finishKey: await crypto.importKey(hashAlgorithm, finishKey),
|
|
47
|
+
resumeMasterSecret,
|
|
48
|
+
earlySecret,
|
|
49
|
+
cipherSuite,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CipherSuite, Key, TLSProtocolVersion } from '../types/index.ts';
|
|
2
|
+
import type { PacketHeaderOptions } from './packets.ts';
|
|
3
|
+
type WrappedRecordMacGenOptions = {
|
|
4
|
+
macKey?: Key;
|
|
5
|
+
recordNumber: number | undefined;
|
|
6
|
+
cipherSuite: CipherSuite;
|
|
7
|
+
version: TLSProtocolVersion;
|
|
8
|
+
} & ({
|
|
9
|
+
recordHeaderOpts: PacketHeaderOptions;
|
|
10
|
+
} | {
|
|
11
|
+
recordHeader: Uint8Array;
|
|
12
|
+
});
|
|
13
|
+
type WrappedRecordCipherOptions = {
|
|
14
|
+
iv: Uint8Array;
|
|
15
|
+
key: Key;
|
|
16
|
+
} & WrappedRecordMacGenOptions;
|
|
17
|
+
export declare function decryptWrappedRecord(encryptedData: Uint8Array, opts: WrappedRecordCipherOptions): Promise<{
|
|
18
|
+
plaintext: Uint8Array<ArrayBufferLike>;
|
|
19
|
+
iv: Uint8Array<ArrayBufferLike>;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function encryptWrappedRecord(plaintext: Uint8Array, opts: WrappedRecordCipherOptions): Promise<{
|
|
22
|
+
ciphertext: Uint8Array<ArrayBufferLike>;
|
|
23
|
+
iv: Uint8Array<ArrayBufferLike>;
|
|
24
|
+
}>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { crypto } from "../crypto/index.js";
|
|
2
|
+
import { AUTH_TAG_BYTE_LENGTH, SUPPORTED_CIPHER_SUITE_MAP } from "./constants.js";
|
|
3
|
+
import { areUint8ArraysEqual, concatenateUint8Arrays, generateIV, isSymmetricCipher, padTls, toHexStringWithWhitespace, uint8ArrayToDataView, unpadTls } from "./generics.js";
|
|
4
|
+
import { packPacketHeader } from "./packets.js";
|
|
5
|
+
const AUTH_CIPHER_LENGTH = 12;
|
|
6
|
+
export async function decryptWrappedRecord(encryptedData, opts) {
|
|
7
|
+
if (!('recordHeader' in opts)) {
|
|
8
|
+
throw new Error('recordHeader is required for decrypt');
|
|
9
|
+
}
|
|
10
|
+
const { key, recordNumber, cipherSuite, } = opts;
|
|
11
|
+
const { cipher, hashLength } = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite];
|
|
12
|
+
return isSymmetricCipher(cipher)
|
|
13
|
+
? doCipherDecrypt(cipher)
|
|
14
|
+
: doAuthCipherDecrypt(cipher);
|
|
15
|
+
async function doCipherDecrypt(cipher) {
|
|
16
|
+
const iv = encryptedData.slice(0, 16);
|
|
17
|
+
const ciphertext = encryptedData.slice(16);
|
|
18
|
+
let plaintextAndMac = await crypto.decrypt(cipher, {
|
|
19
|
+
key,
|
|
20
|
+
iv,
|
|
21
|
+
data: ciphertext,
|
|
22
|
+
});
|
|
23
|
+
plaintextAndMac = unpadTls(plaintextAndMac);
|
|
24
|
+
plaintextAndMac = plaintextAndMac.slice(0, -1);
|
|
25
|
+
const mac = plaintextAndMac.slice(-hashLength);
|
|
26
|
+
const plaintext = plaintextAndMac.slice(0, -hashLength);
|
|
27
|
+
const macComputed = await computeMacTls12(plaintext, opts);
|
|
28
|
+
if (!areUint8ArraysEqual(mac, macComputed)) {
|
|
29
|
+
throw new Error(`MAC mismatch: expected ${toHexStringWithWhitespace(macComputed)}, got ${toHexStringWithWhitespace(mac)}`);
|
|
30
|
+
}
|
|
31
|
+
return { plaintext, iv };
|
|
32
|
+
}
|
|
33
|
+
async function doAuthCipherDecrypt(cipher) {
|
|
34
|
+
let iv = opts.iv;
|
|
35
|
+
const recordIvLength = AUTH_CIPHER_LENGTH - iv.length;
|
|
36
|
+
if (recordIvLength) {
|
|
37
|
+
// const recordIv = new Uint8Array(recordIvLength)
|
|
38
|
+
// const seqNumberView = uint8ArrayToDataView(recordIv)
|
|
39
|
+
// seqNumberView.setUint32(recordIvLength - 4, recordNumber)
|
|
40
|
+
const recordIv = encryptedData.slice(0, recordIvLength);
|
|
41
|
+
encryptedData = encryptedData.slice(recordIvLength);
|
|
42
|
+
iv = concatenateUint8Arrays([
|
|
43
|
+
iv,
|
|
44
|
+
recordIv
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
else if (
|
|
48
|
+
// use IV generation alg for TLS 1.3
|
|
49
|
+
// and ChaCha20-Poly1305
|
|
50
|
+
(opts.version === 'TLS1_3'
|
|
51
|
+
|| cipher === 'CHACHA20-POLY1305') && typeof recordNumber !== 'undefined') {
|
|
52
|
+
iv = generateIV(iv, recordNumber);
|
|
53
|
+
}
|
|
54
|
+
const authTag = encryptedData.slice(-AUTH_TAG_BYTE_LENGTH);
|
|
55
|
+
encryptedData = encryptedData.slice(0, -AUTH_TAG_BYTE_LENGTH);
|
|
56
|
+
const aead = getAead(encryptedData.length, opts);
|
|
57
|
+
const { plaintext } = await crypto.authenticatedDecrypt(cipher, {
|
|
58
|
+
key,
|
|
59
|
+
iv,
|
|
60
|
+
data: encryptedData,
|
|
61
|
+
aead,
|
|
62
|
+
authTag,
|
|
63
|
+
});
|
|
64
|
+
if (plaintext.length !== encryptedData.length) {
|
|
65
|
+
throw new Error('Decrypted length does not match encrypted length');
|
|
66
|
+
}
|
|
67
|
+
return { plaintext, iv };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function encryptWrappedRecord(plaintext, opts) {
|
|
71
|
+
const { key, recordNumber, cipherSuite, } = opts;
|
|
72
|
+
const { cipher } = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite];
|
|
73
|
+
let iv = opts.iv;
|
|
74
|
+
return isSymmetricCipher(cipher)
|
|
75
|
+
? doSymmetricEncrypt(cipher)
|
|
76
|
+
: doAuthSymmetricEncrypt(cipher);
|
|
77
|
+
async function doAuthSymmetricEncrypt(cipher) {
|
|
78
|
+
const aead = getAead(plaintext.length, opts);
|
|
79
|
+
// record IV is the record number as a 64-bit big-endian integer
|
|
80
|
+
const recordIvLength = AUTH_CIPHER_LENGTH - iv.length;
|
|
81
|
+
let recordIv;
|
|
82
|
+
let completeIv = iv;
|
|
83
|
+
if (recordIvLength && typeof recordNumber !== 'undefined') {
|
|
84
|
+
recordIv = new Uint8Array(recordIvLength);
|
|
85
|
+
const seqNumberView = uint8ArrayToDataView(recordIv);
|
|
86
|
+
seqNumberView.setUint32(recordIvLength - 4, recordNumber);
|
|
87
|
+
completeIv = concatenateUint8Arrays([
|
|
88
|
+
iv,
|
|
89
|
+
recordIv
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
else if (
|
|
93
|
+
// use IV generation alg for TLS 1.3
|
|
94
|
+
// and ChaCha20-Poly1305
|
|
95
|
+
(opts.version === 'TLS1_3'
|
|
96
|
+
|| cipher === 'CHACHA20-POLY1305')
|
|
97
|
+
&& typeof recordNumber !== 'undefined') {
|
|
98
|
+
completeIv = generateIV(completeIv, recordNumber);
|
|
99
|
+
}
|
|
100
|
+
const enc = await crypto.authenticatedEncrypt(cipher, {
|
|
101
|
+
key,
|
|
102
|
+
iv: completeIv,
|
|
103
|
+
data: plaintext,
|
|
104
|
+
aead,
|
|
105
|
+
});
|
|
106
|
+
if (recordIv) {
|
|
107
|
+
enc.ciphertext = concatenateUint8Arrays([
|
|
108
|
+
recordIv,
|
|
109
|
+
enc.ciphertext,
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
ciphertext: concatenateUint8Arrays([
|
|
114
|
+
enc.ciphertext,
|
|
115
|
+
enc.authTag,
|
|
116
|
+
]),
|
|
117
|
+
iv: completeIv
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function doSymmetricEncrypt(cipher) {
|
|
121
|
+
const blockSize = 16;
|
|
122
|
+
iv = padBytes(opts.iv, 16).slice(0, 16);
|
|
123
|
+
const mac = await computeMacTls12(plaintext, opts);
|
|
124
|
+
const completeData = concatenateUint8Arrays([
|
|
125
|
+
plaintext,
|
|
126
|
+
mac,
|
|
127
|
+
]);
|
|
128
|
+
// add TLS's special padding :(
|
|
129
|
+
const padded = padTls(completeData, blockSize);
|
|
130
|
+
const result = await crypto.encrypt(cipher, { key, iv, data: padded });
|
|
131
|
+
return {
|
|
132
|
+
ciphertext: concatenateUint8Arrays([
|
|
133
|
+
iv,
|
|
134
|
+
result
|
|
135
|
+
]),
|
|
136
|
+
iv,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function padBytes(arr, len) {
|
|
140
|
+
const returnVal = new Uint8Array(len);
|
|
141
|
+
returnVal.set(arr, len - arr.length);
|
|
142
|
+
return returnVal;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function getAead(plaintextLength, opts) {
|
|
146
|
+
const isTls13 = opts.version === 'TLS1_3';
|
|
147
|
+
let aead;
|
|
148
|
+
if (isTls13) {
|
|
149
|
+
const dataLen = plaintextLength + AUTH_TAG_BYTE_LENGTH;
|
|
150
|
+
const recordHeader = 'recordHeaderOpts' in opts
|
|
151
|
+
? packPacketHeader(dataLen, opts.recordHeaderOpts)
|
|
152
|
+
: replaceRecordHeaderLen(opts.recordHeader, dataLen);
|
|
153
|
+
aead = recordHeader;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
aead = getTls12Header(plaintextLength, opts);
|
|
157
|
+
}
|
|
158
|
+
return aead;
|
|
159
|
+
}
|
|
160
|
+
function getTls12Header(plaintextLength, opts) {
|
|
161
|
+
const { recordNumber } = opts;
|
|
162
|
+
const recordHeader = 'recordHeaderOpts' in opts
|
|
163
|
+
? packPacketHeader(plaintextLength, opts.recordHeaderOpts)
|
|
164
|
+
: replaceRecordHeaderLen(opts.recordHeader, plaintextLength);
|
|
165
|
+
const seqNumberBytes = new Uint8Array(8);
|
|
166
|
+
const seqNumberView = uint8ArrayToDataView(seqNumberBytes);
|
|
167
|
+
seqNumberView.setUint32(4, recordNumber || 0);
|
|
168
|
+
return concatenateUint8Arrays([
|
|
169
|
+
seqNumberBytes,
|
|
170
|
+
recordHeader,
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
async function computeMacTls12(plaintext, opts) {
|
|
174
|
+
const { macKey, cipherSuite } = opts;
|
|
175
|
+
if (!macKey) {
|
|
176
|
+
throw new Error('macKey is required for non-AEAD cipher');
|
|
177
|
+
}
|
|
178
|
+
const { hashAlgorithm } = SUPPORTED_CIPHER_SUITE_MAP[cipherSuite];
|
|
179
|
+
const dataToSign = concatenateUint8Arrays([
|
|
180
|
+
getTls12Header(plaintext.length, opts),
|
|
181
|
+
plaintext,
|
|
182
|
+
]);
|
|
183
|
+
const mac = await crypto.hmac(hashAlgorithm, macKey, dataToSign);
|
|
184
|
+
return mac;
|
|
185
|
+
}
|
|
186
|
+
function replaceRecordHeaderLen(header, newLength) {
|
|
187
|
+
const newRecordHeader = new Uint8Array(header);
|
|
188
|
+
const dataView = uint8ArrayToDataView(newRecordHeader);
|
|
189
|
+
dataView.setUint16(3, newLength);
|
|
190
|
+
return newRecordHeader;
|
|
191
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as peculiar from '@peculiar/x509';
|
|
2
|
+
import type { X509Certificate } from '../types/index.ts';
|
|
3
|
+
export declare function loadX509FromPem(pem: string | Uint8Array): X509Certificate<peculiar.X509Certificate>;
|
|
4
|
+
export declare function loadX509FromDer(der: Uint8Array): X509Certificate<peculiar.X509Certificate>;
|
|
5
|
+
export declare function defaultFetchCertificateBytes(url: string): Promise<Uint8Array<any>>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as peculiar from '@peculiar/x509';
|
|
2
|
+
import { SubjectAlternativeNameExtension } from '@peculiar/x509';
|
|
3
|
+
import { crypto } from "../crypto/index.js";
|
|
4
|
+
const AIA_EXT_TYPE = '1.3.6.1.5.5.7.1.1';
|
|
5
|
+
export function loadX509FromPem(pem) {
|
|
6
|
+
let cert;
|
|
7
|
+
try {
|
|
8
|
+
cert = new peculiar.X509Certificate(pem);
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
throw new Error(`Unsupported certificate: ${e}`);
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
internal: cert,
|
|
15
|
+
isWithinValidity() {
|
|
16
|
+
const now = new Date();
|
|
17
|
+
return now > cert.notBefore && now < cert.notAfter;
|
|
18
|
+
},
|
|
19
|
+
getAIAExtension() {
|
|
20
|
+
const aiaExt = cert
|
|
21
|
+
.getExtension(AIA_EXT_TYPE);
|
|
22
|
+
return aiaExt?.caIssuers?.find(obj => obj.type === 'url')?.value;
|
|
23
|
+
},
|
|
24
|
+
getSubjectField(name) {
|
|
25
|
+
return cert.subjectName.getField(name);
|
|
26
|
+
},
|
|
27
|
+
getAlternativeDNSNames() {
|
|
28
|
+
// search for names in SubjectAlternativeNameExtension
|
|
29
|
+
const ext = cert.extensions
|
|
30
|
+
.find(e => e.type === '2.5.29.17'); //subjectAltName
|
|
31
|
+
if (ext instanceof SubjectAlternativeNameExtension) {
|
|
32
|
+
return ext.names.items
|
|
33
|
+
.filter(n => n.type === 'dns')
|
|
34
|
+
.map(n => n.value);
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
},
|
|
38
|
+
isIssuer({ internal: ofCert }) {
|
|
39
|
+
var i = ofCert.issuer;
|
|
40
|
+
var s = cert.subject;
|
|
41
|
+
return i === s;
|
|
42
|
+
},
|
|
43
|
+
getPublicKey() {
|
|
44
|
+
return {
|
|
45
|
+
buffer: new Uint8Array(cert.publicKey.rawData),
|
|
46
|
+
algorithm: cert.publicKey.algorithm.name,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
async verifyIssued(otherCert) {
|
|
50
|
+
const sigAlg = getSigAlgorithm(cert.publicKey, otherCert.internal);
|
|
51
|
+
const impPublicKey = await crypto
|
|
52
|
+
.importKey(sigAlg, new Uint8Array(cert.publicKey.rawData), 'public');
|
|
53
|
+
const signature = new Uint8Array(otherCert.internal.signature);
|
|
54
|
+
const verified = await crypto.verify(sigAlg, {
|
|
55
|
+
publicKey: impPublicKey,
|
|
56
|
+
signature,
|
|
57
|
+
data: new Uint8Array(otherCert.internal['tbs'])
|
|
58
|
+
});
|
|
59
|
+
return verified;
|
|
60
|
+
},
|
|
61
|
+
serialiseToPem() {
|
|
62
|
+
return cert.toString('pem');
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function getSigAlgorithm(key, { signatureAlgorithm }) {
|
|
67
|
+
if (!('name' in signatureAlgorithm)) {
|
|
68
|
+
throw new Error('Missing signature algorithm name');
|
|
69
|
+
}
|
|
70
|
+
const { name, hash } = signatureAlgorithm;
|
|
71
|
+
const { algorithm: keyAlg } = key;
|
|
72
|
+
if (keyAlg.name !== name) {
|
|
73
|
+
throw new Error(`Signature algorithm ${name} does not match`
|
|
74
|
+
+ ` public key algorithm ${keyAlg.name}`);
|
|
75
|
+
}
|
|
76
|
+
let hashName;
|
|
77
|
+
switch (hash.name) {
|
|
78
|
+
case 'SHA-256':
|
|
79
|
+
hashName = 'SHA256';
|
|
80
|
+
break;
|
|
81
|
+
case 'SHA-384':
|
|
82
|
+
hashName = 'SHA384';
|
|
83
|
+
break;
|
|
84
|
+
case 'SHA-512':
|
|
85
|
+
hashName = 'SHA512';
|
|
86
|
+
break;
|
|
87
|
+
case 'SHA-1':
|
|
88
|
+
hashName = 'SHA1';
|
|
89
|
+
break;
|
|
90
|
+
default:
|
|
91
|
+
throw new Error(`Unsupported hash algorithm: ${hash.name}`);
|
|
92
|
+
}
|
|
93
|
+
switch (name) {
|
|
94
|
+
case 'RSASSA-PKCS1-v1_5':
|
|
95
|
+
case 'RSA-PKCS1-SHA1':
|
|
96
|
+
return `RSA-PKCS1-${hashName}`;
|
|
97
|
+
case 'ECDSA':
|
|
98
|
+
if (hashName === 'SHA512' || hashName === 'SHA1') {
|
|
99
|
+
throw new Error(`Unsupported hash algorithm for ECDSA: ${hashName}`);
|
|
100
|
+
}
|
|
101
|
+
switch (keyAlg.namedCurve) {
|
|
102
|
+
case 'P-256':
|
|
103
|
+
return `ECDSA-SECP256R1-${hashName}`;
|
|
104
|
+
case 'P-384':
|
|
105
|
+
return `ECDSA-SECP384R1-${hashName}`;
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Unsupported named curve: ${keyAlg.namedCurve}`);
|
|
108
|
+
}
|
|
109
|
+
default:
|
|
110
|
+
throw new Error(`Unsupported signature algorithm: ${name}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function loadX509FromDer(der) {
|
|
114
|
+
// peculiar handles both
|
|
115
|
+
return loadX509FromPem(der);
|
|
116
|
+
}
|
|
117
|
+
export async function defaultFetchCertificateBytes(url) {
|
|
118
|
+
const res = await fetch(url);
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
throw new Error(`Failed to fetch certificate from ${url}: ${res.statusText}`);
|
|
121
|
+
}
|
|
122
|
+
const buffer = await res.arrayBuffer();
|
|
123
|
+
return new Uint8Array(buffer);
|
|
124
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joclaim/tls",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TLS 1.2/1.3 for any JavaScript Environment",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./lib/index.d.ts",
|
|
9
|
+
"default": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./webcrypto": {
|
|
12
|
+
"types": "./lib/crypto/webcrypto.d.ts",
|
|
13
|
+
"default": "./lib/crypto/webcrypto.js"
|
|
14
|
+
},
|
|
15
|
+
"./purejs-crypto": {
|
|
16
|
+
"types": "./lib/crypto/pure-js.d.ts",
|
|
17
|
+
"default": "./lib/crypto/pure-js.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "npm exec tsc -- -p tsconfig.build.json",
|
|
22
|
+
"build:jsc": "npm run run:tsc src/scripts/build-jsc.ts",
|
|
23
|
+
"run:tsc": "node --experimental-strip-types",
|
|
24
|
+
"test:pure-js": "npm run run:tsc -- --import=./src/tests/load-purejs-crypto.ts --test",
|
|
25
|
+
"test:webcrypto": "npm run run:tsc -- --import=./src/tests/load-webcrypto.ts --test",
|
|
26
|
+
"handshake": "npm run run:tsc -- --import=./src/tests/load-webcrypto.ts ./src/scripts/handshake.ts",
|
|
27
|
+
"lint": "eslint . --ext .js,.ts,.jsx,.tsx",
|
|
28
|
+
"lint:fix": "eslint . --fix --ext .js,.ts,.jsx,.tsx",
|
|
29
|
+
"prepare": "npm run build",
|
|
30
|
+
"commitlint": "commitlint --edit",
|
|
31
|
+
"update:root-ca": "npm run run:tsc -- src/scripts/update-ca-certs.ts"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"tls",
|
|
35
|
+
"webcrypto",
|
|
36
|
+
"cross-platform",
|
|
37
|
+
"cryptography",
|
|
38
|
+
"network-security",
|
|
39
|
+
"encryption",
|
|
40
|
+
"reclaim-protocol",
|
|
41
|
+
"asn1",
|
|
42
|
+
"x509",
|
|
43
|
+
"chacha20poly1305",
|
|
44
|
+
"typescript",
|
|
45
|
+
"nodejs",
|
|
46
|
+
"web-security",
|
|
47
|
+
"certificate-handling",
|
|
48
|
+
"secure-communication"
|
|
49
|
+
],
|
|
50
|
+
"author": "Adhiraj Singh",
|
|
51
|
+
"license": "See License in <https://github.com/reclaimprotocol/.github/blob/main/LICENSE>",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/reclaimprotocol/tls/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/reclaimprotocol/tls/",
|
|
56
|
+
"files": [
|
|
57
|
+
"lib/*"
|
|
58
|
+
],
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@noble/ciphers": "^1.3.0",
|
|
61
|
+
"@noble/curves": "^1.9.6",
|
|
62
|
+
"@noble/hashes": "^1.8.0",
|
|
63
|
+
"@peculiar/asn1-ecc": "^2.3.14",
|
|
64
|
+
"@peculiar/asn1-schema": "^2.3.13",
|
|
65
|
+
"@peculiar/x509": "^1.12.3",
|
|
66
|
+
"micro-rsa-dsa-dh": "^0.1.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@adiwajshing/eslint-config": "github:adiwajshing/eslint-config",
|
|
70
|
+
"@commitlint/cli": "^17.8.1",
|
|
71
|
+
"@commitlint/config-conventional": "^17.8.1",
|
|
72
|
+
"@types/chance": "^1.1.6",
|
|
73
|
+
"@types/node": "^22.0.0",
|
|
74
|
+
"@typescript-eslint/eslint-plugin": "^8.17.0",
|
|
75
|
+
"chance": "^1.1.12",
|
|
76
|
+
"csv-parse": "^5.6.0",
|
|
77
|
+
"esbuild": "^0.25.8",
|
|
78
|
+
"eslint": "^8.57.1",
|
|
79
|
+
"pino": "^9.5.0",
|
|
80
|
+
"typescript": "^5.9.2"
|
|
81
|
+
}
|
|
82
|
+
}
|