From bc91220ca280c014d0cad61ab4a392cba62362b3 Mon Sep 17 00:00:00 2001 From: Shigeki Ohtsu Date: Thu, 12 Apr 2018 22:10:59 +0200 Subject: [PATCH] test: add tls write error regression test Add a mock TLS socket implementation and a regression test for the previous commit. This is to prevent v8.x from becoming susceptible to CVE-2018-7162. Refs: https://github.com/nodejs-private/security/issues/189 PR-URL: https://github.com/nodejs-private/node-private/pull/131 Reviewed-By: Anna Henningsen Reviewed-By: Evan Lucas --- test/common/tls.js | 176 ++++++++++++++++++++++++++ test/parallel/test-tls-write-error.js | 55 ++++++++ 2 files changed, 231 insertions(+) create mode 100644 test/common/tls.js create mode 100644 test/parallel/test-tls-write-error.js diff --git a/test/common/tls.js b/test/common/tls.js new file mode 100644 index 00000000000000..6881768254ae75 --- /dev/null +++ b/test/common/tls.js @@ -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; diff --git a/test/parallel/test-tls-write-error.js b/test/parallel/test-tls-write-error.js new file mode 100644 index 00000000000000..2783e62d063a28 --- /dev/null +++ b/test/parallel/test-tls-write-error.js @@ -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(); + })); + })); +}));