lemon-tls 0.5.0 → 0.6.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/package.json +5 -3
- package/src/compat.js +10 -0
- package/src/crypto.js +146 -48
- package/src/dtls_session.js +501 -66
- package/src/dtls_socket.js +7 -0
- package/src/record.js +88 -21
- package/src/session/ecdh.js +124 -0
- package/src/session/message.js +35 -0
- package/src/session/signing.js +240 -47
- package/src/tls_session.js +1839 -431
- package/src/tls_socket.js +378 -39
- package/src/wire.js +272 -14
package/src/dtls_socket.js
CHANGED
|
@@ -52,6 +52,7 @@ function DTLSSocket(udpSocket, options) {
|
|
|
52
52
|
mtu: options.mtu,
|
|
53
53
|
ticketKeys: options.ticketKeys,
|
|
54
54
|
useCookies: options.useCookies,
|
|
55
|
+
srtpProfiles: options.srtpProfiles,
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
// Wire session → UDP
|
|
@@ -136,6 +137,11 @@ function DTLSSocket(udpSocket, options) {
|
|
|
136
137
|
return session.exportKeyingMaterial(length, label, context_value);
|
|
137
138
|
};
|
|
138
139
|
|
|
140
|
+
/** Negotiated DTLS-SRTP protection profile (RFC 5764), or null. */
|
|
141
|
+
self.getSelectedSrtpProfile = function() {
|
|
142
|
+
return session.getSelectedSrtpProfile();
|
|
143
|
+
};
|
|
144
|
+
|
|
139
145
|
/** Peer hello extensions / single extension by numeric type. */
|
|
140
146
|
self.getRemoteExtensions = function() { return session.getRemoteExtensions(); };
|
|
141
147
|
self.getRemoteExtension = function(type) { return session.getRemoteExtension(type); };
|
|
@@ -190,6 +196,7 @@ function createDTLSServer(options, connectionListener) {
|
|
|
190
196
|
mtu: options.mtu,
|
|
191
197
|
ticketKeys: options.ticketKeys,
|
|
192
198
|
useCookies: options.useCookies,
|
|
199
|
+
srtpProfiles: options.srtpProfiles,
|
|
193
200
|
});
|
|
194
201
|
|
|
195
202
|
connections[key] = socket;
|
package/src/record.js
CHANGED
|
@@ -33,12 +33,12 @@ function getAeadAlgo(cipherSuite) {
|
|
|
33
33
|
// ===================== TLS 1.3 primitives =====================
|
|
34
34
|
|
|
35
35
|
/** Derive write key + IV from a TLS 1.3 traffic secret. */
|
|
36
|
-
function deriveKeys(trafficSecret, cipherSuite) {
|
|
36
|
+
function deriveKeys(trafficSecret, cipherSuite, labelPrefix) {
|
|
37
37
|
const empty = new Uint8Array(0);
|
|
38
38
|
let cs = TLS_CIPHER_SUITES[cipherSuite];
|
|
39
39
|
return {
|
|
40
|
-
key: hkdf_expand_label(cs.hash, trafficSecret, 'key', empty, cs.keylen),
|
|
41
|
-
iv: hkdf_expand_label(cs.hash, trafficSecret, 'iv', empty, 12)
|
|
40
|
+
key: hkdf_expand_label(cs.hash, trafficSecret, 'key', empty, cs.keylen, labelPrefix),
|
|
41
|
+
iv: hkdf_expand_label(cs.hash, trafficSecret, 'iv', empty, 12, labelPrefix)
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -956,7 +956,9 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
956
956
|
let seq = reconstructDtlsSeq(truncated, hdr.seqLen2 ? 16 : 8,
|
|
957
957
|
(keys.maxReadSeq === undefined ? -1 : keys.maxReadSeq));
|
|
958
958
|
|
|
959
|
-
// Rebuild AAD with plaintext RN
|
|
959
|
+
// Rebuild AAD with plaintext RN. AAD depends only on the wire bytes (the
|
|
960
|
+
// decrypted record number + length), NOT on the reconstructed full seq —
|
|
961
|
+
// so it's computed once and reused across seq candidates below.
|
|
960
962
|
let hdrLen = 1 + rnLen + (hdr.hasLength ? 2 : 0);
|
|
961
963
|
let aad = new Uint8Array(hdrLen);
|
|
962
964
|
aad[0] = data[0];
|
|
@@ -966,7 +968,6 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
966
968
|
aad[1 + rnLen + 1] = ctLen & 0xFF;
|
|
967
969
|
}
|
|
968
970
|
|
|
969
|
-
let nonce = getNonce(keys.iv, seq);
|
|
970
971
|
let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
|
|
971
972
|
let isChaCha = algo === 'chacha20-poly1305';
|
|
972
973
|
|
|
@@ -974,20 +975,66 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
974
975
|
let ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
975
976
|
let tag = ciphertext.subarray(ciphertext.length - 16);
|
|
976
977
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
978
|
+
// The wire carries only the low bits of the record seq. reconstructDtlsSeq
|
|
979
|
+
// returns the most-likely full value, but under large gaps or reordering
|
|
980
|
+
// the true seq can be one window away in either direction. Only the NONCE
|
|
981
|
+
// depends on the full seq (AAD does not), so try the neighbouring
|
|
982
|
+
// candidates on AEAD failure before giving up — this is what lets
|
|
983
|
+
// DTLS-Replay-TLS13 / -LargeGaps / -NonMonotonic deprotect instead of
|
|
984
|
+
// failing as "bad record MAC". Candidates are deduped and non-negative.
|
|
985
|
+
let window = hdr.seqLen2 ? 65536 : 256;
|
|
986
|
+
let candidates = [seq];
|
|
987
|
+
if (seq - window >= 0) candidates.push(seq - window);
|
|
988
|
+
candidates.push(seq + window);
|
|
989
|
+
|
|
990
|
+
let pt = null, acceptedSeq = -1;
|
|
991
|
+
for (let ci = 0; ci < candidates.length; ci++) {
|
|
992
|
+
let cand = candidates[ci];
|
|
993
|
+
let nonce = getNonce(keys.iv, cand);
|
|
994
|
+
try {
|
|
995
|
+
let decipher = crypto.createDecipheriv(algo, keys.key, nonce,
|
|
996
|
+
isChaCha ? { authTagLength: 16 } : undefined);
|
|
997
|
+
decipher.setAAD(aad, isChaCha ? { plaintextLength: ct.length } : undefined);
|
|
998
|
+
decipher.setAuthTag(tag);
|
|
999
|
+
let out = decipher.update(ct);
|
|
1000
|
+
decipher.final();
|
|
1001
|
+
pt = out; acceptedSeq = cand;
|
|
1002
|
+
break;
|
|
1003
|
+
} catch (e) {
|
|
1004
|
+
// try next candidate
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
984
1007
|
|
|
1008
|
+
if (pt === null) return null;
|
|
1009
|
+
seq = acceptedSeq;
|
|
1010
|
+
|
|
1011
|
+
{
|
|
985
1012
|
let inner = parseInnerPlaintext(new Uint8Array(pt));
|
|
986
1013
|
|
|
987
|
-
//
|
|
988
|
-
//
|
|
989
|
-
|
|
1014
|
+
// RFC 9147 §4.5.1 anti-replay: only AFTER the tag verifies do we consult
|
|
1015
|
+
// and update the sliding window. Checking before deprotection would let a
|
|
1016
|
+
// forged record number poison the window; checking after means only
|
|
1017
|
+
// genuine records move it. The window lives on the per-epoch key object,
|
|
1018
|
+
// next to maxReadSeq, because both are per-epoch receive state.
|
|
1019
|
+
// replayWindow: bitmask of the 64 seqs at or below maxReadSeq that have
|
|
1020
|
+
// been accepted. Bit 0 = maxReadSeq, bit k = maxReadSeq - k.
|
|
1021
|
+
if (keys.maxReadSeq === undefined || keys.maxReadSeq < 0) {
|
|
1022
|
+
// First accepted record in this epoch: seed the window with this seq as
|
|
1023
|
+
// the high-water mark (bit 0 set), so an immediate replay of it is
|
|
1024
|
+
// caught below rather than sailing through an unset window.
|
|
1025
|
+
keys.maxReadSeq = seq;
|
|
1026
|
+
keys.replayWindow = 1n;
|
|
1027
|
+
} else if (seq > keys.maxReadSeq) {
|
|
1028
|
+
// Advance: shift the window up by the gap, then set bit 0.
|
|
1029
|
+
let shift = BigInt(seq - keys.maxReadSeq);
|
|
1030
|
+
keys.replayWindow = shift >= 64n ? 1n : ((keys.replayWindow << shift) | 1n) & ((1n << 64n) - 1n);
|
|
990
1031
|
keys.maxReadSeq = seq;
|
|
1032
|
+
} else {
|
|
1033
|
+
let back = BigInt(keys.maxReadSeq - seq);
|
|
1034
|
+
if (back >= 64n) return null; // too old — outside window
|
|
1035
|
+
let bit = 1n << back;
|
|
1036
|
+
if (keys.replayWindow & bit) return null; // already seen — replay
|
|
1037
|
+
keys.replayWindow |= bit;
|
|
991
1038
|
}
|
|
992
1039
|
|
|
993
1040
|
return {
|
|
@@ -997,8 +1044,6 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
997
1044
|
content: inner.content,
|
|
998
1045
|
total_length: off + ctLen,
|
|
999
1046
|
};
|
|
1000
|
-
} catch (e) {
|
|
1001
|
-
return null;
|
|
1002
1047
|
}
|
|
1003
1048
|
}
|
|
1004
1049
|
|
|
@@ -1024,15 +1069,36 @@ function parseDtlsDatagram(data, keysByEpoch) {
|
|
|
1024
1069
|
let keys = keysByEpoch ? keysByEpoch[hdr.epoch] : null;
|
|
1025
1070
|
|
|
1026
1071
|
if (!keys) {
|
|
1027
|
-
//
|
|
1072
|
+
// No keys for this epoch YET. Do not discard the record: a DTLS peer
|
|
1073
|
+
// legitimately packs its whole flight into one datagram, so the very
|
|
1074
|
+
// record that lets us derive these keys (the ServerHello) can sit
|
|
1075
|
+
// ahead of records encrypted under them. Throwing the rest away meant
|
|
1076
|
+
// the entire encrypted flight vanished and the handshake stalled until
|
|
1077
|
+
// the peer retransmitted — or forever, if it does not.
|
|
1078
|
+
//
|
|
1079
|
+
// Keep the raw bytes and mark the record deferred; the caller retries
|
|
1080
|
+
// it after processing each earlier record, once the keys exist. This
|
|
1081
|
+
// generalises the CCS-only re-decrypt that DTLS 1.2 already had.
|
|
1028
1082
|
let rnLen = hdr.seqLen2 ? 2 : 1;
|
|
1029
1083
|
let skip = 1 + rnLen;
|
|
1084
|
+
let total;
|
|
1030
1085
|
if (hdr.hasLength && off + skip + 2 <= data.length) {
|
|
1031
|
-
skip
|
|
1086
|
+
total = skip + 2 + readU16(data, off + skip);
|
|
1032
1087
|
} else {
|
|
1033
|
-
|
|
1088
|
+
total = data.length - off;
|
|
1034
1089
|
}
|
|
1035
|
-
off
|
|
1090
|
+
if (total <= 0 || off + total > data.length) total = data.length - off;
|
|
1091
|
+
|
|
1092
|
+
records.push({
|
|
1093
|
+
type: null,
|
|
1094
|
+
epoch: hdr.epoch,
|
|
1095
|
+
seq: null,
|
|
1096
|
+
content: null,
|
|
1097
|
+
encrypted: false,
|
|
1098
|
+
deferred: true,
|
|
1099
|
+
raw: data.subarray(off, off + total),
|
|
1100
|
+
});
|
|
1101
|
+
off += total;
|
|
1036
1102
|
continue;
|
|
1037
1103
|
}
|
|
1038
1104
|
|
|
@@ -1174,6 +1240,7 @@ export {
|
|
|
1174
1240
|
// DTLS 1.3 unified header
|
|
1175
1241
|
buildUnifiedHdr,
|
|
1176
1242
|
parseUnifiedHdr,
|
|
1243
|
+
reconstructDtlsSeq,
|
|
1177
1244
|
maskRecordNumber,
|
|
1178
1245
|
|
|
1179
1246
|
// DTLS 1.3 encrypted records
|
package/src/session/ecdh.js
CHANGED
|
@@ -90,7 +90,131 @@ function p384_get_shared_secret(localPrivateRaw, remotePublicRaw) {
|
|
|
90
90
|
return new Uint8Array(ecdh.computeSecret(Buffer.from(remotePublicRaw)));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Named groups this module actually implements, in preference order.
|
|
95
|
+
*
|
|
96
|
+
* SINGLE SOURCE OF TRUTH: consumers (TLSSession defaults, TLSSocket options,
|
|
97
|
+
* DTLSSession) must derive their advertised supported_groups from this list
|
|
98
|
+
* rather than hardcoding their own. Previously three call sites carried three
|
|
99
|
+
* different hardcoded lists — DTLS advertised P-384 while TLS did not, even
|
|
100
|
+
* though the code below has supported it all along, so a peer offering only
|
|
101
|
+
* P-384 got "no curve supported by both" from a stack that could do P-384.
|
|
102
|
+
*
|
|
103
|
+
* Adding a curve should be a change to THIS file only.
|
|
104
|
+
*/
|
|
105
|
+
const NAMED_GROUP = {
|
|
106
|
+
X25519: 0x001d,
|
|
107
|
+
SECP256R1: 0x0017,
|
|
108
|
+
SECP384R1: 0x0018,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const SUPPORTED_GROUPS = [
|
|
112
|
+
NAMED_GROUP.X25519, // preferred: fastest, no point validation pitfalls
|
|
113
|
+
NAMED_GROUP.SECP256R1,
|
|
114
|
+
NAMED_GROUP.SECP384R1,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
function is_supported_group(group) {
|
|
118
|
+
return SUPPORTED_GROUPS.indexOf(group) >= 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Generate a keypair for any supported group.
|
|
123
|
+
* Returns { private_key, public_key } or null for an unsupported group.
|
|
124
|
+
*/
|
|
125
|
+
function generate_keypair(group) {
|
|
126
|
+
if (group === NAMED_GROUP.X25519) {
|
|
127
|
+
let priv = new Uint8Array(crypto.randomBytes(32));
|
|
128
|
+
return { private_key: priv, public_key: x25519_get_public_key(priv) };
|
|
129
|
+
}
|
|
130
|
+
if (group === NAMED_GROUP.SECP256R1) return p256_generate_keypair();
|
|
131
|
+
if (group === NAMED_GROUP.SECP384R1) return p384_generate_keypair();
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate a peer's key_exchange value for `group` and normalize it to the
|
|
137
|
+
* form the primitives below expect.
|
|
138
|
+
*
|
|
139
|
+
* Point-format knowledge belongs HERE, next to the curve implementations —
|
|
140
|
+
* not in the handshake state machine, which only needs to know "valid or not"
|
|
141
|
+
* so it can decide whether to abort. Throws on anything malformed; the caller
|
|
142
|
+
* converts that into an illegal_parameter alert.
|
|
143
|
+
*/
|
|
144
|
+
function normalize_peer_public_key(group, pub) {
|
|
145
|
+
if (!pub || pub.length === 0) throw new Error('empty key_exchange');
|
|
146
|
+
|
|
147
|
+
if (group === NAMED_GROUP.X25519) {
|
|
148
|
+
if (pub.length !== 32) {
|
|
149
|
+
throw new Error('X25519 key_exchange must be 32 bytes, got ' + pub.length);
|
|
150
|
+
}
|
|
151
|
+
return pub;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let coordLen = (group === NAMED_GROUP.SECP256R1) ? 32
|
|
155
|
+
: (group === NAMED_GROUP.SECP384R1) ? 48 : 0;
|
|
156
|
+
if (coordLen === 0) throw new Error('unsupported group 0x' + (group >>> 0).toString(16));
|
|
157
|
+
|
|
158
|
+
// RFC 8446 §4.2.8.2: for the secp*r1 groups the key_exchange is an
|
|
159
|
+
// UNCOMPRESSED point — 0x04 || X || Y — and nothing else. The length is
|
|
160
|
+
// therefore exact.
|
|
161
|
+
//
|
|
162
|
+
// This used to also accept a bare X || Y (no 0x04) and re-prefix it, as a
|
|
163
|
+
// tolerance for TLS 1.2 peers that strip the prefix. That tolerance is a
|
|
164
|
+
// security hole, not a kindness: truncating a valid 65-byte P-256 point by
|
|
165
|
+
// one byte yields exactly 64 bytes, which the tolerance then reinterpreted
|
|
166
|
+
// as a DIFFERENT, prefix-less point and happily fed to the ECDH. The same
|
|
167
|
+
// arithmetic applies to P-384 (97 → 96). Malformed input must be rejected,
|
|
168
|
+
// not silently reinterpreted.
|
|
169
|
+
let expected = 1 + 2 * coordLen;
|
|
170
|
+
|
|
171
|
+
if (pub.length !== expected) {
|
|
172
|
+
// Name the two shapes we know we are refusing, so the failure is
|
|
173
|
+
// diagnosable rather than just "bad length".
|
|
174
|
+
if (pub.length === 2 * coordLen) {
|
|
175
|
+
throw new Error('key_exchange is missing the 0x04 uncompressed-point prefix (got ' +
|
|
176
|
+
pub.length + ' bytes, expected ' + expected + ')');
|
|
177
|
+
}
|
|
178
|
+
if (pub.length === 1 + coordLen && (pub[0] === 0x02 || pub[0] === 0x03)) {
|
|
179
|
+
throw new Error('compressed points are not permitted; key_exchange must be uncompressed (RFC 8446 §4.2.8.2)');
|
|
180
|
+
}
|
|
181
|
+
throw new Error('key_exchange must be a ' + expected + '-byte uncompressed point, got ' + pub.length);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (pub[0] !== 0x04) {
|
|
185
|
+
throw new Error('key_exchange must start with the 0x04 uncompressed-point marker, got 0x' +
|
|
186
|
+
pub[0].toString(16));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Whether the point actually lies on the curve is checked by the ECDH
|
|
190
|
+
// primitive below (node throws ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY), which
|
|
191
|
+
// happens before any secret is produced — so an off-curve point can never
|
|
192
|
+
// yield key material.
|
|
193
|
+
return pub;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Compute the ECDHE shared secret for any supported group.
|
|
198
|
+
* Validates the peer's public key first; throws on invalid input (including
|
|
199
|
+
* the RFC 7748 §6.1 all-zero X25519 result and node's own on-curve checks).
|
|
200
|
+
*/
|
|
201
|
+
function get_shared_secret(group, localPrivateRaw, remotePublicRaw) {
|
|
202
|
+
let pub = normalize_peer_public_key(group, remotePublicRaw);
|
|
203
|
+
|
|
204
|
+
if (group === NAMED_GROUP.X25519) return x25519_get_shared_secret(localPrivateRaw, pub);
|
|
205
|
+
if (group === NAMED_GROUP.SECP256R1) return p256_get_shared_secret(localPrivateRaw, pub);
|
|
206
|
+
if (group === NAMED_GROUP.SECP384R1) return p384_get_shared_secret(localPrivateRaw, pub);
|
|
207
|
+
|
|
208
|
+
throw new Error('unsupported group 0x' + (group >>> 0).toString(16));
|
|
209
|
+
}
|
|
210
|
+
|
|
93
211
|
export {
|
|
212
|
+
NAMED_GROUP,
|
|
213
|
+
SUPPORTED_GROUPS,
|
|
214
|
+
is_supported_group,
|
|
215
|
+
generate_keypair,
|
|
216
|
+
normalize_peer_public_key,
|
|
217
|
+
get_shared_secret,
|
|
94
218
|
x25519_get_public_key,
|
|
95
219
|
x25519_get_shared_secret,
|
|
96
220
|
p256_generate_keypair,
|
package/src/session/message.js
CHANGED
|
@@ -193,6 +193,10 @@ function parse_tls_message(data, negotiatedVersion) {
|
|
|
193
193
|
out.type = 'certificate';
|
|
194
194
|
|
|
195
195
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.CERTIFICATE_VERIFY) {
|
|
196
|
+
// Decode scheme + signature so the session can actually VERIFY it.
|
|
197
|
+
// Previously only the raw body was handed up and no caller ever checked
|
|
198
|
+
// the signature — every peer signature was accepted unconditionally.
|
|
199
|
+
out = wire.parse_certificate_verify(message.body);
|
|
196
200
|
out.type = 'certificate_verify';
|
|
197
201
|
out.body = message.body;
|
|
198
202
|
|
|
@@ -218,6 +222,37 @@ function parse_tls_message(data, negotiatedVersion) {
|
|
|
218
222
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.CERTIFICATE_REQUEST) {
|
|
219
223
|
out = wire.parse_certificate_request(message.body);
|
|
220
224
|
out.type = 'certificate_request';
|
|
225
|
+
|
|
226
|
+
// Surface the requested signature algorithms as a plain field.
|
|
227
|
+
//
|
|
228
|
+
// In TLS 1.3 they arrive inside the extensions vector (RFC 8446 §4.3.2);
|
|
229
|
+
// in TLS 1.2 the decoder already returns them as `signature_algorithms`
|
|
230
|
+
// (RFC 5246 §7.4.4). The session asks for one name, so normalise here —
|
|
231
|
+
// exactly what normalize_hello does for the hello messages. Without this
|
|
232
|
+
// the 1.3 list was silently undefined and the client fell back to signing
|
|
233
|
+
// with its OWN preference, ignoring what the server actually asked for;
|
|
234
|
+
// a server that requested a specific algorithm then rejects the signature
|
|
235
|
+
// as an unsupported one.
|
|
236
|
+
if (out.signature_algorithms === undefined && Array.isArray(out.extensions)) {
|
|
237
|
+
for (let i = 0; i < out.extensions.length; i++) {
|
|
238
|
+
let e = out.extensions[i];
|
|
239
|
+
if (e && e.type === wire.TLS_EXT.SIGNATURE_ALGORITHMS && Array.isArray(e.value)) {
|
|
240
|
+
out.signature_algorithms = e.value;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
} else {
|
|
247
|
+
// No silent fall-through. Returning a typeless object here made every
|
|
248
|
+
// unrecognized handshake type vanish: the session's dispatch chain matched
|
|
249
|
+
// nothing and simply returned, so a peer could stream unknown (or
|
|
250
|
+
// version-illegal, e.g. HelloRequest in TLS 1.3) messages indefinitely with
|
|
251
|
+
// no alert and no state change. Name the message instead and let the
|
|
252
|
+
// session decide — which it can only do if it is told.
|
|
253
|
+
out.type = 'unknown_handshake';
|
|
254
|
+
out.handshake_type = message.type;
|
|
255
|
+
out.body = message.body;
|
|
221
256
|
}
|
|
222
257
|
|
|
223
258
|
return out;
|