Skip to content

Commit 7684ba6

Browse files
shigekievanlucas
authored andcommitted
test: add tls write error regression test
Add a mock TLS socket implementation and a regression test for the previous commit. Refs: https://github.com/nodejs-private/security/issues/189 PR-URL: https://github.com/nodejs-private/node-private/pull/130 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Evan Lucas <[email protected]>
1 parent 84f23d2 commit 7684ba6

File tree

2 files changed

+231
-0
lines changed

2 files changed

+231
-0
lines changed

test/common/tls.js

+176
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/* eslint-disable required-modules, crypto-check */
2+
3+
'use strict';
4+
const crypto = require('crypto');
5+
const net = require('net');
6+
7+
exports.ccs = Buffer.from('140303000101', 'hex');
8+
9+
class TestTLSSocket extends net.Socket {
10+
constructor(server_cert) {
11+
super();
12+
this.server_cert = server_cert;
13+
this.version = Buffer.from('0303', 'hex');
14+
this.handshake_list = [];
15+
// AES128-GCM-SHA256
16+
this.ciphers = Buffer.from('000002009c0', 'hex');
17+
this.pre_master_secret =
18+
Buffer.concat([this.version, crypto.randomBytes(46)]);
19+
this.master_secret = null;
20+
this.write_seq = 0;
21+
this.client_random = crypto.randomBytes(32);
22+
23+
this.on('handshake', (msg) => {
24+
this.handshake_list.push(msg);
25+
});
26+
27+
this.on('server_random', (server_random) => {
28+
this.master_secret = PRF12('sha256', this.pre_master_secret,
29+
'master secret',
30+
Buffer.concat([this.client_random,
31+
server_random]),
32+
48);
33+
const key_block = PRF12('sha256', this.master_secret,
34+
'key expansion',
35+
Buffer.concat([server_random,
36+
this.client_random]),
37+
40);
38+
this.client_writeKey = key_block.slice(0, 16);
39+
this.client_writeIV = key_block.slice(32, 36);
40+
});
41+
}
42+
43+
createClientHello() {
44+
const compressions = Buffer.from('0100', 'hex'); // null
45+
const msg = addHandshakeHeader(0x01, Buffer.concat([
46+
this.version, this.client_random, this.ciphers, compressions
47+
]));
48+
this.emit('handshake', msg);
49+
return addRecordHeader(0x16, msg);
50+
}
51+
52+
createClientKeyExchange() {
53+
const encrypted_pre_master_secret = crypto.publicEncrypt({
54+
key: this.server_cert,
55+
padding: crypto.constants.RSA_PKCS1_PADDING
56+
}, this.pre_master_secret);
57+
const length = Buffer.alloc(2);
58+
length.writeUIntBE(encrypted_pre_master_secret.length, 0, 2);
59+
const msg = addHandshakeHeader(0x10, Buffer.concat([
60+
length, encrypted_pre_master_secret]));
61+
this.emit('handshake', msg);
62+
return addRecordHeader(0x16, msg);
63+
}
64+
65+
createFinished() {
66+
const shasum = crypto.createHash('sha256');
67+
shasum.update(Buffer.concat(this.handshake_list));
68+
const message_hash = shasum.digest();
69+
const r = PRF12('sha256', this.master_secret,
70+
'client finished', message_hash, 12);
71+
const msg = addHandshakeHeader(0x14, r);
72+
this.emit('handshake', msg);
73+
return addRecordHeader(0x16, msg);
74+
}
75+
76+
createIllegalHandshake() {
77+
const illegal_handshake = Buffer.alloc(5);
78+
return addRecordHeader(0x16, illegal_handshake);
79+
}
80+
81+
parseTLSFrame(buf) {
82+
let offset = 0;
83+
const record = buf.slice(offset, 5);
84+
const type = record[0];
85+
const length = record.slice(3, 5).readUInt16BE(0);
86+
offset += 5;
87+
let remaining = buf.slice(offset, offset + length);
88+
if (type === 0x16) {
89+
do {
90+
remaining = this.parseTLSHandshake(remaining);
91+
} while (remaining.length > 0);
92+
}
93+
offset += length;
94+
return buf.slice(offset);
95+
}
96+
97+
parseTLSHandshake(buf) {
98+
let offset = 0;
99+
const handshake_type = buf[offset];
100+
if (handshake_type === 0x02) {
101+
const server_random = buf.slice(6, 6 + 32);
102+
this.emit('server_random', server_random);
103+
}
104+
offset += 1;
105+
const length = buf.readUIntBE(offset, 3);
106+
offset += 3;
107+
const handshake = buf.slice(0, offset + length);
108+
this.emit('handshake', handshake);
109+
offset += length;
110+
const remaining = buf.slice(offset);
111+
return remaining;
112+
}
113+
114+
encrypt(plain) {
115+
const type = plain.slice(0, 1);
116+
const version = plain.slice(1, 3);
117+
const nonce = crypto.randomBytes(8);
118+
const iv = Buffer.concat([this.client_writeIV.slice(0, 4), nonce]);
119+
const bob = crypto.createCipheriv('aes-128-gcm', this.client_writeKey, iv);
120+
const write_seq = Buffer.alloc(8);
121+
write_seq.writeUInt32BE(this.write_seq++, 4);
122+
const aad = Buffer.concat([write_seq, plain.slice(0, 5)]);
123+
bob.setAAD(aad);
124+
const encrypted1 = bob.update(plain.slice(5));
125+
const encrypted = Buffer.concat([encrypted1, bob.final()]);
126+
const tag = bob.getAuthTag();
127+
const length = Buffer.alloc(2);
128+
length.writeUInt16BE(nonce.length + encrypted.length + tag.length, 0);
129+
return Buffer.concat([type, version, length, nonce, encrypted, tag]);
130+
}
131+
}
132+
133+
function addRecordHeader(type, frame) {
134+
const record_layer = Buffer.from('0003030000', 'hex');
135+
record_layer[0] = type;
136+
record_layer.writeUInt16BE(frame.length, 3);
137+
return Buffer.concat([record_layer, frame]);
138+
}
139+
140+
function addHandshakeHeader(type, msg) {
141+
const handshake_header = Buffer.alloc(4);
142+
handshake_header[0] = type;
143+
handshake_header.writeUIntBE(msg.length, 1, 3);
144+
return Buffer.concat([handshake_header, msg]);
145+
}
146+
147+
function PRF12(algo, secret, label, seed, size) {
148+
const newSeed = Buffer.concat([Buffer.from(label, 'utf8'), seed]);
149+
return P_hash(algo, secret, newSeed, size);
150+
}
151+
152+
function P_hash(algo, secret, seed, size) {
153+
const result = Buffer.alloc(size);
154+
let hmac = crypto.createHmac(algo, secret);
155+
hmac.update(seed);
156+
let a = hmac.digest();
157+
let j = 0;
158+
while (j < size) {
159+
hmac = crypto.createHmac(algo, secret);
160+
hmac.update(a);
161+
hmac.update(seed);
162+
const b = hmac.digest();
163+
let todo = b.length;
164+
if (j + todo > size) {
165+
todo = size - j;
166+
}
167+
b.copy(result, j, 0, todo);
168+
j += todo;
169+
hmac = crypto.createHmac(algo, secret);
170+
hmac.update(a);
171+
a = hmac.digest();
172+
}
173+
return result;
174+
}
175+
176+
exports.TestTLSSocket = TestTLSSocket;

test/parallel/test-tls-write-error.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
6+
const { TestTLSSocket, ccs } = require('../common/tls');
7+
const fixtures = require('../common/fixtures');
8+
const https = require('https');
9+
10+
// Regression test for an use-after-free bug in the TLS implementation that
11+
// would occur when `SSL_write()` failed.
12+
// Refs: https://github.com/nodejs-private/security/issues/189
13+
14+
const server_key = fixtures.readKey('agent1-key.pem');
15+
const server_cert = fixtures.readKey('agent1-cert.pem');
16+
17+
const opts = {
18+
key: server_key,
19+
cert: server_cert
20+
};
21+
22+
const server = https.createServer(opts, (req, res) => {
23+
res.write('hello');
24+
}).listen(0, common.mustCall(() => {
25+
const client = new TestTLSSocket(server_cert);
26+
27+
client.connect({
28+
host: 'localhost',
29+
port: server.address().port
30+
}, common.mustCall(() => {
31+
const ch = client.createClientHello();
32+
client.write(ch);
33+
}));
34+
35+
client.once('data', common.mustCall((buf) => {
36+
let remaining = buf;
37+
do {
38+
remaining = client.parseTLSFrame(remaining);
39+
} while (remaining.length > 0);
40+
41+
const cke = client.createClientKeyExchange();
42+
const finished = client.createFinished();
43+
const ill = client.createIllegalHandshake();
44+
const frames = Buffer.concat([
45+
cke,
46+
ccs,
47+
client.encrypt(finished),
48+
client.encrypt(ill)
49+
]);
50+
client.write(frames, common.mustCall(() => {
51+
client.end();
52+
server.close();
53+
}));
54+
}));
55+
}));

0 commit comments

Comments
 (0)