node-rtc-connection 1.0.18 → 2.0.4

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.
Files changed (65) hide show
  1. package/README.md +94 -85
  2. package/dist/index.cjs +20 -5421
  3. package/dist/index.mjs +25 -5413
  4. package/dist/types/crypto/der.d.ts +107 -0
  5. package/dist/types/crypto/x509.d.ts +56 -0
  6. package/dist/types/datachannel/RTCDataChannel.d.ts +179 -0
  7. package/dist/types/dtls/RTCCertificate.d.ts +163 -0
  8. package/dist/types/dtls/cipher.d.ts +81 -0
  9. package/dist/types/dtls/connection.d.ts +81 -0
  10. package/dist/types/dtls/prf.d.ts +29 -0
  11. package/dist/types/dtls/protocol.d.ts +127 -0
  12. package/dist/types/foundation/ByteBufferQueue.d.ts +71 -0
  13. package/dist/types/foundation/RTCError.d.ts +152 -0
  14. package/dist/types/ice/RTCIceCandidate.d.ts +161 -0
  15. package/dist/types/ice/ice-agent.d.ts +154 -0
  16. package/dist/types/ice/stun-message.d.ts +92 -0
  17. package/dist/types/index.d.ts +29 -0
  18. package/dist/types/peerconnection/RTCPeerConnection.d.ts +74 -0
  19. package/dist/types/sctp/association.d.ts +77 -0
  20. package/dist/types/sctp/chunks.d.ts +200 -0
  21. package/dist/types/sctp/crc32c.d.ts +24 -0
  22. package/dist/types/sctp/datachannel-manager.d.ts +51 -0
  23. package/dist/types/sctp/dcep.d.ts +56 -0
  24. package/dist/types/sdp/RTCSessionDescription.d.ts +73 -0
  25. package/dist/types/sdp/sdp-utils.d.ts +103 -0
  26. package/dist/types/stun/stun-client.d.ts +119 -0
  27. package/dist/types/transport-stack.d.ts +68 -0
  28. package/package.json +26 -21
  29. package/src/crypto/der.ts +205 -0
  30. package/src/crypto/x509.ts +146 -0
  31. package/src/datachannel/RTCDataChannel.ts +388 -0
  32. package/src/dtls/RTCCertificate.ts +396 -0
  33. package/src/dtls/cipher.ts +198 -0
  34. package/src/dtls/connection.ts +974 -0
  35. package/src/dtls/prf.ts +62 -0
  36. package/src/dtls/protocol.ts +204 -0
  37. package/src/foundation/{ByteBufferQueue.js → ByteBufferQueue.ts} +74 -72
  38. package/src/foundation/{RTCError.js → RTCError.ts} +110 -60
  39. package/src/ice/{RTCIceCandidate.js → RTCIceCandidate.ts} +140 -92
  40. package/src/ice/ice-agent.ts +609 -0
  41. package/src/ice/stun-message.ts +260 -0
  42. package/src/index.ts +72 -0
  43. package/src/peerconnection/RTCPeerConnection.ts +430 -0
  44. package/src/sctp/association.ts +523 -0
  45. package/src/sctp/chunks.ts +350 -0
  46. package/src/sctp/crc32c.ts +57 -0
  47. package/src/sctp/datachannel-manager.ts +187 -0
  48. package/src/sctp/dcep.ts +94 -0
  49. package/src/sdp/{RTCSessionDescription.js → RTCSessionDescription.ts} +42 -29
  50. package/src/sdp/sdp-utils.ts +229 -0
  51. package/src/stun/stun-client.ts +936 -0
  52. package/src/transport-stack.ts +165 -0
  53. package/dist/index.cjs.map +0 -1
  54. package/dist/index.mjs.map +0 -1
  55. package/src/datachannel/RTCDataChannel.js +0 -354
  56. package/src/dtls/RTCCertificate.js +0 -310
  57. package/src/dtls/RTCDtlsTransport.js +0 -247
  58. package/src/ice/RTCIceTransport.js +0 -998
  59. package/src/index.d.ts +0 -400
  60. package/src/index.js +0 -92
  61. package/src/network/network-transport.js +0 -478
  62. package/src/peerconnection/RTCPeerConnection.js +0 -851
  63. package/src/sctp/RTCSctpTransport.js +0 -253
  64. package/src/sdp/sdp-utils.js +0 -224
  65. package/src/stun/stun-client.js +0 -643
@@ -0,0 +1,974 @@
1
+ /**
2
+ * @file connection.ts
3
+ * @description DTLS 1.2 connection state machine (client and server roles).
4
+ * @module dtls/connection
5
+ *
6
+ * Implements the handshake for TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 over an
7
+ * abstract datagram channel. The owner supplies an `output` callback to send
8
+ * datagrams and feeds inbound datagrams to `handlePacket`. On success the
9
+ * connection emits 'connect'; application records arrive via 'data' and are
10
+ * sent via `send`.
11
+ *
12
+ * Scope: one cipher suite, secp256r1, ECDSA P-256 certificates, extended
13
+ * master secret. This is the subset Chromium/Firefox negotiate for data
14
+ * channels, so it interoperates with browsers while staying pure-Node.
15
+ *
16
+ * References: RFC 6347 (DTLS 1.2), RFC 5246 (TLS 1.2), RFC 7627 (EMS),
17
+ * RFC 8422 (ECC cipher suites), RFC 5288 (AES-GCM).
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ import * as crypto from 'crypto';
23
+ import { EventEmitter } from 'events';
24
+ import * as P from './protocol';
25
+ import { prf } from './prf';
26
+ import * as cipher from './cipher';
27
+ import * as x509 from '../crypto/x509';
28
+
29
+ const HANDSHAKE_TIMEOUT_MS = 1000; // initial retransmit timer (doubles per RFC 6347)
30
+ const MAX_RETRANSMITS = 10;
31
+ const MAX_FRAGMENT = 1200; // keep handshake fragments inside a typical MTU
32
+
33
+ const ROLE = Object.freeze({ CLIENT: 'client', SERVER: 'server' });
34
+
35
+ const STATE = Object.freeze({
36
+ NEW: 'new',
37
+ HANDSHAKING: 'handshaking',
38
+ CONNECTED: 'connected',
39
+ CLOSED: 'closed',
40
+ FAILED: 'failed',
41
+ });
42
+
43
+ /** A certificate fingerprint as advertised in SDP a=fingerprint. */
44
+ interface Fingerprint {
45
+ algorithm: string;
46
+ value: string;
47
+ }
48
+
49
+ /** Callback used to verify the peer's certificate fingerprint. */
50
+ type VerifyFingerprint = (
51
+ fp: Fingerprint,
52
+ remoteCertDer: Buffer
53
+ ) => boolean;
54
+
55
+ /** Constructor options for {@link DtlsConnection}. */
56
+ interface DtlsConnectionOptions {
57
+ /** 'client' | 'server' */
58
+ role: string;
59
+ /** local DER certificate */
60
+ certDer: Buffer;
61
+ /** local EC private key */
62
+ privateKey: crypto.KeyObject;
63
+ /** called with the peer cert fingerprint; return false to reject. */
64
+ verifyFingerprint?: VerifyFingerprint;
65
+ /** send a datagram to the peer */
66
+ output: (datagram: Buffer) => void;
67
+ }
68
+
69
+ /** A handshake message queued for sending: a type plus its body. */
70
+ interface HandshakeMessage {
71
+ type: number;
72
+ body: Buffer;
73
+ }
74
+
75
+ /** A record-layer datagram queued in the current flight. */
76
+ interface FlightDatagram {
77
+ type: number;
78
+ payload: Buffer;
79
+ }
80
+
81
+ /** State for reassembling a fragmented inbound handshake message. */
82
+ interface ReassemblyEntry {
83
+ type: number;
84
+ length: number;
85
+ data: Buffer;
86
+ received: number;
87
+ ranges: Array<[number, number]>;
88
+ }
89
+
90
+ /**
91
+ * @class DtlsConnection
92
+ * @extends EventEmitter
93
+ */
94
+ class DtlsConnection extends EventEmitter {
95
+ role: string;
96
+ state: string;
97
+
98
+ #certDer: Buffer;
99
+ #privateKey: crypto.KeyObject;
100
+ #verifyFingerprint: VerifyFingerprint | null;
101
+ #output: (datagram: Buffer) => void;
102
+
103
+ // Record layer state.
104
+ #sendEpoch: number;
105
+ #sendSeq: number;
106
+ #handshakeMessageSeq: number;
107
+
108
+ // Cipher state (set after key derivation).
109
+ #writeCipher: cipher.GcmCipher | null;
110
+ #readCipher: cipher.GcmCipher | null;
111
+ #sendEncrypted: boolean;
112
+
113
+ // Handshake crypto material.
114
+ #clientRandom: Buffer | null;
115
+ #serverRandom: Buffer | null;
116
+ #cookie: Buffer;
117
+ #ecdh: crypto.ECDH | null;
118
+ #masterSecret: Buffer | null;
119
+ #remoteCertDer: Buffer | null;
120
+ #remoteEcdhePub: Buffer | null;
121
+ #useExtendedMasterSecret: boolean;
122
+
123
+ #transcript: Buffer[];
124
+
125
+ #reassembly: Map<number, ReassemblyEntry>;
126
+ #nextExpectedHsSeq: number;
127
+
128
+ #lastFlight: FlightDatagram[];
129
+ #retransmitTimer: NodeJS.Timeout | null;
130
+ #retransmitCount: number;
131
+
132
+ #handshakeDone: boolean;
133
+
134
+ #cookieSecret?: Buffer;
135
+ #renegRequested?: boolean;
136
+
137
+ /**
138
+ * @param opts connection options
139
+ */
140
+ constructor(opts: DtlsConnectionOptions) {
141
+ super();
142
+ this.role = opts.role;
143
+ this.#certDer = opts.certDer;
144
+ this.#privateKey = opts.privateKey;
145
+ this.#verifyFingerprint = opts.verifyFingerprint || null;
146
+ this.#output = opts.output;
147
+
148
+ this.state = STATE.NEW;
149
+
150
+ // Record layer state.
151
+ this.#sendEpoch = 0;
152
+ this.#sendSeq = 0; // 48-bit record seq within current epoch
153
+ this.#handshakeMessageSeq = 0;
154
+
155
+ // Cipher state (set after key derivation).
156
+ this.#writeCipher = null; // GcmCipher for our outbound epoch-1 records
157
+ this.#readCipher = null; // GcmCipher for inbound epoch-1 records
158
+ this.#sendEncrypted = false; // becomes true after we send ChangeCipherSpec
159
+
160
+ // Handshake crypto material.
161
+ this.#clientRandom = null;
162
+ this.#serverRandom = null;
163
+ this.#cookie = Buffer.alloc(0);
164
+ this.#ecdh = null; // local ECDH keypair
165
+ this.#masterSecret = null;
166
+ this.#remoteCertDer = null;
167
+ this.#remoteEcdhePub = null; // peer ECDHE public point (server role)
168
+ this.#useExtendedMasterSecret = false;
169
+
170
+ // Transcript of handshake messages (DTLS 12-byte header + body), used for
171
+ // Finished / CertificateVerify / EMS. Excludes HelloVerifyRequest and the
172
+ // first (cookieless) ClientHello, per RFC 6347.
173
+ this.#transcript = [];
174
+
175
+ // Reassembly of inbound fragmented handshake messages, keyed by msg seq.
176
+ this.#reassembly = new Map();
177
+ this.#nextExpectedHsSeq = 0;
178
+
179
+ // Last flight we sent, for retransmission.
180
+ this.#lastFlight = [];
181
+ this.#retransmitTimer = null;
182
+ this.#retransmitCount = 0;
183
+
184
+ this.#handshakeDone = false;
185
+ }
186
+
187
+ /** Begin the handshake (client sends the first flight). */
188
+ start(): void {
189
+ if (this.state !== STATE.NEW) return;
190
+ this.state = STATE.HANDSHAKING;
191
+ if (this.role === ROLE.CLIENT) {
192
+ this.#clientRandom = this.#makeRandom();
193
+ this.#sendClientHello();
194
+ }
195
+ // Server waits for ClientHello.
196
+ }
197
+
198
+ /** 32-byte Random (4-byte gmt_unix_time || 28 random). */
199
+ #makeRandom(): Buffer {
200
+ const r = crypto.randomBytes(32);
201
+ r.writeUInt32BE(Math.floor(Date.now() / 1000), 0);
202
+ return r;
203
+ }
204
+
205
+ // ---- Outbound record/handshake plumbing ---------------------------------
206
+
207
+ /**
208
+ * Emit a set of handshake messages as one flight: fragment, frame as records,
209
+ * append to transcript, and arm retransmission.
210
+ * @param messages
211
+ */
212
+ #sendFlight(messages: HandshakeMessage[]): void {
213
+ const datagrams: FlightDatagram[] = [];
214
+ for (const msg of messages) {
215
+ const seq = this.#handshakeMessageSeq++;
216
+ // Full (unfragmented) message goes into the transcript hash.
217
+ const full = P.encodeHandshake(msg.type, seq, msg.body);
218
+ this.#transcript.push(full);
219
+
220
+ // Fragment the handshake body across records if large.
221
+ const total = msg.body.length;
222
+ let offset = 0;
223
+ do {
224
+ const chunk = msg.body.slice(offset, offset + MAX_FRAGMENT);
225
+ const hdr = Buffer.alloc(12);
226
+ hdr.writeUInt8(msg.type, 0);
227
+ P.uint24(total).copy(hdr, 1);
228
+ hdr.writeUInt16BE(seq, 4);
229
+ P.uint24(offset).copy(hdr, 6);
230
+ P.uint24(chunk.length).copy(hdr, 9);
231
+ const fragment = Buffer.concat([hdr, chunk]);
232
+ datagrams.push({ type: P.CONTENT_TYPE.HANDSHAKE, payload: fragment });
233
+ offset += chunk.length;
234
+ } while (offset < total);
235
+ }
236
+
237
+ this.#lastFlight = datagrams;
238
+ this.#retransmitCount = 0;
239
+ this.#flushFlight();
240
+ this.#armRetransmit();
241
+ }
242
+
243
+ /** Encode each queued message as a record and send. */
244
+ #flushFlight(): void {
245
+ for (const d of this.#lastFlight) {
246
+ this.#sendRecord(d.type, d.payload);
247
+ }
248
+ }
249
+
250
+ /** Send a ChangeCipherSpec record (epoch boundary on our side). */
251
+ #sendChangeCipherSpec(): void {
252
+ this.#sendRecord(P.CONTENT_TYPE.CHANGE_CIPHER_SPEC, Buffer.from([1]));
253
+ // After CCS, subsequent records use the new epoch and are encrypted.
254
+ this.#sendEpoch = 1;
255
+ this.#sendSeq = 0;
256
+ this.#sendEncrypted = true;
257
+ }
258
+
259
+ /**
260
+ * Frame a payload as a DTLS record, encrypting if we're past CCS.
261
+ * @param type
262
+ * @param payload
263
+ */
264
+ #sendRecord(type: number, payload: Buffer): void {
265
+ let fragment = payload;
266
+ const seq = this.#sendSeq++;
267
+ if (this.#sendEncrypted && this.#writeCipher) {
268
+ fragment = this.#writeCipher.encrypt(this.#sendEpoch, seq, type, P.DTLS_1_2, payload);
269
+ }
270
+ const record = P.encodeRecord(type, this.#sendEpoch, seq, fragment, P.DTLS_1_2);
271
+ this.#output(record);
272
+ }
273
+
274
+ #armRetransmit(): void {
275
+ this.#clearRetransmit();
276
+ this.#retransmitTimer = setTimeout(() => {
277
+ if (this.#handshakeDone || this.state !== STATE.HANDSHAKING) return;
278
+ if (this.#retransmitCount >= MAX_RETRANSMITS) {
279
+ this.#fail(new Error('DTLS handshake timed out'));
280
+ return;
281
+ }
282
+ this.#retransmitCount++;
283
+ this.#flushFlight();
284
+ // Exponential backoff.
285
+ this.#armRetransmit();
286
+ }, HANDSHAKE_TIMEOUT_MS * Math.pow(2, this.#retransmitCount));
287
+ // A pending retransmit must not, by itself, keep the process alive.
288
+ if (this.#retransmitTimer.unref) this.#retransmitTimer.unref();
289
+ }
290
+
291
+ #clearRetransmit(): void {
292
+ if (this.#retransmitTimer) {
293
+ clearTimeout(this.#retransmitTimer);
294
+ this.#retransmitTimer = null;
295
+ }
296
+ }
297
+
298
+ // ---- Inbound ------------------------------------------------------------
299
+
300
+ /**
301
+ * Feed an inbound datagram (one UDP packet, possibly several records).
302
+ * @param packet
303
+ */
304
+ handlePacket(packet: Buffer): void {
305
+ if (this.state === STATE.CLOSED || this.state === STATE.FAILED) return;
306
+ let records: P.Record[];
307
+ try {
308
+ records = P.parseRecords(packet);
309
+ } catch (err) {
310
+ return; // ignore malformed datagrams
311
+ }
312
+ for (const rec of records) {
313
+ try {
314
+ this.#handleRecord(rec);
315
+ } catch (err) {
316
+ this.#fail(err instanceof Error ? err : new Error(String(err)));
317
+ return;
318
+ }
319
+ }
320
+ }
321
+
322
+ #handleRecord(rec: P.Record): void {
323
+ let fragment = rec.fragment;
324
+
325
+ // Decrypt records from the peer's encrypted epoch.
326
+ if (rec.epoch >= 1) {
327
+ if (!this.#readCipher) {
328
+ // Can't decrypt yet (keys not derived) — drop.
329
+ return;
330
+ }
331
+ fragment = this.#readCipher.decrypt(rec.epoch, rec.seq, rec.type, rec.version, rec.fragment);
332
+ }
333
+
334
+ switch (rec.type) {
335
+ case P.CONTENT_TYPE.HANDSHAKE:
336
+ this.#handleHandshakeFragment(fragment);
337
+ break;
338
+ case P.CONTENT_TYPE.CHANGE_CIPHER_SPEC:
339
+ // Peer switched to its encrypted epoch; records now carry epoch 1,
340
+ // which _handleRecord already routes through the read cipher.
341
+ break;
342
+ case P.CONTENT_TYPE.APPLICATION_DATA:
343
+ if (this.state === STATE.CONNECTED) this.emit('data', fragment);
344
+ break;
345
+ case P.CONTENT_TYPE.ALERT:
346
+ this.#handleAlert(fragment);
347
+ break;
348
+ default:
349
+ break;
350
+ }
351
+ }
352
+
353
+ #handleAlert(fragment: Buffer): void {
354
+ if (fragment.length < 2) return;
355
+ const level = fragment[0]!;
356
+ const desc = fragment[1]!;
357
+ if (desc === P.ALERT_DESC.CLOSE_NOTIFY) {
358
+ this.close();
359
+ } else if (level === P.ALERT_LEVEL.FATAL) {
360
+ this.#fail(new Error(`DTLS fatal alert: ${desc}`));
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Reassemble a (possibly fragmented) handshake message, then dispatch
366
+ * complete messages in order.
367
+ * @param buf - one handshake fragment (12-byte header + chunk)
368
+ */
369
+ #handleHandshakeFragment(buf: Buffer): void {
370
+ const h = P.parseHandshake(buf);
371
+
372
+ // Initialize / fetch reassembly buffer for this message_seq.
373
+ let entry = this.#reassembly.get(h.messageSeq);
374
+ if (!entry) {
375
+ entry = { type: h.msgType, length: h.length, data: Buffer.alloc(h.length), received: 0, ranges: [] };
376
+ this.#reassembly.set(h.messageSeq, entry);
377
+ }
378
+ // Copy this fragment into place (ignore duplicates/overlap simply).
379
+ h.body.copy(entry.data, h.fragmentOffset);
380
+ entry.received = Math.max(entry.received, h.fragmentOffset + h.fragmentLength);
381
+
382
+ // Dispatch any in-order, fully-received messages.
383
+ while (true) {
384
+ const next = this.#reassembly.get(this.#nextExpectedHsSeq);
385
+ if (!next || next.received < next.length) break;
386
+ this.#reassembly.delete(this.#nextExpectedHsSeq);
387
+ this.#nextExpectedHsSeq++;
388
+ this.#dispatchHandshake(next.type, next.data);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Add a received handshake message to the transcript (reconstructed as a
394
+ * single unfragmented message, per RFC 6347 §4.2.6).
395
+ */
396
+ #appendInboundTranscript(type: number, body: Buffer): void {
397
+ const seq = this.#nextExpectedHsSeq - 1; // message_seq just consumed
398
+ this.#transcript.push(P.encodeHandshake(type, seq, body));
399
+ }
400
+
401
+ #dispatchHandshake(type: number, body: Buffer): void {
402
+ if (this.role === ROLE.CLIENT) {
403
+ this.#clientHandle(type, body);
404
+ } else {
405
+ this.#serverHandle(type, body);
406
+ }
407
+ }
408
+
409
+ #transcriptHash(): Buffer {
410
+ const h = crypto.createHash('sha256');
411
+ for (const m of this.#transcript) h.update(m);
412
+ return h.digest();
413
+ }
414
+
415
+ /** Raw concatenation of all transcript handshake messages (for signing). */
416
+ #transcriptBytes(): Buffer {
417
+ return Buffer.concat(this.#transcript);
418
+ }
419
+
420
+ // ---- CLIENT role --------------------------------------------------------
421
+
422
+ #sendClientHello(): void {
423
+ const body = this.#buildClientHello();
424
+ if (this.#cookie.length === 0) {
425
+ // First ClientHello is excluded from the transcript: send as a raw
426
+ // handshake record without recording it, and without retransmit arming
427
+ // beyond the cookie exchange.
428
+ const seq = this.#handshakeMessageSeq++; // message_seq 0
429
+ const fragment = P.encodeHandshake(P.HANDSHAKE_TYPE.CLIENT_HELLO, seq, body);
430
+ this.#lastFlight = [{ type: P.CONTENT_TYPE.HANDSHAKE, payload: fragment }];
431
+ this.#flushFlight();
432
+ this.#armRetransmit();
433
+ } else {
434
+ // Second ClientHello (with cookie) starts the real transcript.
435
+ // It reuses message_seq = 1.
436
+ const seq = this.#handshakeMessageSeq++; // message_seq 1
437
+ const full = P.encodeHandshake(P.HANDSHAKE_TYPE.CLIENT_HELLO, seq, body);
438
+ this.#transcript.push(full);
439
+ this.#lastFlight = [{ type: P.CONTENT_TYPE.HANDSHAKE, payload: full }];
440
+ this.#retransmitCount = 0;
441
+ this.#flushFlight();
442
+ this.#armRetransmit();
443
+ }
444
+ }
445
+
446
+ #buildClientHello(): Buffer {
447
+ const parts: Buffer[] = [];
448
+ parts.push(Buffer.from([0xfe, 0xfd])); // client_version DTLS 1.2
449
+ parts.push(this.#clientRandom!);
450
+ parts.push(P.vec8(Buffer.alloc(0))); // session_id (empty)
451
+ parts.push(P.vec8(this.#cookie)); // cookie
452
+ // cipher_suites
453
+ const cs = Buffer.alloc(2);
454
+ cs.writeUInt16BE(P.CIPHER_SUITE, 0);
455
+ parts.push(P.vec16(cs));
456
+ // compression_methods: null only
457
+ parts.push(P.vec8(Buffer.from([0x00])));
458
+ // extensions
459
+ parts.push(P.vec16(this.#buildClientExtensions()));
460
+ return Buffer.concat(parts);
461
+ }
462
+
463
+ #buildClientExtensions(): Buffer {
464
+ const exts: Buffer[] = [];
465
+
466
+ // supported_groups: secp256r1
467
+ const groups = Buffer.alloc(2);
468
+ groups.writeUInt16BE(P.NAMED_GROUP.secp256r1, 0);
469
+ exts.push(this.#ext(P.EXTENSION.SUPPORTED_GROUPS, P.vec16(groups)));
470
+
471
+ // ec_point_formats: uncompressed
472
+ exts.push(this.#ext(P.EXTENSION.EC_POINT_FORMATS, P.vec8(Buffer.from([P.EC_POINT_FORMAT.uncompressed]))));
473
+
474
+ // signature_algorithms: ecdsa_secp256r1_sha256
475
+ const sigalgs = Buffer.from([P.HASH_ALG.sha256, P.SIG_ALG.ecdsa]);
476
+ exts.push(this.#ext(P.EXTENSION.SIGNATURE_ALGORITHMS, P.vec16(sigalgs)));
477
+
478
+ // extended_master_secret (empty)
479
+ exts.push(this.#ext(P.EXTENSION.EXTENDED_MASTER_SECRET, Buffer.alloc(0)));
480
+
481
+ return Buffer.concat(exts);
482
+ }
483
+
484
+ #ext(type: number, body: Buffer): Buffer {
485
+ const head = Buffer.alloc(4);
486
+ head.writeUInt16BE(type, 0);
487
+ head.writeUInt16BE(body.length, 2);
488
+ return Buffer.concat([head, body]);
489
+ }
490
+
491
+ #clientHandle(type: number, body: Buffer): void {
492
+ switch (type) {
493
+ case P.HANDSHAKE_TYPE.HELLO_VERIFY_REQUEST: {
494
+ // Extract cookie and resend ClientHello. Not added to transcript.
495
+ // body: server_version(2) || cookie<0..255>
496
+ const cookieLen = body.readUInt8(2);
497
+ this.#cookie = body.slice(3, 3 + cookieLen);
498
+ this.#clearRetransmit();
499
+ // Reset message seq: RFC 6347 — second ClientHello has message_seq 1.
500
+ this.#sendClientHello();
501
+ break;
502
+ }
503
+ case P.HANDSHAKE_TYPE.SERVER_HELLO:
504
+ this.#appendInboundTranscript(type, body);
505
+ this.#parseServerHello(body);
506
+ break;
507
+ case P.HANDSHAKE_TYPE.CERTIFICATE:
508
+ this.#appendInboundTranscript(type, body);
509
+ this.#remoteCertDer = this.#parseCertificate(body);
510
+ break;
511
+ case P.HANDSHAKE_TYPE.SERVER_KEY_EXCHANGE:
512
+ this.#appendInboundTranscript(type, body);
513
+ this.#parseServerKeyExchange(body);
514
+ break;
515
+ case P.HANDSHAKE_TYPE.CERTIFICATE_REQUEST:
516
+ // We always send our certificate (WebRTC is mutual-auth), so the
517
+ // request only needs to be folded into the transcript.
518
+ this.#appendInboundTranscript(type, body);
519
+ break;
520
+ case P.HANDSHAKE_TYPE.SERVER_HELLO_DONE:
521
+ this.#appendInboundTranscript(type, body);
522
+ this.#clearRetransmit();
523
+ this.#sendClientSecondFlight();
524
+ break;
525
+ case P.HANDSHAKE_TYPE.FINISHED:
526
+ this.#verifyPeerFinished(body, P.FINISHED_LABEL.SERVER);
527
+ this.#appendInboundTranscript(type, body);
528
+ this.#onHandshakeComplete();
529
+ break;
530
+ default:
531
+ break;
532
+ }
533
+ }
534
+
535
+ #parseServerHello(body: Buffer): void {
536
+ // server_version(2) || random(32) || session_id<vec8> || cipher_suite(2)
537
+ // || compression(1) || extensions<vec16>
538
+ let o = 2;
539
+ this.#serverRandom = body.slice(o, o + 32);
540
+ o += 32;
541
+ const sidLen = body.readUInt8(o);
542
+ o += 1 + sidLen;
543
+ const suite = body.readUInt16BE(o);
544
+ o += 2;
545
+ if (suite !== P.CIPHER_SUITE) {
546
+ throw new Error(`Server chose unsupported cipher suite 0x${suite.toString(16)}`);
547
+ }
548
+ o += 1; // compression
549
+ // Parse extensions for extended_master_secret.
550
+ if (o + 2 <= body.length) {
551
+ const extLen = body.readUInt16BE(o);
552
+ o += 2;
553
+ const end = o + extLen;
554
+ while (o + 4 <= end) {
555
+ const etype = body.readUInt16BE(o);
556
+ const elen = body.readUInt16BE(o + 2);
557
+ o += 4;
558
+ if (etype === P.EXTENSION.EXTENDED_MASTER_SECRET) {
559
+ this.#useExtendedMasterSecret = true;
560
+ }
561
+ o += elen;
562
+ }
563
+ }
564
+ }
565
+
566
+ #sendClientSecondFlight(): void {
567
+ // Generate our ECDHE key.
568
+ this.#ecdh = crypto.createECDH('prime256v1');
569
+ this.#ecdh.generateKeys();
570
+ const clientPub = this.#ecdh.getPublicKey(); // uncompressed point (65 bytes)
571
+
572
+ // Compute pre-master secret = ECDH(serverPub).
573
+ const pms = this.#ecdh.computeSecret(this.#remoteEcdhePub!);
574
+
575
+ // client Certificate
576
+ const certMsg = this.#buildCertificateMessage();
577
+ // ClientKeyExchange: ECPoint as vec8
578
+ const cke = P.vec8(clientPub);
579
+
580
+ // Build the messages we're about to send so the transcript is correct for
581
+ // CertificateVerify (which signs everything through ClientKeyExchange) and
582
+ // for the master secret (EMS hashes through ClientKeyExchange).
583
+ const certSeq = this.#handshakeMessageSeq;
584
+ const ckeSeq = certSeq + 1;
585
+ const certFull = P.encodeHandshake(P.HANDSHAKE_TYPE.CERTIFICATE, certSeq, certMsg);
586
+ const ckeFull = P.encodeHandshake(P.HANDSHAKE_TYPE.CLIENT_KEY_EXCHANGE, ckeSeq, cke);
587
+
588
+ // Master secret derivation.
589
+ if (this.#useExtendedMasterSecret) {
590
+ const h = crypto.createHash('sha256');
591
+ for (const m of this.#transcript) h.update(m);
592
+ h.update(certFull);
593
+ h.update(ckeFull);
594
+ const sessionHash = h.digest();
595
+ this.#masterSecret = cipher.deriveExtendedMasterSecret(pms, sessionHash);
596
+ } else {
597
+ this.#masterSecret = cipher.deriveMasterSecret(pms, this.#clientRandom!, this.#serverRandom!);
598
+ }
599
+ this.#deriveCipherKeys();
600
+
601
+ // CertificateVerify: sign the raw handshake transcript through
602
+ // ClientKeyExchange. crypto.sign applies SHA-256 itself, so we feed it the
603
+ // concatenated messages, not a pre-computed digest.
604
+ const cvData = Buffer.concat([...this.#transcript, certFull, ckeFull]);
605
+ const cvSig = crypto.sign('sha256', cvData, { key: this.#privateKey, dsaEncoding: 'der' });
606
+ const cvBody = Buffer.concat([
607
+ Buffer.from([P.HASH_ALG.sha256, P.SIG_ALG.ecdsa]),
608
+ P.vec16(cvSig),
609
+ ]);
610
+
611
+ // Now actually send: Certificate, ClientKeyExchange, CertificateVerify
612
+ // as a flight (these get recorded in transcript by _sendFlight), then CCS,
613
+ // then Finished.
614
+ this.#sendFlight([
615
+ { type: P.HANDSHAKE_TYPE.CERTIFICATE, body: certMsg },
616
+ { type: P.HANDSHAKE_TYPE.CLIENT_KEY_EXCHANGE, body: cke },
617
+ { type: P.HANDSHAKE_TYPE.CERTIFICATE_VERIFY, body: cvBody },
618
+ ]);
619
+
620
+ this.#sendChangeCipherSpec();
621
+ this.#sendFinished(P.FINISHED_LABEL.CLIENT);
622
+ }
623
+
624
+ // ---- SERVER role --------------------------------------------------------
625
+
626
+ #serverHandle(type: number, body: Buffer): void {
627
+ switch (type) {
628
+ case P.HANDSHAKE_TYPE.CLIENT_HELLO:
629
+ this.#handleClientHello(body);
630
+ break;
631
+ case P.HANDSHAKE_TYPE.CERTIFICATE:
632
+ this.#appendInboundTranscript(type, body);
633
+ this.#remoteCertDer = this.#parseCertificate(body);
634
+ break;
635
+ case P.HANDSHAKE_TYPE.CLIENT_KEY_EXCHANGE: {
636
+ this.#appendInboundTranscript(type, body);
637
+ const pubLen = body.readUInt8(0);
638
+ this.#remoteEcdhePub = body.slice(1, 1 + pubLen);
639
+ const pms = this.#ecdh!.computeSecret(this.#remoteEcdhePub);
640
+ if (this.#useExtendedMasterSecret) {
641
+ this.#masterSecret = cipher.deriveExtendedMasterSecret(pms, this.#transcriptHash());
642
+ } else {
643
+ this.#masterSecret = cipher.deriveMasterSecret(pms, this.#clientRandom!, this.#serverRandom!);
644
+ }
645
+ this.#deriveCipherKeys();
646
+ break;
647
+ }
648
+ case P.HANDSHAKE_TYPE.CERTIFICATE_VERIFY:
649
+ this.#verifyClientCertificateVerify(body);
650
+ this.#appendInboundTranscript(type, body);
651
+ break;
652
+ case P.HANDSHAKE_TYPE.FINISHED:
653
+ this.#verifyPeerFinished(body, P.FINISHED_LABEL.CLIENT);
654
+ this.#appendInboundTranscript(type, body);
655
+ // Server responds with its own CCS + Finished.
656
+ this.#sendChangeCipherSpec();
657
+ this.#sendFinished(P.FINISHED_LABEL.SERVER);
658
+ this.#onHandshakeComplete();
659
+ break;
660
+ default:
661
+ break;
662
+ }
663
+ }
664
+
665
+ #handleClientHello(body: Buffer): void {
666
+ // Parse enough to extract random, cookie, and extensions.
667
+ let o = 2; // skip client_version
668
+ const random = body.slice(o, o + 32);
669
+ o += 32;
670
+ const sidLen = body.readUInt8(o);
671
+ o += 1 + sidLen;
672
+ const cookieLen = body.readUInt8(o);
673
+ const cookie = body.slice(o + 1, o + 1 + cookieLen);
674
+ o += 1 + cookieLen;
675
+ const csLen = body.readUInt16BE(o);
676
+ const cipherSuites = body.slice(o + 2, o + 2 + csLen);
677
+ o += 2 + csLen;
678
+ const compLen = body.readUInt8(o);
679
+ o += 1 + compLen;
680
+ // Extensions
681
+ let emsRequested = false;
682
+ // Secure renegotiation (RFC 5746): the client signals support via the
683
+ // renegotiation_info extension or the SCSV cipher (0x00FF). OpenSSL 3.x
684
+ // requires the server to acknowledge it, or it aborts with
685
+ // handshake_failure; older OpenSSL and browsers tolerate its absence.
686
+ let renegRequested = false;
687
+ for (let i = 0; i + 1 < cipherSuites.length; i += 2) {
688
+ if (cipherSuites.readUInt16BE(i) === 0x00ff) renegRequested = true;
689
+ }
690
+ if (o + 2 <= body.length) {
691
+ const extLen = body.readUInt16BE(o);
692
+ o += 2;
693
+ const end = o + extLen;
694
+ while (o + 4 <= end) {
695
+ const etype = body.readUInt16BE(o);
696
+ const elen = body.readUInt16BE(o + 2);
697
+ o += 4;
698
+ if (etype === P.EXTENSION.EXTENDED_MASTER_SECRET) emsRequested = true;
699
+ if (etype === P.EXTENSION.RENEGOTIATION_INFO) renegRequested = true;
700
+ o += elen;
701
+ }
702
+ }
703
+ this.#renegRequested = renegRequested;
704
+
705
+ if (cookie.length === 0) {
706
+ // Stateless cookie exchange: reply with HelloVerifyRequest. Not part of
707
+ // the transcript, and we do not yet commit any state.
708
+ this.#clientRandom = random;
709
+ this.#sendHelloVerifyRequest(this.#makeCookie(random));
710
+ // The client resends ClientHello as message_seq 1; expect that next.
711
+ this.#reassembly.clear();
712
+ this.#nextExpectedHsSeq = 1;
713
+ return;
714
+ }
715
+
716
+ // Validate cookie.
717
+ const expected = this.#makeCookie(random);
718
+ if (!cookie.equals(expected)) {
719
+ // Tolerate by just re-issuing HVR.
720
+ this.#sendHelloVerifyRequest(expected);
721
+ this.#reassembly.clear();
722
+ this.#nextExpectedHsSeq = 1;
723
+ return;
724
+ }
725
+
726
+ // Cookie OK — this ClientHello starts the transcript.
727
+ this.#clientRandom = random;
728
+ this.#useExtendedMasterSecret = emsRequested;
729
+ this.#appendInboundTranscript(P.HANDSHAKE_TYPE.CLIENT_HELLO, body);
730
+ this.#sendServerFlight();
731
+ }
732
+
733
+ #makeCookie(clientRandom: Buffer): Buffer {
734
+ if (!this.#cookieSecret) this.#cookieSecret = crypto.randomBytes(32);
735
+ return crypto.createHmac('sha256', this.#cookieSecret).update(clientRandom).digest().slice(0, 20);
736
+ }
737
+
738
+ #sendHelloVerifyRequest(cookie: Buffer): void {
739
+ // body: server_version(2) || cookie<vec8>
740
+ const body = Buffer.concat([Buffer.from([0xfe, 0xff]), P.vec8(cookie)]);
741
+ // HVR uses message_seq 0 and is not retransmitted via flight machinery.
742
+ const fragment = P.encodeHandshake(P.HANDSHAKE_TYPE.HELLO_VERIFY_REQUEST, 0, body);
743
+ this.#sendRecord(P.CONTENT_TYPE.HANDSHAKE, fragment);
744
+ // Server-side handshake message seq for the real flight starts at 1.
745
+ this.#handshakeMessageSeq = 1;
746
+ }
747
+
748
+ #sendServerFlight(): void {
749
+ this.#serverRandom = this.#makeRandom();
750
+ this.#ecdh = crypto.createECDH('prime256v1');
751
+ this.#ecdh.generateKeys();
752
+ const serverPub = this.#ecdh.getPublicKey();
753
+
754
+ // ServerHello
755
+ const shBody = this.#buildServerHello();
756
+
757
+ // Certificate
758
+ const certMsg = this.#buildCertificateMessage();
759
+
760
+ // ServerKeyExchange: ServerECDHParams + signature
761
+ const ecdhParams = Buffer.concat([
762
+ Buffer.from([0x03]), // curve_type = named_curve
763
+ (() => { const b = Buffer.alloc(2); b.writeUInt16BE(P.NAMED_GROUP.secp256r1, 0); return b; })(),
764
+ P.vec8(serverPub),
765
+ ]);
766
+ const signed = Buffer.concat([this.#clientRandom!, this.#serverRandom, ecdhParams]);
767
+ const sig = crypto.sign('sha256', signed, { key: this.#privateKey, dsaEncoding: 'der' });
768
+ const skeBody = Buffer.concat([
769
+ ecdhParams,
770
+ Buffer.from([P.HASH_ALG.sha256, P.SIG_ALG.ecdsa]),
771
+ P.vec16(sig),
772
+ ]);
773
+
774
+ // CertificateRequest: ask client for an ECDSA cert (WebRTC is mutual-auth).
775
+ const certTypes = P.vec8(Buffer.from([P.CERT_TYPE.ecdsa_sign, P.CERT_TYPE.rsa_sign]));
776
+ const sigAlgs = P.vec16(Buffer.from([P.HASH_ALG.sha256, P.SIG_ALG.ecdsa]));
777
+ const cas = P.vec16(Buffer.alloc(0));
778
+ const crBody = Buffer.concat([certTypes, sigAlgs, cas]);
779
+
780
+ // ServerHelloDone
781
+ const shdBody = Buffer.alloc(0);
782
+
783
+ this.#sendFlight([
784
+ { type: P.HANDSHAKE_TYPE.SERVER_HELLO, body: shBody },
785
+ { type: P.HANDSHAKE_TYPE.CERTIFICATE, body: certMsg },
786
+ { type: P.HANDSHAKE_TYPE.SERVER_KEY_EXCHANGE, body: skeBody },
787
+ { type: P.HANDSHAKE_TYPE.CERTIFICATE_REQUEST, body: crBody },
788
+ { type: P.HANDSHAKE_TYPE.SERVER_HELLO_DONE, body: shdBody },
789
+ ]);
790
+ }
791
+
792
+ #buildServerHello(): Buffer {
793
+ const parts: Buffer[] = [];
794
+ parts.push(Buffer.from([0xfe, 0xfd])); // server_version DTLS 1.2
795
+ parts.push(this.#serverRandom!);
796
+ parts.push(P.vec8(Buffer.alloc(0))); // session_id empty
797
+ const cs = Buffer.alloc(2);
798
+ cs.writeUInt16BE(P.CIPHER_SUITE, 0);
799
+ parts.push(cs); // cipher_suite (2 bytes, not a vector)
800
+ parts.push(Buffer.from([0x00])); // compression null
801
+ // extensions
802
+ const exts: Buffer[] = [];
803
+ if (this.#useExtendedMasterSecret) {
804
+ exts.push(this.#ext(P.EXTENSION.EXTENDED_MASTER_SECRET, Buffer.alloc(0)));
805
+ }
806
+ exts.push(this.#ext(P.EXTENSION.EC_POINT_FORMATS, P.vec8(Buffer.from([P.EC_POINT_FORMAT.uncompressed]))));
807
+ // Acknowledge secure renegotiation with an empty renegotiated_connection
808
+ // (a 1-byte zero-length vector). Required by OpenSSL 3.x clients.
809
+ if (this.#renegRequested) {
810
+ exts.push(this.#ext(P.EXTENSION.RENEGOTIATION_INFO, P.vec8(Buffer.alloc(0))));
811
+ }
812
+ parts.push(P.vec16(Buffer.concat(exts)));
813
+ return Buffer.concat(parts);
814
+ }
815
+
816
+ #verifyClientCertificateVerify(body: Buffer): void {
817
+ // body: SignatureAndHashAlgorithm(2) || signature<vec16>
818
+ const sigLen = body.readUInt16BE(2);
819
+ const sig = body.slice(4, 4 + sigLen);
820
+ // Verify over the raw transcript through ClientKeyExchange (crypto.verify
821
+ // hashes internally, mirroring the signer).
822
+ const data = this.#transcriptBytes();
823
+ const pub = this.#publicKeyFromCert(this.#remoteCertDer!);
824
+ const ok = crypto.verify('sha256', data, { key: pub, dsaEncoding: 'der' }, sig);
825
+ if (!ok) throw new Error('Client CertificateVerify signature invalid');
826
+ }
827
+
828
+ // ---- Shared handshake helpers ------------------------------------------
829
+
830
+ /** Build a Certificate message carrying our single DER cert. */
831
+ #buildCertificateMessage(): Buffer {
832
+ // certificate_list: each entry is cert<vec24>; whole list is vec24.
833
+ const entry = P.vec24(this.#certDer);
834
+ return P.vec24(entry);
835
+ }
836
+
837
+ /** Parse a Certificate message and return the first cert's DER. */
838
+ #parseCertificate(body: Buffer): Buffer | null {
839
+ // body: certificate_list<vec24> of cert<vec24>
840
+ const listLen = P.readUint24(body, 0);
841
+ let o = 3;
842
+ const end = 3 + listLen;
843
+ if (o + 3 > end) {
844
+ // Empty certificate list.
845
+ return null;
846
+ }
847
+ const certLen = P.readUint24(body, o);
848
+ o += 3;
849
+ const certDer = body.slice(o, o + certLen);
850
+
851
+ // Fingerprint verification against the SDP-advertised value.
852
+ if (this.#verifyFingerprint) {
853
+ const fp = { algorithm: 'sha-256', value: x509.fingerprint(certDer, 'sha-256') };
854
+ if (!this.#verifyFingerprint(fp, certDer)) {
855
+ throw new Error('Remote certificate fingerprint mismatch');
856
+ }
857
+ }
858
+ return certDer;
859
+ }
860
+
861
+ #parseServerKeyExchange(body: Buffer): void {
862
+ // ServerECDHParams: curve_type(1) || named_curve(2) || public<vec8>
863
+ // then SignatureAndHashAlgorithm(2) || signature<vec16>
864
+ let o = 0;
865
+ const curveType = body.readUInt8(o); o += 1;
866
+ const namedCurve = body.readUInt16BE(o); o += 2;
867
+ if (curveType !== 3 || namedCurve !== P.NAMED_GROUP.secp256r1) {
868
+ throw new Error('Unsupported ECDHE curve from server');
869
+ }
870
+ const pubLen = body.readUInt8(o); o += 1;
871
+ const serverPub = body.slice(o, o + pubLen); o += pubLen;
872
+ this.#remoteEcdhePub = serverPub;
873
+
874
+ // Verify the signature over client_random || server_random || ECDHParams.
875
+ const ecdhParams = body.slice(0, o);
876
+ // skip SignatureAndHashAlgorithm(2)
877
+ o += 2;
878
+ const sigLen = body.readUInt16BE(o); o += 2;
879
+ const sig = body.slice(o, o + sigLen);
880
+ const signed = Buffer.concat([this.#clientRandom!, this.#serverRandom!, ecdhParams]);
881
+ const pub = this.#publicKeyFromCert(this.#remoteCertDer!);
882
+ const ok = crypto.verify('sha256', signed, { key: pub, dsaEncoding: 'der' }, sig);
883
+ if (!ok) throw new Error('ServerKeyExchange signature invalid');
884
+ }
885
+
886
+ #publicKeyFromCert(certDer: Buffer): crypto.KeyObject {
887
+ const cert = new crypto.X509Certificate(certDer);
888
+ return cert.publicKey;
889
+ }
890
+
891
+ #deriveCipherKeys(): void {
892
+ const { clientKey, serverKey, clientIV, serverIV } = cipher.deriveKeys(
893
+ this.#masterSecret!,
894
+ this.#clientRandom!,
895
+ this.#serverRandom!
896
+ );
897
+ if (this.role === ROLE.CLIENT) {
898
+ this.#writeCipher = new cipher.GcmCipher(clientKey, clientIV);
899
+ this.#readCipher = new cipher.GcmCipher(serverKey, serverIV);
900
+ } else {
901
+ this.#writeCipher = new cipher.GcmCipher(serverKey, serverIV);
902
+ this.#readCipher = new cipher.GcmCipher(clientKey, clientIV);
903
+ }
904
+ }
905
+
906
+ #sendFinished(label: string): void {
907
+ const verifyData = prf(this.#masterSecret!, label, this.#transcriptHash(), 12);
908
+ // Finished is itself a handshake message and goes into the transcript.
909
+ const seq = this.#handshakeMessageSeq++;
910
+ const full = P.encodeHandshake(P.HANDSHAKE_TYPE.FINISHED, seq, verifyData);
911
+ this.#transcript.push(full);
912
+ this.#sendRecord(P.CONTENT_TYPE.HANDSHAKE, full);
913
+ }
914
+
915
+ #verifyPeerFinished(body: Buffer, label: string): void {
916
+ const expected = prf(this.#masterSecret!, label, this.#transcriptHash(), 12);
917
+ if (!crypto.timingSafeEqual(body, expected)) {
918
+ throw new Error('Peer Finished verify_data mismatch');
919
+ }
920
+ }
921
+
922
+ #onHandshakeComplete(): void {
923
+ if (this.#handshakeDone) return;
924
+ this.#handshakeDone = true;
925
+ this.#clearRetransmit();
926
+ this.state = STATE.CONNECTED;
927
+ this.emit('connect');
928
+ }
929
+
930
+ // ---- Application data ----------------------------------------------------
931
+
932
+ /**
933
+ * Send application data over the established connection.
934
+ * @param data
935
+ */
936
+ send(data: Buffer): void {
937
+ if (this.state !== STATE.CONNECTED) {
938
+ throw new Error('DTLS connection not established');
939
+ }
940
+ this.#sendRecord(P.CONTENT_TYPE.APPLICATION_DATA, data);
941
+ }
942
+
943
+ /** Send a close_notify and tear down. */
944
+ close(): void {
945
+ if (this.state === STATE.CLOSED || this.state === STATE.FAILED) return;
946
+ try {
947
+ if (this.#sendEncrypted) {
948
+ this.#sendRecord(
949
+ P.CONTENT_TYPE.ALERT,
950
+ Buffer.from([P.ALERT_LEVEL.WARNING, P.ALERT_DESC.CLOSE_NOTIFY])
951
+ );
952
+ }
953
+ } catch (_) {
954
+ // best-effort
955
+ }
956
+ this.#clearRetransmit();
957
+ this.state = STATE.CLOSED;
958
+ this.emit('close');
959
+ }
960
+
961
+ #fail(err: Error): void {
962
+ if (this.state === STATE.FAILED || this.state === STATE.CLOSED) return;
963
+ this.#clearRetransmit();
964
+ this.state = STATE.FAILED;
965
+ this.emit('error', err);
966
+ }
967
+
968
+ /** The peer's certificate DER, available after the handshake. */
969
+ getRemoteCertificate(): Buffer | null {
970
+ return this.#remoteCertDer;
971
+ }
972
+ }
973
+
974
+ export { DtlsConnection, ROLE, STATE };