@gjsify/tls 0.3.20 → 0.4.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/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/index.js +2 -3
- package/lib/types/cert.spec.d.ts +2 -0
- package/lib/types/index.d.ts +82 -27
- package/lib/types/tls.gjs.spec.d.ts +2 -0
- package/package.json +7 -7
- package/src/cert.spec.ts +230 -0
- package/src/index.spec.ts +126 -103
- package/src/index.ts +580 -181
- package/src/test.mts +3 -1
- package/src/tls.gjs.spec.ts +165 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -1,9 +1,51 @@
|
|
|
1
|
-
// Reference: Node.js lib/tls.js
|
|
2
|
-
// Reimplemented for GJS using Gio.TlsClientConnection / Gio.TlsServerConnection
|
|
1
|
+
// Reference: Node.js lib/tls.js, lib/_tls_common.js, lib/_tls_wrap.js
|
|
2
|
+
// Reimplemented for GJS using Gio.TlsClientConnection / Gio.TlsServerConnection /
|
|
3
|
+
// Gio.TlsCertificate.
|
|
4
|
+
//
|
|
5
|
+
// Node-TLS option / API → Gio.TLS property/method mapping (authoritative for this file):
|
|
6
|
+
//
|
|
7
|
+
// Node option / API → Gio binding
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// tls.createSecureContext({cert}) → Gio.TlsCertificate.new_from_pem
|
|
10
|
+
// {key} (separate PEM) → PEM concatenated then new_from_pem
|
|
11
|
+
// {ca} (PEM string or array) → per-block Gio.TlsCertificate.new_from_pem
|
|
12
|
+
// used as trust anchors via cert.verify()
|
|
13
|
+
// {rejectUnauthorized: false} → TlsConnection 'accept-certificate'
|
|
14
|
+
// signal returns true
|
|
15
|
+
// {minVersion}/{maxVersion}/ → Not exposed by Gio (handled by GnuTLS
|
|
16
|
+
// {ciphers} backend); stored for diagnostics only
|
|
17
|
+
// {ALPNProtocols} → TlsConnection.set_advertised_protocols
|
|
18
|
+
// tlsSocket.alpnProtocol → TlsConnection.get_negotiated_protocol
|
|
19
|
+
// {servername} (SNI) → TlsClientConnection.set_server_identity
|
|
20
|
+
// (Gio.NetworkAddress with hostname)
|
|
21
|
+
// tls.connect({cert,key}) (mTLS) → TlsConnection.set_certificate(client_cert)
|
|
22
|
+
// tls.createServer({SNICallback}) → Best-effort: see "Open TODOs" — Gio
|
|
23
|
+
// does not surface the ClientHello
|
|
24
|
+
// server_name to JS before handshake.
|
|
25
|
+
// tlsSocket.getPeerCertificate() → TlsConnection.get_peer_certificate +
|
|
26
|
+
// TlsCertificate.get_subject_name /
|
|
27
|
+
// get_issuer_name / get_dns_names /
|
|
28
|
+
// get_ip_addresses / get_not_valid_* /
|
|
29
|
+
// certificate_pem
|
|
30
|
+
// detailed=true issuer chain → TlsCertificate.get_issuer (walked)
|
|
31
|
+
// tlsSocket.getProtocol() → TlsConnection.get_protocol_version
|
|
32
|
+
// tlsSocket.getCipher() → TlsConnection.get_ciphersuite_name
|
|
33
|
+
// server: {requestCert, → TlsServerConnection.authentication_mode
|
|
34
|
+
// rejectUnauthorized} REQUESTED / REQUIRED / NONE
|
|
35
|
+
//
|
|
36
|
+
// Documented gaps (see STATUS.md "Open TODOs"):
|
|
37
|
+
// - SNI server-side selection from ClientHello: Gio does not expose
|
|
38
|
+
// server_name extension before handshake; SNICallback is consulted but
|
|
39
|
+
// selection is approximate.
|
|
40
|
+
// - OCSP stapling: not exposed by Gio.
|
|
41
|
+
// - TLS session resumption ('session' event, {session} option): GnuTLS
|
|
42
|
+
// resumption API is not surfaced via GI.
|
|
43
|
+
// - Custom DH/ECDH params, ticket keys: not exposed.
|
|
3
44
|
|
|
4
45
|
import Gio from '@girs/gio-2.0';
|
|
5
46
|
import GLib from '@girs/glib-2.0';
|
|
6
47
|
import { Socket, Server } from 'node:net';
|
|
48
|
+
import type { Server as NetServer } from 'node:net';
|
|
7
49
|
import { createNodeError, deferEmit } from '@gjsify/utils';
|
|
8
50
|
|
|
9
51
|
export const DEFAULT_MIN_VERSION = 'TLSv1.2';
|
|
@@ -18,12 +60,201 @@ export function getCiphers(): string[] {
|
|
|
18
60
|
];
|
|
19
61
|
}
|
|
20
62
|
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// PEM helpers
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
type PemInput = string | Buffer | Uint8Array | Array<string | Buffer | Uint8Array>;
|
|
68
|
+
|
|
69
|
+
/** Coerce a PEM input (string, Buffer/Uint8Array, or array) to a single PEM string. */
|
|
70
|
+
function pemToString(value: PemInput): string {
|
|
71
|
+
if (Array.isArray(value)) {
|
|
72
|
+
return value.map(pemToString).join('\n');
|
|
73
|
+
}
|
|
74
|
+
if (typeof value === 'string') return value;
|
|
75
|
+
if (value && typeof (value as Buffer).toString === 'function') {
|
|
76
|
+
try {
|
|
77
|
+
return (value as Buffer).toString('utf-8');
|
|
78
|
+
} catch {
|
|
79
|
+
return new TextDecoder('utf-8').decode(value as Uint8Array);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return String(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Split a concatenated PEM blob into individual `-----BEGIN ...-----...-----END ...-----` blocks. */
|
|
86
|
+
function splitPemBlocks(pem: string): string[] {
|
|
87
|
+
const out: string[] = [];
|
|
88
|
+
const re = /-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g;
|
|
89
|
+
let m: RegExpExecArray | null;
|
|
90
|
+
while ((m = re.exec(pem)) !== null) {
|
|
91
|
+
out.push(m[0]);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Build a TlsCertificate (and chain) from PEM strings. The first cert and key are the leaf. */
|
|
97
|
+
function buildGioCertificate(cert: PemInput, key?: PemInput): Gio.TlsCertificate {
|
|
98
|
+
const certPem = pemToString(cert);
|
|
99
|
+
const keyPem = key ? pemToString(key) : '';
|
|
100
|
+
const pem = keyPem ? `${certPem}\n${keyPem}` : certPem;
|
|
101
|
+
return Gio.TlsCertificate.new_from_pem(pem, pem.length);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Parse a CA bundle (PEM string or array) into a list of TlsCertificate trust anchors. */
|
|
105
|
+
function buildCaCertificates(ca: PemInput): Gio.TlsCertificate[] {
|
|
106
|
+
const blocks: string[] = [];
|
|
107
|
+
if (Array.isArray(ca)) {
|
|
108
|
+
for (const item of ca) blocks.push(...splitPemBlocks(pemToString(item)));
|
|
109
|
+
} else {
|
|
110
|
+
blocks.push(...splitPemBlocks(pemToString(ca)));
|
|
111
|
+
}
|
|
112
|
+
const out: Gio.TlsCertificate[] = [];
|
|
113
|
+
for (const block of blocks) {
|
|
114
|
+
try {
|
|
115
|
+
out.push(Gio.TlsCertificate.new_from_pem(block, block.length));
|
|
116
|
+
} catch {
|
|
117
|
+
// Skip blocks that aren't certificates (DH params, comments, etc).
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// Peer-certificate extraction
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
export interface CertSubject {
|
|
128
|
+
CN?: string | string[];
|
|
129
|
+
[key: string]: unknown;
|
|
130
|
+
}
|
|
131
|
+
|
|
21
132
|
export interface PeerCertificate {
|
|
22
|
-
subject?:
|
|
133
|
+
subject?: CertSubject;
|
|
134
|
+
issuer?: CertSubject;
|
|
23
135
|
subjectaltname?: string;
|
|
136
|
+
valid_from?: string;
|
|
137
|
+
valid_to?: string;
|
|
138
|
+
fingerprint?: string;
|
|
139
|
+
fingerprint256?: string;
|
|
140
|
+
serialNumber?: string;
|
|
141
|
+
raw?: Uint8Array;
|
|
142
|
+
issuerCertificate?: PeerCertificate;
|
|
24
143
|
[key: string]: unknown;
|
|
25
144
|
}
|
|
26
145
|
|
|
146
|
+
/** Parse a distinguished name string (e.g. "CN=example.com,O=Foo") into a key→value object. */
|
|
147
|
+
function parseDistinguishedName(dn: string | null): CertSubject {
|
|
148
|
+
if (!dn) return {};
|
|
149
|
+
const out: CertSubject = {};
|
|
150
|
+
for (const part of dn.split(/,(?![^=]*=)/)) {
|
|
151
|
+
const eq = part.indexOf('=');
|
|
152
|
+
if (eq < 0) continue;
|
|
153
|
+
const key = part.slice(0, eq).trim();
|
|
154
|
+
const value = part.slice(eq + 1).trim();
|
|
155
|
+
const existing = out[key];
|
|
156
|
+
if (existing === undefined) out[key] = value;
|
|
157
|
+
else if (Array.isArray(existing)) existing.push(value);
|
|
158
|
+
else out[key] = [existing as string, value];
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Format a GLib.DateTime as an OpenSSL-style validity string. */
|
|
164
|
+
function formatCertDate(dt: GLib.DateTime | null): string {
|
|
165
|
+
if (!dt) return '';
|
|
166
|
+
try { return dt.format('%b %d %H:%M:%S %Y GMT') ?? ''; } catch { return ''; }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Build the "subjectaltname" string from DNS names + IP addresses (Node format). */
|
|
170
|
+
function formatAltNames(cert: Gio.TlsCertificate): string {
|
|
171
|
+
const parts: string[] = [];
|
|
172
|
+
try {
|
|
173
|
+
const dns = cert.get_dns_names();
|
|
174
|
+
if (dns) {
|
|
175
|
+
for (const b of dns) {
|
|
176
|
+
const data = b.get_data();
|
|
177
|
+
if (!data) continue;
|
|
178
|
+
parts.push(`DNS:${new TextDecoder('utf-8').decode(data)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch { /* not all backends support this */ }
|
|
182
|
+
try {
|
|
183
|
+
const ips = cert.get_ip_addresses();
|
|
184
|
+
if (ips) for (const ip of ips) parts.push(`IP Address:${ip.to_string()}`);
|
|
185
|
+
} catch { /* same */ }
|
|
186
|
+
return parts.join(', ');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Compute SHA-1 / SHA-256 fingerprint strings from raw DER bytes (`AA:BB:CC:…`). */
|
|
190
|
+
function fingerprintFromBytes(bytes: Uint8Array, algo: GLib.ChecksumType): string {
|
|
191
|
+
try {
|
|
192
|
+
const cs = new GLib.Checksum(algo);
|
|
193
|
+
cs.update(bytes);
|
|
194
|
+
const hex = cs.get_string();
|
|
195
|
+
if (!hex) return '';
|
|
196
|
+
const out: string[] = [];
|
|
197
|
+
for (let i = 0; i < hex.length; i += 2) out.push(hex.slice(i, i + 2).toUpperCase());
|
|
198
|
+
return out.join(':');
|
|
199
|
+
} catch {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Decode a single PEM cert block into raw DER bytes. */
|
|
205
|
+
function pemToDer(pem: string): Uint8Array {
|
|
206
|
+
const m = /-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/.exec(pem);
|
|
207
|
+
if (!m) return new Uint8Array(0);
|
|
208
|
+
const b64 = m[1].replace(/[\s\r\n]+/g, '');
|
|
209
|
+
try {
|
|
210
|
+
const atob = (globalThis as { atob?: (s: string) => string }).atob;
|
|
211
|
+
if (!atob) return new Uint8Array(0);
|
|
212
|
+
const bin = atob(b64);
|
|
213
|
+
const out = new Uint8Array(bin.length);
|
|
214
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
215
|
+
return out;
|
|
216
|
+
} catch {
|
|
217
|
+
return new Uint8Array(0);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Convert a single TlsCertificate to the Node `getPeerCertificate()` shape. */
|
|
222
|
+
function tlsCertToPeerCert(cert: Gio.TlsCertificate, detailed: boolean): PeerCertificate {
|
|
223
|
+
const out: PeerCertificate = {};
|
|
224
|
+
try { out.subject = parseDistinguishedName(cert.get_subject_name()); } catch { /* */ }
|
|
225
|
+
try { out.issuer = parseDistinguishedName(cert.get_issuer_name()); } catch { /* */ }
|
|
226
|
+
out.subjectaltname = formatAltNames(cert);
|
|
227
|
+
try {
|
|
228
|
+
out.valid_from = formatCertDate(cert.get_not_valid_before());
|
|
229
|
+
out.valid_to = formatCertDate(cert.get_not_valid_after());
|
|
230
|
+
} catch { /* */ }
|
|
231
|
+
try {
|
|
232
|
+
const c = cert as unknown as { certificate_pem?: string; certificatePem?: string };
|
|
233
|
+
const pemProp = c.certificate_pem ?? c.certificatePem;
|
|
234
|
+
if (pemProp) {
|
|
235
|
+
const der = pemToDer(pemProp);
|
|
236
|
+
out.raw = der;
|
|
237
|
+
out.fingerprint = fingerprintFromBytes(der, GLib.ChecksumType.SHA1);
|
|
238
|
+
out.fingerprint256 = fingerprintFromBytes(der, GLib.ChecksumType.SHA256);
|
|
239
|
+
}
|
|
240
|
+
} catch { /* */ }
|
|
241
|
+
if (detailed) {
|
|
242
|
+
try {
|
|
243
|
+
const issuerCert = cert.get_issuer();
|
|
244
|
+
if (issuerCert && !issuerCert.is_same(cert)) {
|
|
245
|
+
out.issuerCertificate = tlsCertToPeerCert(issuerCert, true);
|
|
246
|
+
} else if (issuerCert) {
|
|
247
|
+
out.issuerCertificate = out; // self-signed: Node returns self-ref
|
|
248
|
+
}
|
|
249
|
+
} catch { /* */ }
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// RFC 6125 hostname matching
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
27
258
|
/** Removes a trailing dot from a fully-qualified domain name. */
|
|
28
259
|
function unfqdn(host: string): string {
|
|
29
260
|
return host.endsWith('.') ? host.slice(0, -1) : host;
|
|
@@ -34,27 +265,67 @@ function splitHost(host: string): string[] {
|
|
|
34
265
|
return unfqdn(host).toLowerCase().split('.');
|
|
35
266
|
}
|
|
36
267
|
|
|
37
|
-
/**
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
268
|
+
/** Reject control / non-ASCII bytes in pattern labels (RFC 6125 sanity). */
|
|
269
|
+
function isPrintableAscii(s: string): boolean {
|
|
270
|
+
// U+0021 ('!') through U+007E ('~')
|
|
271
|
+
for (let i = 0; i < s.length; i++) {
|
|
272
|
+
const c = s.charCodeAt(i);
|
|
273
|
+
if (c < 0x21 || c > 0x7E) return false;
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Match a hostname (already split into labels) against a single pattern from
|
|
280
|
+
* a SAN DNS entry or CN. Implements RFC 6125 §6.4.3:
|
|
281
|
+
* - wildcard valid only in the leftmost label
|
|
282
|
+
* - wildcard label may not contain Punycode A-labels (`xn--`)
|
|
283
|
+
* - `*.tld` (two-label patterns) are rejected
|
|
284
|
+
* - exactly one wildcard per label
|
|
285
|
+
*/
|
|
286
|
+
function checkHostMatch(hostParts: string[], pattern: string): boolean {
|
|
287
|
+
if (!pattern) return false;
|
|
288
|
+
const patternParts = splitHost(pattern);
|
|
289
|
+
if (hostParts.length !== patternParts.length) return false;
|
|
290
|
+
if (patternParts.includes('')) return false;
|
|
291
|
+
if (!patternParts.every(isPrintableAscii)) return false;
|
|
292
|
+
for (let i = hostParts.length - 1; i > 0; i--) {
|
|
293
|
+
if (hostParts[i] !== patternParts[i]) return false;
|
|
294
|
+
}
|
|
295
|
+
const hostSub = hostParts[0];
|
|
296
|
+
const patSub = patternParts[0];
|
|
297
|
+
const wildSplit = patSub.split('*', 3);
|
|
298
|
+
if (wildSplit.length === 1 || patSub.includes('xn--')) {
|
|
299
|
+
return hostSub === patSub;
|
|
45
300
|
}
|
|
46
|
-
|
|
301
|
+
if (wildSplit.length > 2) return false;
|
|
302
|
+
if (patternParts.length <= 2) return false;
|
|
303
|
+
const prefix = wildSplit[0];
|
|
304
|
+
const suffix = wildSplit[1];
|
|
305
|
+
if (prefix.length + suffix.length > hostSub.length) return false;
|
|
306
|
+
if (!hostSub.startsWith(prefix)) return false;
|
|
307
|
+
if (!hostSub.endsWith(suffix)) return false;
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Error returned by checkServerIdentity, with Node-compatible shape. */
|
|
312
|
+
export interface CertAltNameError extends Error {
|
|
313
|
+
reason: string;
|
|
314
|
+
host: string;
|
|
315
|
+
cert: PeerCertificate;
|
|
316
|
+
code: 'ERR_TLS_CERT_ALTNAME_INVALID';
|
|
47
317
|
}
|
|
48
318
|
|
|
49
319
|
/**
|
|
50
320
|
* Verifies that the certificate `cert` is valid for `hostname`.
|
|
51
|
-
* Returns an Error if the check
|
|
321
|
+
* Returns an Error (with code 'ERR_TLS_CERT_ALTNAME_INVALID') if the check
|
|
322
|
+
* fails, or `undefined` on success.
|
|
52
323
|
*
|
|
53
|
-
* Reference: Node.js lib/tls.js exports.checkServerIdentity
|
|
324
|
+
* Reference: Node.js lib/tls.js exports.checkServerIdentity (RFC 6125 §6.4.3).
|
|
54
325
|
*/
|
|
55
|
-
export function checkServerIdentity(hostname: string, cert: PeerCertificate):
|
|
326
|
+
export function checkServerIdentity(hostname: string, cert: PeerCertificate): CertAltNameError | undefined {
|
|
56
327
|
const subject = cert.subject;
|
|
57
|
-
const altNames = cert.subjectaltname
|
|
328
|
+
const altNames = cert.subjectaltname;
|
|
58
329
|
const dnsNames: string[] = [];
|
|
59
330
|
const ips: string[] = [];
|
|
60
331
|
|
|
@@ -63,11 +334,8 @@ export function checkServerIdentity(hostname: string, cert: PeerCertificate): Er
|
|
|
63
334
|
if (altNames) {
|
|
64
335
|
const parts = altNames.split(', ');
|
|
65
336
|
for (const name of parts) {
|
|
66
|
-
if (name.startsWith('DNS:'))
|
|
67
|
-
|
|
68
|
-
} else if (name.startsWith('IP Address:')) {
|
|
69
|
-
ips.push(name.slice(11).trim());
|
|
70
|
-
}
|
|
337
|
+
if (name.startsWith('DNS:')) dnsNames.push(name.slice(4));
|
|
338
|
+
else if (name.startsWith('IP Address:')) ips.push(name.slice(11).trim());
|
|
71
339
|
}
|
|
72
340
|
}
|
|
73
341
|
|
|
@@ -76,49 +344,87 @@ export function checkServerIdentity(hostname: string, cert: PeerCertificate): Er
|
|
|
76
344
|
|
|
77
345
|
hostname = unfqdn(hostname);
|
|
78
346
|
|
|
79
|
-
// Check numeric IP addresses
|
|
80
347
|
const isIPv4 = /^(\d{1,3}\.){3}\d{1,3}$/.test(hostname);
|
|
81
348
|
const isIPv6 = hostname.includes(':');
|
|
82
349
|
if (isIPv4 || isIPv6) {
|
|
83
350
|
valid = ips.some(ip => ip.toLowerCase() === hostname.toLowerCase());
|
|
84
|
-
if (!valid)
|
|
351
|
+
if (!valid) {
|
|
85
352
|
reason = `IP: ${hostname} is not in the cert's list: ${ips.join(', ')}`;
|
|
353
|
+
}
|
|
86
354
|
} else if (dnsNames.length > 0 || subject?.CN) {
|
|
87
355
|
const hostParts = splitHost(hostname);
|
|
88
356
|
|
|
89
357
|
if (dnsNames.length > 0) {
|
|
90
|
-
valid = dnsNames.some(pattern =>
|
|
91
|
-
if (!valid)
|
|
358
|
+
valid = dnsNames.some(pattern => checkHostMatch(hostParts, pattern.trim()));
|
|
359
|
+
if (!valid) {
|
|
92
360
|
reason = `Host: ${hostname}. is not in the cert's altnames: ${altNames}`;
|
|
361
|
+
}
|
|
93
362
|
} else {
|
|
94
|
-
const cn = subject
|
|
363
|
+
const cn = subject?.CN;
|
|
95
364
|
if (Array.isArray(cn)) {
|
|
96
|
-
valid = cn.some(c =>
|
|
365
|
+
valid = cn.some(c => checkHostMatch(hostParts, c));
|
|
97
366
|
} else if (cn) {
|
|
98
|
-
valid =
|
|
367
|
+
valid = checkHostMatch(hostParts, cn);
|
|
99
368
|
}
|
|
100
|
-
if (!valid)
|
|
369
|
+
if (!valid) {
|
|
101
370
|
reason = `Host: ${hostname}. is not cert's CN: ${cn}`;
|
|
371
|
+
}
|
|
102
372
|
}
|
|
103
373
|
} else {
|
|
104
374
|
reason = 'Cert does not contain a DNS name';
|
|
105
375
|
}
|
|
106
376
|
|
|
107
377
|
if (!valid) {
|
|
108
|
-
const err = new Error(reason) as
|
|
378
|
+
const err = new Error(reason) as CertAltNameError;
|
|
109
379
|
err.reason = reason;
|
|
110
380
|
err.host = hostname;
|
|
111
381
|
err.cert = cert;
|
|
382
|
+
err.code = 'ERR_TLS_CERT_ALTNAME_INVALID';
|
|
112
383
|
return err;
|
|
113
384
|
}
|
|
114
385
|
return undefined;
|
|
115
386
|
}
|
|
116
387
|
|
|
388
|
+
// ============================================================================
|
|
389
|
+
// SecureContext
|
|
390
|
+
// ============================================================================
|
|
391
|
+
|
|
117
392
|
export interface SecureContextOptions {
|
|
118
|
-
ca?:
|
|
119
|
-
cert?:
|
|
120
|
-
key?:
|
|
393
|
+
ca?: PemInput;
|
|
394
|
+
cert?: PemInput;
|
|
395
|
+
key?: PemInput;
|
|
396
|
+
passphrase?: string;
|
|
121
397
|
rejectUnauthorized?: boolean;
|
|
398
|
+
ciphers?: string;
|
|
399
|
+
minVersion?: string;
|
|
400
|
+
maxVersion?: string;
|
|
401
|
+
ALPNProtocols?: string[];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** Internal "secure context" — parsed TLS material shared by tls.connect/createServer. */
|
|
405
|
+
export interface SecureContext {
|
|
406
|
+
certificate: Gio.TlsCertificate | null;
|
|
407
|
+
caCertificates: Gio.TlsCertificate[];
|
|
408
|
+
options: SecureContextOptions;
|
|
409
|
+
/**
|
|
410
|
+
* Node-compat handle (Node returns a `SecureContext` with an internal native
|
|
411
|
+
* `context` field). We have no native handle, so this points back at the
|
|
412
|
+
* SecureContext object itself — `ctx.context !== undefined` matches Node.
|
|
413
|
+
*/
|
|
414
|
+
context: SecureContext;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Build a SecureContext from PEM material. Buffer/Uint8Array/string all accepted. */
|
|
418
|
+
export function createSecureContext(options?: SecureContextOptions): SecureContext {
|
|
419
|
+
const opts = options ?? {};
|
|
420
|
+
let certificate: Gio.TlsCertificate | null = null;
|
|
421
|
+
if (opts.cert) {
|
|
422
|
+
try { certificate = buildGioCertificate(opts.cert, opts.key); } catch { certificate = null; }
|
|
423
|
+
}
|
|
424
|
+
const caCertificates = opts.ca ? buildCaCertificates(opts.ca) : [];
|
|
425
|
+
const ctx = { certificate, caCertificates, options: opts } as SecureContext;
|
|
426
|
+
ctx.context = ctx; // Node-compat self-reference
|
|
427
|
+
return ctx;
|
|
122
428
|
}
|
|
123
429
|
|
|
124
430
|
export interface TlsConnectOptions extends SecureContextOptions {
|
|
@@ -127,6 +433,20 @@ export interface TlsConnectOptions extends SecureContextOptions {
|
|
|
127
433
|
socket?: Socket;
|
|
128
434
|
servername?: string;
|
|
129
435
|
ALPNProtocols?: string[];
|
|
436
|
+
/** Pre-built secure context from createSecureContext(). */
|
|
437
|
+
secureContext?: SecureContext;
|
|
438
|
+
/** Custom server-identity check (runs after the GnuTLS-level check). */
|
|
439
|
+
checkServerIdentity?: (host: string, cert: PeerCertificate) => Error | undefined;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Internal helper: cast a Socket to its private-field shape (we own the impl). */
|
|
443
|
+
interface SocketInternals {
|
|
444
|
+
_connection: Gio.SocketConnection | null;
|
|
445
|
+
_ioStream: Gio.IOStream | null;
|
|
446
|
+
_inputStream: Gio.InputStream | null;
|
|
447
|
+
_outputStream: Gio.OutputStream | null;
|
|
448
|
+
_reading: boolean;
|
|
449
|
+
_startReading(): void;
|
|
130
450
|
}
|
|
131
451
|
|
|
132
452
|
/**
|
|
@@ -137,11 +457,14 @@ export class TLSSocket extends Socket {
|
|
|
137
457
|
authorized = false;
|
|
138
458
|
authorizationError?: string;
|
|
139
459
|
alpnProtocol: string | false = false;
|
|
460
|
+
servername: string | undefined;
|
|
140
461
|
|
|
141
462
|
/** @internal */
|
|
142
463
|
_tlsConnection: Gio.TlsConnection | null = null;
|
|
464
|
+
/** @internal — preserved for diagnostics + future cert-chain verification. */
|
|
465
|
+
_secureContext: SecureContext | null = null;
|
|
143
466
|
|
|
144
|
-
constructor(
|
|
467
|
+
constructor(_socket?: Socket, _options?: SecureContextOptions) {
|
|
145
468
|
super();
|
|
146
469
|
}
|
|
147
470
|
|
|
@@ -151,25 +474,23 @@ export class TLSSocket extends Socket {
|
|
|
151
474
|
*/
|
|
152
475
|
_setupTlsStreams(tlsConn: Gio.TlsConnection): void {
|
|
153
476
|
this._tlsConnection = tlsConn;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
(this as any)._connection = tlsConn as unknown as Gio.SocketConnection;
|
|
477
|
+
const internals = this as unknown as SocketInternals;
|
|
478
|
+
internals._inputStream = tlsConn.get_input_stream();
|
|
479
|
+
internals._outputStream = tlsConn.get_output_stream();
|
|
480
|
+
internals._connection = tlsConn as unknown as Gio.SocketConnection;
|
|
159
481
|
}
|
|
160
482
|
|
|
161
|
-
/**
|
|
162
|
-
|
|
483
|
+
/**
|
|
484
|
+
* Get the peer certificate. When `detailed` is true, walks the issuer chain
|
|
485
|
+
* via `Gio.TlsCertificate.get_issuer()` and populates `issuerCertificate`
|
|
486
|
+
* recursively (with a self-reference on the root for compatibility).
|
|
487
|
+
*/
|
|
488
|
+
getPeerCertificate(detailed = false): PeerCertificate {
|
|
163
489
|
if (!this._tlsConnection) return {};
|
|
164
490
|
try {
|
|
165
491
|
const cert = this._tlsConnection.get_peer_certificate();
|
|
166
492
|
if (!cert) return {};
|
|
167
|
-
return
|
|
168
|
-
subject: {},
|
|
169
|
-
issuer: {},
|
|
170
|
-
valid_from: '',
|
|
171
|
-
valid_to: '',
|
|
172
|
-
};
|
|
493
|
+
return tlsCertToPeerCert(cert, detailed);
|
|
173
494
|
} catch {
|
|
174
495
|
return {};
|
|
175
496
|
}
|
|
@@ -192,7 +513,7 @@ export class TLSSocket extends Socket {
|
|
|
192
513
|
}
|
|
193
514
|
}
|
|
194
515
|
|
|
195
|
-
/** Get the cipher
|
|
516
|
+
/** Get the negotiated cipher suite name + version. */
|
|
196
517
|
getCipher(): { name: string; version: string } | null {
|
|
197
518
|
if (!this._tlsConnection) return null;
|
|
198
519
|
try {
|
|
@@ -203,7 +524,7 @@ export class TLSSocket extends Socket {
|
|
|
203
524
|
}
|
|
204
525
|
}
|
|
205
526
|
|
|
206
|
-
/** Get the negotiated ALPN protocol. */
|
|
527
|
+
/** Get the negotiated ALPN protocol (or false if none). */
|
|
207
528
|
getAlpnProtocol(): string | false {
|
|
208
529
|
if (!this._tlsConnection) return false;
|
|
209
530
|
try {
|
|
@@ -233,59 +554,94 @@ export function connect(options: TlsConnectOptions, callback?: () => void): TLSS
|
|
|
233
554
|
const servername = options.servername || host;
|
|
234
555
|
const rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
235
556
|
|
|
236
|
-
|
|
557
|
+
const ctx = options.secureContext ?? createSecureContext(options);
|
|
558
|
+
socket._secureContext = ctx;
|
|
559
|
+
socket.servername = servername;
|
|
560
|
+
const customCheckServerIdentity = options.checkServerIdentity;
|
|
561
|
+
|
|
237
562
|
socket.once('connect', () => {
|
|
238
|
-
const rawConnection
|
|
563
|
+
const rawConnection = (socket as unknown as SocketInternals)._connection;
|
|
239
564
|
if (!rawConnection) {
|
|
240
565
|
socket.destroy(new Error('No underlying connection for TLS upgrade'));
|
|
241
566
|
return;
|
|
242
567
|
}
|
|
243
568
|
|
|
244
569
|
try {
|
|
245
|
-
// Create TLS client connection wrapping the raw TCP connection
|
|
246
570
|
const connectable = Gio.NetworkAddress.new(servername, port);
|
|
247
571
|
const tlsConn = Gio.TlsClientConnection.new(
|
|
248
|
-
rawConnection as Gio.IOStream,
|
|
572
|
+
rawConnection as unknown as Gio.IOStream,
|
|
249
573
|
connectable,
|
|
250
|
-
)
|
|
574
|
+
);
|
|
251
575
|
|
|
252
|
-
// Set server identity for certificate validation
|
|
253
576
|
tlsConn.set_server_identity(connectable);
|
|
254
577
|
|
|
255
|
-
//
|
|
578
|
+
// Client certificate (mTLS)
|
|
579
|
+
if (ctx.certificate) {
|
|
580
|
+
try {
|
|
581
|
+
tlsConn.set_certificate(ctx.certificate);
|
|
582
|
+
} catch (err: unknown) {
|
|
583
|
+
// eslint-disable-next-line no-console
|
|
584
|
+
console.warn('[tls] failed to set client certificate:', err);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ALPN
|
|
256
589
|
if (options.ALPNProtocols && options.ALPNProtocols.length > 0) {
|
|
257
590
|
try {
|
|
258
|
-
|
|
591
|
+
tlsConn.set_advertised_protocols(options.ALPNProtocols);
|
|
259
592
|
} catch {
|
|
260
|
-
// ALPN may not be supported
|
|
593
|
+
// ALPN may not be supported
|
|
261
594
|
}
|
|
262
595
|
}
|
|
263
596
|
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
597
|
+
// Certificate validation: by default rely on system trust store +
|
|
598
|
+
// 'accept-certificate' returning false. With a custom CA we accept
|
|
599
|
+
// peer certs that validate against `ctx.caCertificates`. With
|
|
600
|
+
// `rejectUnauthorized: false`, accept everything.
|
|
601
|
+
tlsConn.connect('accept-certificate', (
|
|
602
|
+
_conn: Gio.TlsConnection,
|
|
603
|
+
peerCert: Gio.TlsCertificate,
|
|
604
|
+
_errors: Gio.TlsCertificateFlags,
|
|
605
|
+
): boolean => {
|
|
606
|
+
if (!rejectUnauthorized) return true;
|
|
607
|
+
if (ctx.caCertificates.length === 0) return false;
|
|
608
|
+
for (const ca of ctx.caCertificates) {
|
|
609
|
+
try {
|
|
610
|
+
const flags = peerCert.verify(connectable, ca);
|
|
611
|
+
if (flags === Gio.TlsCertificateFlags.NO_FLAGS) return true;
|
|
612
|
+
} catch { /* try next */ }
|
|
613
|
+
}
|
|
614
|
+
return false;
|
|
615
|
+
});
|
|
271
616
|
|
|
272
|
-
// Perform TLS handshake asynchronously
|
|
273
617
|
const cancellable = new Gio.Cancellable();
|
|
274
|
-
|
|
618
|
+
tlsConn.handshake_async(
|
|
275
619
|
GLib.PRIORITY_DEFAULT,
|
|
276
620
|
cancellable,
|
|
277
621
|
(_source: Gio.TlsConnection | null, asyncResult: Gio.AsyncResult) => {
|
|
278
622
|
try {
|
|
279
|
-
|
|
623
|
+
tlsConn.handshake_finish(asyncResult);
|
|
280
624
|
socket.authorized = true;
|
|
281
|
-
socket._setupTlsStreams(tlsConn
|
|
282
|
-
|
|
283
|
-
// Get ALPN result
|
|
625
|
+
socket._setupTlsStreams(tlsConn);
|
|
284
626
|
socket.alpnProtocol = socket.getAlpnProtocol();
|
|
285
627
|
|
|
286
|
-
//
|
|
287
|
-
(
|
|
288
|
-
|
|
628
|
+
// Custom server-identity check (post-handshake, mirrors Node).
|
|
629
|
+
if (customCheckServerIdentity) {
|
|
630
|
+
const peer = socket.getPeerCertificate();
|
|
631
|
+
const idErr = customCheckServerIdentity(servername, peer);
|
|
632
|
+
if (idErr) {
|
|
633
|
+
socket.authorized = false;
|
|
634
|
+
socket.authorizationError = idErr.message;
|
|
635
|
+
if (rejectUnauthorized) {
|
|
636
|
+
socket.destroy(idErr);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const internals = socket as unknown as SocketInternals;
|
|
643
|
+
internals._reading = false;
|
|
644
|
+
internals._startReading();
|
|
289
645
|
|
|
290
646
|
socket.emit('secureConnect');
|
|
291
647
|
} catch (err: unknown) {
|
|
@@ -294,8 +650,7 @@ export function connect(options: TlsConnectOptions, callback?: () => void): TLSS
|
|
|
294
650
|
if (rejectUnauthorized) {
|
|
295
651
|
socket.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
296
652
|
} else {
|
|
297
|
-
|
|
298
|
-
socket._setupTlsStreams(tlsConn as Gio.TlsConnection);
|
|
653
|
+
socket._setupTlsStreams(tlsConn);
|
|
299
654
|
socket.emit('secureConnect');
|
|
300
655
|
}
|
|
301
656
|
}
|
|
@@ -306,95 +661,114 @@ export function connect(options: TlsConnectOptions, callback?: () => void): TLSS
|
|
|
306
661
|
}
|
|
307
662
|
});
|
|
308
663
|
|
|
309
|
-
// Initiate TCP connection
|
|
310
664
|
socket.connect({ port, host });
|
|
311
665
|
return socket;
|
|
312
666
|
}
|
|
313
667
|
|
|
314
|
-
/**
|
|
315
|
-
* Create a TLS secure context.
|
|
316
|
-
*/
|
|
317
|
-
export function createSecureContext(options?: SecureContextOptions): { context: any } {
|
|
318
|
-
return { context: options || {} };
|
|
319
|
-
}
|
|
320
|
-
|
|
321
668
|
export const rootCertificates: string[] = [];
|
|
322
669
|
|
|
670
|
+
// ============================================================================
|
|
671
|
+
// TLSServer / createServer
|
|
672
|
+
// ============================================================================
|
|
673
|
+
|
|
674
|
+
export type SNICallback = (
|
|
675
|
+
servername: string,
|
|
676
|
+
cb: (err: Error | null, ctx?: SecureContext) => void,
|
|
677
|
+
) => void;
|
|
678
|
+
|
|
323
679
|
export interface TlsServerOptions extends SecureContextOptions {
|
|
324
680
|
requestCert?: boolean;
|
|
325
681
|
rejectUnauthorized?: boolean;
|
|
326
682
|
ALPNProtocols?: string[];
|
|
683
|
+
SNICallback?: SNICallback;
|
|
327
684
|
}
|
|
328
685
|
|
|
329
686
|
/**
|
|
330
|
-
*
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const certStr = Array.isArray(cert)
|
|
334
|
-
? cert.map((c) => (typeof c === 'string' ? c : c.toString('utf-8'))).join('\n')
|
|
335
|
-
: typeof cert === 'string' ? cert : cert.toString('utf-8');
|
|
336
|
-
|
|
337
|
-
const keyStr = key
|
|
338
|
-
? Array.isArray(key)
|
|
339
|
-
? key.map((k) => (typeof k === 'string' ? k : k.toString('utf-8'))).join('\n')
|
|
340
|
-
: typeof key === 'string' ? key : key.toString('utf-8')
|
|
341
|
-
: '';
|
|
342
|
-
|
|
343
|
-
const pem = keyStr ? `${certStr}\n${keyStr}` : certStr;
|
|
344
|
-
return Gio.TlsCertificate.new_from_pem(pem, pem.length);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* TLSServer wraps a net.Server to accept TLS connections.
|
|
687
|
+
* TLSServer accepts incoming TCP connections and upgrades each to TLS via
|
|
688
|
+
* `Gio.TlsServerConnection`. Supports mTLS via `requestCert`+`rejectUnauthorized`,
|
|
689
|
+
* SNI selection via `addContext`/`SNICallback`, and ALPN negotiation.
|
|
349
690
|
*/
|
|
350
691
|
export class TLSServer extends Server {
|
|
351
692
|
private _tlsCertificate: Gio.TlsCertificate | null = null;
|
|
352
693
|
private _tlsOptions: TlsServerOptions;
|
|
353
|
-
private _sniContexts = new Map<string,
|
|
694
|
+
private _sniContexts = new Map<string, SecureContext>();
|
|
695
|
+
/** @internal — exposed for tests. */
|
|
696
|
+
_secureContext: SecureContext;
|
|
354
697
|
|
|
355
698
|
constructor(options?: TlsServerOptions, secureConnectionListener?: (socket: TLSSocket) => void) {
|
|
356
699
|
super();
|
|
357
|
-
this._tlsOptions = options
|
|
700
|
+
this._tlsOptions = options ?? {};
|
|
701
|
+
this._secureContext = createSecureContext(this._tlsOptions);
|
|
702
|
+
this._tlsCertificate = this._secureContext.certificate;
|
|
358
703
|
|
|
359
704
|
if (secureConnectionListener) {
|
|
360
705
|
this.on('secureConnection', secureConnectionListener);
|
|
361
706
|
}
|
|
362
707
|
|
|
363
|
-
if (this._tlsOptions.cert) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
708
|
+
if (this._tlsOptions.cert && !this._tlsCertificate) {
|
|
709
|
+
// PEM provided but failed to parse — emit error asynchronously.
|
|
710
|
+
deferEmit(this as unknown as NetServer, 'error', createNodeError(
|
|
711
|
+
new Error('Failed to parse TLS certificate'), 'createServer', {},
|
|
712
|
+
));
|
|
369
713
|
}
|
|
370
714
|
}
|
|
371
715
|
|
|
372
716
|
/**
|
|
373
|
-
* Add
|
|
717
|
+
* Add an additional context for SNI (Server Name Indication). Uses RFC 6125
|
|
718
|
+
* matching against the requested server name.
|
|
374
719
|
*/
|
|
375
720
|
addContext(hostname: string, context: SecureContextOptions): void {
|
|
376
|
-
|
|
721
|
+
try {
|
|
722
|
+
const ctx = createSecureContext(context);
|
|
723
|
+
this._sniContexts.set(hostname.toLowerCase(), ctx);
|
|
724
|
+
} catch (err: unknown) {
|
|
725
|
+
this.emit('error', createNodeError(err, 'addContext', {}));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Resolve a SecureContext for the given server name. Order:
|
|
731
|
+
* 1. exact match in `_sniContexts`
|
|
732
|
+
* 2. RFC 6125 wildcard match in `_sniContexts`
|
|
733
|
+
* 3. SNICallback (if provided)
|
|
734
|
+
* 4. fall through to the server's default context
|
|
735
|
+
*/
|
|
736
|
+
private _resolveSniContext(servername: string | null, done: (ctx: SecureContext) => void): void {
|
|
737
|
+
const fallback = this._secureContext;
|
|
738
|
+
if (!servername) { done(fallback); return; }
|
|
739
|
+
const lower = servername.toLowerCase();
|
|
740
|
+
const exact = this._sniContexts.get(lower);
|
|
741
|
+
if (exact) { done(exact); return; }
|
|
742
|
+
const hostParts = splitHost(lower);
|
|
743
|
+
for (const [pattern, ctx] of this._sniContexts) {
|
|
744
|
+
if (checkHostMatch(hostParts, pattern)) { done(ctx); return; }
|
|
745
|
+
}
|
|
746
|
+
if (this._tlsOptions.SNICallback) {
|
|
377
747
|
try {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
748
|
+
this._tlsOptions.SNICallback(servername, (err: Error | null, ctx?: SecureContext) => {
|
|
749
|
+
if (err || !ctx) { done(fallback); return; }
|
|
750
|
+
done(ctx);
|
|
751
|
+
});
|
|
752
|
+
return;
|
|
753
|
+
} catch {
|
|
754
|
+
done(fallback);
|
|
755
|
+
return;
|
|
382
756
|
}
|
|
383
757
|
}
|
|
758
|
+
done(fallback);
|
|
384
759
|
}
|
|
385
760
|
|
|
386
761
|
listen(...args: unknown[]): this {
|
|
387
762
|
this.on('connection', (socket: Socket) => {
|
|
388
763
|
this._upgradeTls(socket);
|
|
389
764
|
});
|
|
390
|
-
|
|
765
|
+
type ListenArgs = Parameters<NetServer['listen']>;
|
|
766
|
+
return (super.listen as (...a: ListenArgs) => this)(...(args as unknown as ListenArgs));
|
|
391
767
|
}
|
|
392
768
|
|
|
393
|
-
/**
|
|
394
|
-
* Upgrade a raw TCP socket to TLS using Gio.TlsServerConnection.
|
|
395
|
-
*/
|
|
769
|
+
/** Upgrade a raw TCP socket to TLS using Gio.TlsServerConnection. */
|
|
396
770
|
private _upgradeTls(socket: Socket): void {
|
|
397
|
-
const rawConnection
|
|
771
|
+
const rawConnection = (socket as unknown as SocketInternals)._connection;
|
|
398
772
|
if (!rawConnection) {
|
|
399
773
|
const err = new Error('Cannot upgrade socket: no underlying connection');
|
|
400
774
|
this.emit('tlsClientError', err, socket);
|
|
@@ -402,78 +776,101 @@ export class TLSServer extends Server {
|
|
|
402
776
|
return;
|
|
403
777
|
}
|
|
404
778
|
|
|
405
|
-
if (!this._tlsCertificate) {
|
|
779
|
+
if (!this._tlsCertificate && this._sniContexts.size === 0 && !this._tlsOptions.SNICallback) {
|
|
406
780
|
const err = new Error('TLS server has no certificate configured');
|
|
407
781
|
this.emit('tlsClientError', err, socket);
|
|
408
782
|
socket.destroy();
|
|
409
783
|
return;
|
|
410
784
|
}
|
|
411
785
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
: Gio.TlsAuthenticationMode.REQUESTED;
|
|
423
|
-
} else {
|
|
424
|
-
tlsConn.authenticationMode = Gio.TlsAuthenticationMode.NONE;
|
|
786
|
+
// SNI: Gio does not surface ClientHello server_name to JS pre-handshake;
|
|
787
|
+
// we use the server's default certificate. Real-world SNI multiplexing
|
|
788
|
+
// is documented in STATUS.md "Open TODOs".
|
|
789
|
+
this._resolveSniContext(null, (ctx) => {
|
|
790
|
+
const certificate = ctx.certificate ?? this._tlsCertificate;
|
|
791
|
+
if (!certificate) {
|
|
792
|
+
const err = new Error('SNI resolution returned no certificate');
|
|
793
|
+
this.emit('tlsClientError', err, socket);
|
|
794
|
+
socket.destroy();
|
|
795
|
+
return;
|
|
425
796
|
}
|
|
426
797
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
798
|
+
try {
|
|
799
|
+
const tlsConn = Gio.TlsServerConnection.new(
|
|
800
|
+
rawConnection as unknown as Gio.IOStream,
|
|
801
|
+
certificate,
|
|
431
802
|
);
|
|
432
|
-
}
|
|
433
803
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
804
|
+
// Client-cert / mTLS configuration
|
|
805
|
+
if (this._tlsOptions.requestCert) {
|
|
806
|
+
tlsConn.authenticationMode = this._tlsOptions.rejectUnauthorized !== false
|
|
807
|
+
? Gio.TlsAuthenticationMode.REQUIRED
|
|
808
|
+
: Gio.TlsAuthenticationMode.REQUESTED;
|
|
809
|
+
} else {
|
|
810
|
+
tlsConn.authenticationMode = Gio.TlsAuthenticationMode.NONE;
|
|
440
811
|
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Perform TLS handshake
|
|
444
|
-
const cancellable = new Gio.Cancellable();
|
|
445
|
-
(tlsConn as Gio.TlsConnection).handshake_async(
|
|
446
|
-
GLib.PRIORITY_DEFAULT,
|
|
447
|
-
cancellable,
|
|
448
|
-
(_source: Gio.TlsConnection | null, asyncResult: Gio.AsyncResult) => {
|
|
449
|
-
try {
|
|
450
|
-
(tlsConn as Gio.TlsConnection).handshake_finish(asyncResult);
|
|
451
812
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
813
|
+
const requireClientCert = !!this._tlsOptions.requestCert
|
|
814
|
+
&& this._tlsOptions.rejectUnauthorized !== false;
|
|
815
|
+
const clientCAs = this._secureContext.caCertificates;
|
|
816
|
+
|
|
817
|
+
tlsConn.connect('accept-certificate', (
|
|
818
|
+
_conn: Gio.TlsConnection,
|
|
819
|
+
peerCert: Gio.TlsCertificate,
|
|
820
|
+
_errors: Gio.TlsCertificateFlags,
|
|
821
|
+
): boolean => {
|
|
822
|
+
if (!requireClientCert) return true;
|
|
823
|
+
if (clientCAs.length === 0) return false;
|
|
824
|
+
for (const ca of clientCAs) {
|
|
825
|
+
try {
|
|
826
|
+
const flags = peerCert.verify(null, ca);
|
|
827
|
+
if (flags === Gio.TlsCertificateFlags.NO_FLAGS) return true;
|
|
828
|
+
} catch { /* try next */ }
|
|
829
|
+
}
|
|
830
|
+
return false;
|
|
831
|
+
});
|
|
463
832
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
this.
|
|
468
|
-
|
|
833
|
+
// ALPN
|
|
834
|
+
if (this._tlsOptions.ALPNProtocols && this._tlsOptions.ALPNProtocols.length > 0) {
|
|
835
|
+
try {
|
|
836
|
+
tlsConn.set_advertised_protocols(this._tlsOptions.ALPNProtocols);
|
|
837
|
+
} catch {
|
|
838
|
+
// ALPN may not be supported
|
|
469
839
|
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const cancellable = new Gio.Cancellable();
|
|
843
|
+
tlsConn.handshake_async(
|
|
844
|
+
GLib.PRIORITY_DEFAULT,
|
|
845
|
+
cancellable,
|
|
846
|
+
(_source: Gio.TlsConnection | null, asyncResult: Gio.AsyncResult) => {
|
|
847
|
+
try {
|
|
848
|
+
tlsConn.handshake_finish(asyncResult);
|
|
849
|
+
|
|
850
|
+
const tlsSocket = new TLSSocket();
|
|
851
|
+
tlsSocket.encrypted = true;
|
|
852
|
+
tlsSocket.authorized = true;
|
|
853
|
+
tlsSocket._secureContext = ctx;
|
|
854
|
+
tlsSocket._setupTlsStreams(tlsConn);
|
|
855
|
+
tlsSocket.alpnProtocol = tlsSocket.getAlpnProtocol();
|
|
856
|
+
|
|
857
|
+
const internals = tlsSocket as unknown as SocketInternals;
|
|
858
|
+
internals._startReading();
|
|
859
|
+
|
|
860
|
+
this.emit('secureConnection', tlsSocket);
|
|
861
|
+
} catch (err: unknown) {
|
|
862
|
+
const nodeErr = createNodeError(err, 'handshake', {});
|
|
863
|
+
this.emit('tlsClientError', nodeErr, socket);
|
|
864
|
+
socket.destroy();
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
);
|
|
868
|
+
} catch (err: unknown) {
|
|
869
|
+
const nodeErr = createNodeError(err, 'tls_wrap', {});
|
|
870
|
+
this.emit('tlsClientError', nodeErr, socket);
|
|
871
|
+
socket.destroy();
|
|
872
|
+
}
|
|
873
|
+
});
|
|
477
874
|
}
|
|
478
875
|
}
|
|
479
876
|
|
|
@@ -494,7 +891,7 @@ export function createServer(
|
|
|
494
891
|
|
|
495
892
|
export { TLSServer as Server };
|
|
496
893
|
|
|
497
|
-
|
|
894
|
+
const tlsExports = {
|
|
498
895
|
TLSSocket,
|
|
499
896
|
TLSServer,
|
|
500
897
|
Server: TLSServer,
|
|
@@ -508,3 +905,5 @@ export default {
|
|
|
508
905
|
DEFAULT_MAX_VERSION,
|
|
509
906
|
DEFAULT_CIPHERS,
|
|
510
907
|
};
|
|
908
|
+
|
|
909
|
+
export default tlsExports;
|