From a20cfac76a4596572243147f0ceb583a41220775 Mon Sep 17 00:00:00 2001 From: Shigeki Ohtsu Date: Fri, 22 May 2015 18:20:26 +0900 Subject: [PATCH 1/3] tls: add TLSSocket.getEphemeralKeyInfo() Returns an object representing a type, name and size of an ephemeral key exchange(PFS) in a client connection. This api is only supported not on a server connection but on a client. When it is called on a server connection or its key exchange is not ephemeral, an empty object is returned. --- doc/api/tls.markdown | 14 +++ lib/_tls_wrap.js | 7 ++ src/node_crypto.cc | 45 +++++++++ src/node_crypto.h | 2 + .../test-tls-client-getephemeralkeyinfo.js | 98 +++++++++++++++++++ 5 files changed, 166 insertions(+) create mode 100644 test/parallel/test-tls-client-getephemeralkeyinfo.js diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index 93779e54bc2d2a..bab547546d7fff 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -799,6 +799,19 @@ See SSL_CIPHER_get_name() and SSL_CIPHER_get_version() in http://www.openssl.org/docs/ssl/ssl.html#DEALING_WITH_CIPHERS for more information. +### tlsSocket.getEphemeralKeyInfo() + +Returns an object representing a type, name and size of parameter of +an ephemeral key exchange in [Perfect forward Secrecy][] on a client +connection. It returns an empty object when the key exchange is not +ephemeral. As it is only supported on a client socket, it returns null +if this is called on a server socket. The supported types are 'DH' and +'ECDH'. The `name` property is only available in 'ECDH'. + +Example: + + { type: 'ECDH', name: 'prime256v1', size: 256 } + ### tlsSocket.renegotiate(options, callback) Initiate TLS renegotiation process. The `options` may contain the following @@ -877,6 +890,7 @@ The numeric representation of the local port. [net.Server.address()]: net.html#net_server_address ['secureConnect']: #tls_event_secureconnect [secureConnection]: #tls_event_secureconnection +[Perfect Forward Secrecy]: #tls_perfect_forward_secrecy [Stream]: stream.html#stream_stream [SSL_METHODS]: http://www.openssl.org/docs/ssl/ssl.html#DEALING_WITH_PROTOCOL_METHODS [tls.Server]: #tls_class_tls_server diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index 1bff7579fc2914..ea2c3698447c7e 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -628,6 +628,13 @@ TLSSocket.prototype.getCipher = function(err) { } }; +TLSSocket.prototype.getEphemeralKeyInfo = function() { + if (this._handle) + return this._handle.getEphemeralKeyInfo(); + + return null; +}; + // TODO: support anonymous (nocert) and PSK diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 0e4fc45353ab68..5fb1986e2f89e1 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -1141,6 +1141,7 @@ void SSLWrap::AddMethods(Environment* env, Handle t) { env->SetProtoMethod(t, "newSessionDone", NewSessionDone); env->SetProtoMethod(t, "setOCSPResponse", SetOCSPResponse); env->SetProtoMethod(t, "requestOCSP", RequestOCSP); + env->SetProtoMethod(t, "getEphemeralKeyInfo", GetEphemeralKeyInfo); #ifdef SSL_set_max_send_fragment env->SetProtoMethod(t, "setMaxSendFragment", SetMaxSendFragment); @@ -1751,6 +1752,50 @@ void SSLWrap::RequestOCSP( } +template +void SSLWrap::GetEphemeralKeyInfo( + const v8::FunctionCallbackInfo& args) { + Base* w = Unwrap(args.Holder()); + Environment* env = Environment::GetCurrent(args); + + CHECK_NE(w->ssl_, nullptr); + + // tmp key is available on only client + if (w->is_server()) + return args.GetReturnValue().SetNull(); + + Local info = Object::New(env->isolate()); + + EVP_PKEY* key; + + if (SSL_get_server_tmp_key(w->ssl_, &key)) { + switch (EVP_PKEY_id(key)) { + case EVP_PKEY_DH: + info->Set(env->type_string(), + FIXED_ONE_BYTE_STRING(env->isolate(), "DH")); + info->Set(env->size_string(), + Integer::New(env->isolate(), EVP_PKEY_bits(key))); + break; + case EVP_PKEY_EC: + { + EC_KEY* ec = EVP_PKEY_get1_EC_KEY(key); + int nid = EC_GROUP_get_curve_name(EC_KEY_get0_group(ec)); + EC_KEY_free(ec); + info->Set(env->type_string(), + FIXED_ONE_BYTE_STRING(env->isolate(), "ECDH")); + info->Set(env->name_string(), + OneByteString(args.GetIsolate(), OBJ_nid2sn(nid))); + info->Set(env->size_string(), + Integer::New(env->isolate(), EVP_PKEY_bits(key))); + } + } + EVP_PKEY_free(key); + } + + return args.GetReturnValue().Set(info); +} + + #ifdef SSL_set_max_send_fragment template void SSLWrap::SetMaxSendFragment( diff --git a/src/node_crypto.h b/src/node_crypto.h index edacaa1b0095b9..fec8d861f2786c 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -236,6 +236,8 @@ class SSLWrap { static void NewSessionDone(const v8::FunctionCallbackInfo& args); static void SetOCSPResponse(const v8::FunctionCallbackInfo& args); static void RequestOCSP(const v8::FunctionCallbackInfo& args); + static void GetEphemeralKeyInfo( + const v8::FunctionCallbackInfo& args); #ifdef SSL_set_max_send_fragment static void SetMaxSendFragment( diff --git a/test/parallel/test-tls-client-getephemeralkeyinfo.js b/test/parallel/test-tls-client-getephemeralkeyinfo.js new file mode 100644 index 00000000000000..8932a4fc8a8f93 --- /dev/null +++ b/test/parallel/test-tls-client-getephemeralkeyinfo.js @@ -0,0 +1,98 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); + +if (!common.hasCrypto) { + console.log('1..0 # Skipped: missing crypto'); + process.exit(); +} +var tls = require('tls'); + +var fs = require('fs'); +var key = fs.readFileSync(common.fixturesDir + '/keys/agent2-key.pem'); +var cert = fs.readFileSync(common.fixturesDir + '/keys/agent2-cert.pem'); + +var ntests = 0; +var nsuccess = 0; + +function loadDHParam(n) { + var path = common.fixturesDir; + if (n !== 'error') path += '/keys'; + return fs.readFileSync(path + '/dh' + n + '.pem'); +} + +var cipherlist = { + 'NOT_PFS': 'AES128-SHA256', + 'DH': 'DHE-RSA-AES128-GCM-SHA256', + 'ECDH': 'ECDHE-RSA-AES128-GCM-SHA256' +}; + +function test(size, type, name, next) { + var cipher = type ? cipherlist[type] : cipherlist['NOT_PFS']; + + if (name) tls.DEFAULT_ECDH_CURVE = name; + + var options = { + key: key, + cert: cert, + ciphers: cipher + }; + + if (type === 'DH') options.dhparam = loadDHParam(size); + + var server = tls.createServer(options, function(conn) { + assert.strictEqual(conn.getEphemeralKeyInfo(), null); + conn.end(); + }); + + server.on('close', function(err) { + assert(!err); + if (next) next(); + }); + + server.listen(common.PORT, '127.0.0.1', function() { + var client = tls.connect({ + port: common.PORT, + rejectUnauthorized: false + }, function() { + var ekeyinfo = client.getEphemeralKeyInfo(); + assert.strictEqual(ekeyinfo.type, type); + assert.strictEqual(ekeyinfo.size, size); + assert.strictEqual(ekeyinfo.name, name); + nsuccess++; + server.close(); + }); + }); +} + +function testNOT_PFS() { + test(undefined, undefined, undefined, testDHE1024); + ntests++; +} + +function testDHE1024() { + test(1024, 'DH', undefined, testDHE2048); + ntests++; +} + +function testDHE2048() { + test(2048, 'DH', undefined, testECDHE256); + ntests++; +} + +function testECDHE256() { + test(256, 'ECDH', tls.DEFAULT_ECDH_CURVE, testECDHE512); + ntests++; +} + +function testECDHE512() { + test(521, 'ECDH', 'secp521r1', null); + ntests++; +} + +testNOT_PFS(); + +process.on('exit', function() { + assert.equal(ntests, nsuccess); + assert.equal(ntests, 5); +}); From a351777b82b1b8158653d3db024c602176e7cc84 Mon Sep 17 00:00:00 2001 From: Shigeki Ohtsu Date: Fri, 22 May 2015 18:21:54 +0900 Subject: [PATCH 2/3] tls: add minDHSize option to tls.connect() Add a new option to specifiy a minimum size of an ephemeral DH parameter to accept a tls connection. Default is 1024 bit. --- doc/api/tls.markdown | 5 ++ lib/_tls_wrap.js | 19 ++++- test/parallel/test-tls-client-mindhsize.js | 81 ++++++++++++++++++++++ 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-tls-client-mindhsize.js diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index bab547546d7fff..c3f0767ec9351d 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -388,6 +388,11 @@ Creates a new client connection to the given `port` and `host` (old API) or - `session`: A `Buffer` instance, containing TLS session. + - `minDHSize`: Minimum size of DH parameter in bits to accept a TLS + connection. When a server offers DH parameter with a size less + than this, the TLS connection is destroyed and throws an + error. Default: 1024. + The `callback` parameter will be added as a listener for the ['secureConnect'][] event. diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index ea2c3698447c7e..ae5e9257ed0b81 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -945,7 +945,8 @@ exports.connect = function(/* [port, host], options, cb */) { var defaults = { rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED, ciphers: tls.DEFAULT_CIPHERS, - checkServerIdentity: tls.checkServerIdentity + checkServerIdentity: tls.checkServerIdentity, + minDHSize: 1024 }; options = util._extend(defaults, options || {}); @@ -953,6 +954,11 @@ exports.connect = function(/* [port, host], options, cb */) { options.singleUse = true; assert(typeof options.checkServerIdentity === 'function'); + assert(typeof options.minDHSize === 'number', + 'options.minDHSize is not a number: ' + options.minDHSize); + assert(options.minDHSize > 0, + 'options.minDHSize is not a posivie number: ' + + options.minDHSize); var hostname = options.servername || options.host || @@ -1004,6 +1010,17 @@ exports.connect = function(/* [port, host], options, cb */) { socket._start(); socket.on('secure', function() { + // Check the size of DHE parameter above minimum requirement + // specified in options. + var ekeyinfo = socket.getEphemeralKeyInfo(); + if (ekeyinfo.type === 'DH' && ekeyinfo.size < options.minDHSize) { + var err = new Error('DH parameter size ' + ekeyinfo.size + + ' is less than ' + options.minDHSize); + socket.emit('error', err); + socket.destroy(); + return; + } + var verifyError = socket._handle.verifyError(); // Verify that server's identity matches it's certificate's names diff --git a/test/parallel/test-tls-client-mindhsize.js b/test/parallel/test-tls-client-mindhsize.js new file mode 100644 index 00000000000000..fde3de512cdc26 --- /dev/null +++ b/test/parallel/test-tls-client-mindhsize.js @@ -0,0 +1,81 @@ +'use strict'; +var common = require('../common'); +var assert = require('assert'); + +if (!common.hasCrypto) { + console.log('1..0 # Skipped: missing crypto'); + process.exit(); +} +var tls = require('tls'); + +var fs = require('fs'); +var key = fs.readFileSync(common.fixturesDir + '/keys/agent2-key.pem'); +var cert = fs.readFileSync(common.fixturesDir + '/keys/agent2-cert.pem'); + +var nsuccess = 0; +var nerror = 0; + +function loadDHParam(n) { + var path = common.fixturesDir; + if (n !== 'error') path += '/keys'; + return fs.readFileSync(path + '/dh' + n + '.pem'); +} + +function test(size, err, next) { + var options = { + key: key, + cert: cert, + dhparam: loadDHParam(size), + ciphers: 'DHE-RSA-AES128-GCM-SHA256' + }; + + var server = tls.createServer(options, function(conn) { + conn.end(); + }); + + server.on('close', function(isException) { + assert(!isException); + if (next) next(); + }); + + server.listen(common.PORT, '127.0.0.1', function() { + // client set minimum DH parameter size to 2048 bits so that + // it fails when it make a connection to the tls server where + // dhparams is 1024 bits + var client = tls.connect({ + minDHSize: 2048, + port: common.PORT, + rejectUnauthorized: false + }, function() { + nsuccess++; + server.close(); + }); + if (err) { + client.on('error', function(e) { + nerror++; + assert.strictEqual(e.message, 'DH parameter size 1024 is less' + + ' than 2048'); + server.close(); + }); + } + }); +} + +// A client connection fails with an error when a client has an +// 2048 bits minDHSize option and a server has 1024 bits dhparam +function testDHE1024() { + test(1024, true, testDHE2048); +} + +// A client connection successes when a client has an +// 2048 bits minDHSize option and a server has 2048 bits dhparam +function testDHE2048() { + test(2048, false, null); +} + +testDHE1024(); + +process.on('exit', function() { + assert.equal(nsuccess, 1); + assert.equal(nerror, 1); +}); From b867a31b1844e664807123b71c6e1458eb981335 Mon Sep 17 00:00:00 2001 From: Shigeki Ohtsu Date: Fri, 22 May 2015 18:23:57 +0900 Subject: [PATCH 3/3] tls: output warning of setDHParam to console.trace To make it easy to figure out where the warning comes from. Also fix style and variable name that was made in #1739. --- lib/_tls_common.js | 6 +++++- src/node_crypto.cc | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/_tls_common.js b/lib/_tls_common.js index d857717dabae15..120dce5784b27b 100644 --- a/lib/_tls_common.js +++ b/lib/_tls_common.js @@ -99,7 +99,11 @@ exports.createSecureContext = function createSecureContext(options, context) { else if (options.ecdhCurve) c.context.setECDHCurve(options.ecdhCurve); - if (options.dhparam) c.context.setDHParam(options.dhparam); + if (options.dhparam) { + var warning = c.context.setDHParam(options.dhparam); + if (warning) + console.trace(warning); + } if (options.crl) { if (Array.isArray(options.crl)) { diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 5fb1986e2f89e1..8e9fc28bfdadd9 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -804,12 +804,12 @@ void SecureContext::SetDHParam(const FunctionCallbackInfo& args) { if (dh == nullptr) return; - const int keylen = BN_num_bits(dh->p); - if (keylen < 1024) { - DH_free(dh); + const int size = BN_num_bits(dh->p); + if (size < 1024) { return env->ThrowError("DH parameter is less than 1024 bits"); - } else if (keylen < 2048) { - fprintf(stderr, "WARNING: DH parameter is less than 2048 bits\n"); + } else if (size < 2048) { + args.GetReturnValue().Set(FIXED_ONE_BYTE_STRING( + env->isolate(), "WARNING: DH parameter is less than 2048 bits")); } SSL_CTX_set_options(sc->ctx_, SSL_OP_SINGLE_DH_USE);