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/127 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Evan Lucas <evanlucas@me.com>
This commit is contained in:
parent
0cb3325f12
commit
785e5ba48c
176
test/common/tls.js
Normal file
176
test/common/tls.js
Normal file
@ -0,0 +1,176 @@
|
||||
/* eslint-disable node-core/required-modules, node-core/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
test/parallel/test-tls-write-error.js
Normal file
55
test/parallel/test-tls-write-error.js
Normal file
@ -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();
|
||||
}));
|
||||
}));
|
||||
}));
|
Loading…
x
Reference in New Issue
Block a user