Skip to content

Commit

Permalink
test: add tls write error regression test
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
shigeki authored and evanlucas committed Jun 12, 2018
1 parent 84f23d2 commit 7684ba6
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 0 deletions.
176 changes: 176 additions & 0 deletions test/common/tls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/* eslint-disable required-modules, crypto-check */

'use strict';
const crypto = require('crypto');
const net = require('net');

exports.ccs = Buffer.from('140303000101', 'hex');

class TestTLSSocket extends net.Socket {
constructor(server_cert) {
super();
this.server_cert = server_cert;
this.version = Buffer.from('0303', 'hex');
this.handshake_list = [];
// AES128-GCM-SHA256
this.ciphers = Buffer.from('000002009c0', 'hex');
this.pre_master_secret =
Buffer.concat([this.version, crypto.randomBytes(46)]);
this.master_secret = null;
this.write_seq = 0;
this.client_random = crypto.randomBytes(32);

this.on('handshake', (msg) => {
this.handshake_list.push(msg);
});

this.on('server_random', (server_random) => {
this.master_secret = PRF12('sha256', this.pre_master_secret,
'master secret',
Buffer.concat([this.client_random,
server_random]),
48);
const key_block = PRF12('sha256', this.master_secret,
'key expansion',
Buffer.concat([server_random,
this.client_random]),
40);
this.client_writeKey = key_block.slice(0, 16);
this.client_writeIV = key_block.slice(32, 36);
});
}

createClientHello() {
const compressions = Buffer.from('0100', 'hex'); // null
const msg = addHandshakeHeader(0x01, Buffer.concat([
this.version, this.client_random, this.ciphers, compressions
]));
this.emit('handshake', msg);
return addRecordHeader(0x16, msg);
}

createClientKeyExchange() {
const encrypted_pre_master_secret = crypto.publicEncrypt({
key: this.server_cert,
padding: crypto.constants.RSA_PKCS1_PADDING
}, this.pre_master_secret);
const length = Buffer.alloc(2);
length.writeUIntBE(encrypted_pre_master_secret.length, 0, 2);
const msg = addHandshakeHeader(0x10, Buffer.concat([
length, encrypted_pre_master_secret]));
this.emit('handshake', msg);
return addRecordHeader(0x16, msg);
}

createFinished() {
const shasum = crypto.createHash('sha256');
shasum.update(Buffer.concat(this.handshake_list));
const message_hash = shasum.digest();
const r = PRF12('sha256', this.master_secret,
'client finished', message_hash, 12);
const msg = addHandshakeHeader(0x14, r);
this.emit('handshake', msg);
return addRecordHeader(0x16, msg);
}

createIllegalHandshake() {
const illegal_handshake = Buffer.alloc(5);
return addRecordHeader(0x16, illegal_handshake);
}

parseTLSFrame(buf) {
let offset = 0;
const record = buf.slice(offset, 5);
const type = record[0];
const length = record.slice(3, 5).readUInt16BE(0);
offset += 5;
let remaining = buf.slice(offset, offset + length);
if (type === 0x16) {
do {
remaining = this.parseTLSHandshake(remaining);
} while (remaining.length > 0);
}
offset += length;
return buf.slice(offset);
}

parseTLSHandshake(buf) {
let offset = 0;
const handshake_type = buf[offset];
if (handshake_type === 0x02) {
const server_random = buf.slice(6, 6 + 32);
this.emit('server_random', server_random);
}
offset += 1;
const length = buf.readUIntBE(offset, 3);
offset += 3;
const handshake = buf.slice(0, offset + length);
this.emit('handshake', handshake);
offset += length;
const remaining = buf.slice(offset);
return remaining;
}

encrypt(plain) {
const type = plain.slice(0, 1);
const version = plain.slice(1, 3);
const nonce = crypto.randomBytes(8);
const iv = Buffer.concat([this.client_writeIV.slice(0, 4), nonce]);
const bob = crypto.createCipheriv('aes-128-gcm', this.client_writeKey, iv);
const write_seq = Buffer.alloc(8);
write_seq.writeUInt32BE(this.write_seq++, 4);
const aad = Buffer.concat([write_seq, plain.slice(0, 5)]);
bob.setAAD(aad);
const encrypted1 = bob.update(plain.slice(5));
const encrypted = Buffer.concat([encrypted1, bob.final()]);
const tag = bob.getAuthTag();
const length = Buffer.alloc(2);
length.writeUInt16BE(nonce.length + encrypted.length + tag.length, 0);
return Buffer.concat([type, version, length, nonce, encrypted, tag]);
}
}

function addRecordHeader(type, frame) {
const record_layer = Buffer.from('0003030000', 'hex');
record_layer[0] = type;
record_layer.writeUInt16BE(frame.length, 3);
return Buffer.concat([record_layer, frame]);
}

function addHandshakeHeader(type, msg) {
const handshake_header = Buffer.alloc(4);
handshake_header[0] = type;
handshake_header.writeUIntBE(msg.length, 1, 3);
return Buffer.concat([handshake_header, msg]);
}

function PRF12(algo, secret, label, seed, size) {
const newSeed = Buffer.concat([Buffer.from(label, 'utf8'), seed]);
return P_hash(algo, secret, newSeed, size);
}

function P_hash(algo, secret, seed, size) {
const result = Buffer.alloc(size);
let hmac = crypto.createHmac(algo, secret);
hmac.update(seed);
let a = hmac.digest();
let j = 0;
while (j < size) {
hmac = crypto.createHmac(algo, secret);
hmac.update(a);
hmac.update(seed);
const b = hmac.digest();
let todo = b.length;
if (j + todo > size) {
todo = size - j;
}
b.copy(result, j, 0, todo);
j += todo;
hmac = crypto.createHmac(algo, secret);
hmac.update(a);
a = hmac.digest();
}
return result;
}

exports.TestTLSSocket = TestTLSSocket;
55 changes: 55 additions & 0 deletions test/parallel/test-tls-write-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');

const { TestTLSSocket, ccs } = require('../common/tls');
const fixtures = require('../common/fixtures');
const https = require('https');

// Regression test for an use-after-free bug in the TLS implementation that
// would occur when `SSL_write()` failed.
// Refs: https://github.com/nodejs-private/security/issues/189

const server_key = fixtures.readKey('agent1-key.pem');
const server_cert = fixtures.readKey('agent1-cert.pem');

const opts = {
key: server_key,
cert: server_cert
};

const server = https.createServer(opts, (req, res) => {
res.write('hello');
}).listen(0, common.mustCall(() => {
const client = new TestTLSSocket(server_cert);

client.connect({
host: 'localhost',
port: server.address().port
}, common.mustCall(() => {
const ch = client.createClientHello();
client.write(ch);
}));

client.once('data', common.mustCall((buf) => {
let remaining = buf;
do {
remaining = client.parseTLSFrame(remaining);
} while (remaining.length > 0);

const cke = client.createClientKeyExchange();
const finished = client.createFinished();
const ill = client.createIllegalHandshake();
const frames = Buffer.concat([
cke,
ccs,
client.encrypt(finished),
client.encrypt(ill)
]);
client.write(frames, common.mustCall(() => {
client.end();
server.close();
}));
}));
}));

0 comments on commit 7684ba6

Please sign in to comment.