From c9b40da368dfba3c026225cf37317beef187c16d Mon Sep 17 00:00:00 2001 From: Fedor Indutny Date: Thu, 14 Apr 2011 10:53:39 +0700 Subject: [PATCH] OpenSSL NPN in node.js closes #926. --- lib/https.js | 10 ++ lib/tls.js | 53 ++++++++- src/node_constants.cc | 5 + src/node_crypto.cc | 134 ++++++++++++++++++++++ src/node_crypto.h | 25 ++++ test/simple/test-tls-npn-server-client.js | 101 ++++++++++++++++ 6 files changed, 324 insertions(+), 4 deletions(-) create mode 100644 test/simple/test-tls-npn-server-client.js diff --git a/lib/https.js b/lib/https.js index bd2e4617a4..7f4aaff36a 100644 --- a/lib/https.js +++ b/lib/https.js @@ -23,9 +23,15 @@ var tls = require('tls'); var http = require('http'); var inherits = require('util').inherits; +var NPN_ENABLED = process.binding('constants').NPN_ENABLED; function Server(opts, requestListener) { if (!(this instanceof Server)) return new Server(opts, requestListener); + + if (NPN_ENABLED && !opts.NPNProtocols) { + opts.NPNProtocols = ['http/1.1', 'http/1.0']; + } + tls.Server.call(this, opts, http._connectionListener); this.httpAllowHalfOpen = false; @@ -58,6 +64,10 @@ Agent.prototype.defaultPort = 443; Agent.prototype._getConnection = function(host, port, cb) { + if (NPN_ENABLED && !this.options.NPNProtocols) { + this.options.NPNProtocols = ['http/1.1', 'http/1.0']; + } + var s = tls.connect(port, host, this.options, function() { // do other checks here? if (cb) cb(); diff --git a/lib/tls.js b/lib/tls.js index a8f1898f49..b535697158 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -27,6 +27,8 @@ var stream = require('stream'); var END_OF_FILE = 42; var assert = require('assert').ok; +var NPN_ENABLED = process.binding('constants').NPN_ENABLED; + var debug; if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) { debug = function(a) { console.error('TLS:', a); }; @@ -38,10 +40,36 @@ if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) { var Connection = null; try { Connection = process.binding('crypto').Connection; + exports.NPN_ENABLED = NPN_ENABLED; } catch (e) { throw new Error('node.js not compiled with openssl crypto support.'); } +// Convert protocols array into valid OpenSSL protocols list +// ("\x06spdy/2\x08http/1.1\x08http/1.0") +function convertNPNProtocols(NPNProtocols, out) { + // If NPNProtocols is Array - translate it into buffer + if (Array.isArray(NPNProtocols)) { + var buff = new Buffer(NPNProtocols.reduce(function(p, c) { + return p + 1 + Buffer.byteLength(c); + }, 0)); + + NPNProtocols.reduce(function(offset, c) { + var clen = Buffer.byteLength(c); + buff[offset] = clen; + buff.write(c, offset + 1); + + return offset + 1 + clen; + }, 0); + + NPNProtocols = buff; + } + + // If it's already a Buffer - store it + if (Buffer.isBuffer(NPNProtocols)) { + out.NPNProtocols = NPNProtocols; + } +}; // Base class of both CleartextStream and EncryptedStream function CryptoStream(pair) { @@ -437,12 +465,14 @@ EncryptedStream.prototype._pusher = function(pool, offset, length) { * Provides a pair of streams to do encrypted communication. */ -function SecurePair(credentials, isServer, requestCert, rejectUnauthorized) { +function SecurePair(credentials, isServer, requestCert, rejectUnauthorized, + NPNProtocols) { if (!(this instanceof SecurePair)) { return new SecurePair(credentials, isServer, requestCert, - rejectUnauthorized); + rejectUnauthorized, + NPNProtocols); } var self = this; @@ -478,6 +508,10 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized) { this._requestCert, this._rejectUnauthorized); + if (NPN_ENABLED && NPNProtocols) { + this._ssl.setNPNProtocols(NPNProtocols); + this.npnProtocol = null; + } /* Acts as a r/w stream to the cleartext side of the stream. */ this.cleartext = new CleartextStream(this); @@ -588,6 +622,10 @@ SecurePair.prototype._cycle = function(depth) { SecurePair.prototype._maybeInitFinished = function() { if (this._ssl && !this._secureEstablished && this._ssl.isInitFinished()) { + if (NPN_ENABLED) { + this.npnProtocol = this._ssl.getNegotiatedProtocol(); + } + this._secureEstablished = true; debug('secure established'); this.emit('secure'); @@ -745,13 +783,15 @@ function Server(/* [options], listener */) { var pair = new SecurePair(creds, true, self.requestCert, - self.rejectUnauthorized); + self.rejectUnauthorized, + self.NPNProtocols); var cleartext = pipe(pair, socket); cleartext._controlReleased = false; pair.on('secure', function() { pair.cleartext.authorized = false; + pair.cleartext.npnProtocol = pair.npnProtocol; if (!self.requestCert) { cleartext._controlReleased = true; self.emit('secureConnection', pair.cleartext, pair.encrypted); @@ -812,6 +852,7 @@ Server.prototype.setOptions = function(options) { if (options.ciphers) this.ciphers = options.ciphers; if (options.secureProtocol) this.secureProtocol = options.secureProtocol; if (options.secureOptions) this.secureOptions = options.secureOptions; + if (options.NPNProtocols) convertNPNProtocols(options.NPNProtocols, this); }; @@ -854,7 +895,9 @@ exports.connect = function(port /* host, options, cb */) { var sslcontext = crypto.createCredentials(options); //sslcontext.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA'); - var pair = new SecurePair(sslcontext, false); + convertNPNProtocols(options.NPNProtocols, this); + var pair = new SecurePair(sslcontext, false, true, false, + this.NPNProtocols); var cleartext = pipe(pair, socket); @@ -863,6 +906,8 @@ exports.connect = function(port /* host, options, cb */) { pair.on('secure', function() { var verifyError = pair._ssl.verifyError(); + cleartext.npnProtocol = pair.npnProtocol; + if (verifyError) { cleartext.authorized = false; cleartext.authorizationError = verifyError; diff --git a/src/node_constants.cc b/src/node_constants.cc index 5cad3c3402..7d316ff107 100644 --- a/src/node_constants.cc +++ b/src/node_constants.cc @@ -912,6 +912,11 @@ void DefineConstants(Handle target) { #ifdef SSL_OP_CRYPTOPRO_TLSEXT_BUG NODE_DEFINE_CONSTANT(target, SSL_OP_CRYPTOPRO_TLSEXT_BUG); #endif + +#ifdef OPENSSL_NPN_NEGOTIATED +#define NPN_ENABLED 1 + NODE_DEFINE_CONSTANT(target, NPN_ENABLED); +#endif } } // namespace node diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 03024c3938..2d82ff063e 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -565,6 +565,11 @@ void Connection::Initialize(Handle target) { NODE_SET_PROTOTYPE_METHOD(t, "receivedShutdown", Connection::ReceivedShutdown); NODE_SET_PROTOTYPE_METHOD(t, "close", Connection::Close); +#ifdef OPENSSL_NPN_NEGOTIATED + NODE_SET_PROTOTYPE_METHOD(t, "getNegotiatedProtocol", Connection::GetNegotiatedProto); + NODE_SET_PROTOTYPE_METHOD(t, "setNPNProtocols", Connection::SetNPNProtocols); +#endif + target->Set(String::NewSymbol("Connection"), t->GetFunction()); } @@ -614,6 +619,76 @@ static int VerifyCallback(int preverify_ok, X509_STORE_CTX *ctx) { return 1; } +#ifdef OPENSSL_NPN_NEGOTIATED + +int Connection::AdvertiseNextProtoCallback_(SSL *s, + const unsigned char **data, + unsigned int *len, + void *arg) { + + Connection *p = static_cast(SSL_get_app_data(s)); + + if (p->npnProtos_.IsEmpty()) { + // No initialization - no NPN protocols + *data = reinterpret_cast(""); + *len = 0; + } else { + *data = reinterpret_cast(Buffer::Data(p->npnProtos_)); + *len = Buffer::Length(p->npnProtos_); + } + + return SSL_TLSEXT_ERR_OK; +} + +int Connection::SelectNextProtoCallback_(SSL *s, + unsigned char **out, unsigned char *outlen, + const unsigned char* in, + unsigned int inlen, void *arg) { + Connection *p = static_cast SSL_get_app_data(s); + + // Release old protocol handler if present + if (!p->selectedNPNProto_.IsEmpty()) { + p->selectedNPNProto_.Dispose(); + } + + if (p->npnProtos_.IsEmpty()) { + // We should at least select one protocol + // If server is using NPN + *out = reinterpret_cast(const_cast("http/1.1")); + *outlen = 8; + + // set status unsupported + p->selectedNPNProto_ = Persistent::New(False()); + + return SSL_TLSEXT_ERR_OK; + } + + const unsigned char* npnProtos = + reinterpret_cast(Buffer::Data(p->npnProtos_)); + + int status = SSL_select_next_proto(out, outlen, in, inlen, npnProtos, + Buffer::Length(p->npnProtos_)); + + switch (status) { + case OPENSSL_NPN_UNSUPPORTED: + p->selectedNPNProto_ = Persistent::New(Null()); + break; + case OPENSSL_NPN_NEGOTIATED: + p->selectedNPNProto_ = Persistent::New(String::New( + reinterpret_cast(*out), *outlen + )); + break; + case OPENSSL_NPN_NO_OVERLAP: + p->selectedNPNProto_ = Persistent::New(False()); + break; + default: + break; + } + + return SSL_TLSEXT_ERR_OK; +} +#endif + Handle Connection::New(const Arguments& args) { HandleScope scope; @@ -633,6 +708,23 @@ Handle Connection::New(const Arguments& args) { p->ssl_ = SSL_new(sc->ctx_); p->bio_read_ = BIO_new(BIO_s_mem()); p->bio_write_ = BIO_new(BIO_s_mem()); + +#ifdef OPENSSL_NPN_NEGOTIATED + SSL_set_app_data(p->ssl_, p); + if (is_server) { + // Server should advertise NPN protocols + SSL_CTX_set_next_protos_advertised_cb(sc->ctx_, + AdvertiseNextProtoCallback_, + NULL); + } else { + // Client should select protocol from advertised + // If server supports NPN + SSL_CTX_set_next_proto_select_cb(sc->ctx_, + SelectNextProtoCallback_, + NULL); + } +#endif + SSL_set_bio(p->ssl_, p->bio_read_, p->bio_write_); #ifdef SSL_MODE_RELEASE_BUFFERS @@ -1184,6 +1276,48 @@ Handle Connection::Close(const Arguments& args) { return True(); } +#ifdef OPENSSL_NPN_NEGOTIATED +Handle Connection::GetNegotiatedProto(const Arguments& args) { + HandleScope scope; + + Connection *ss = Connection::Unwrap(args); + + if (ss->is_server_) { + const unsigned char *npn_proto; + unsigned int npn_proto_len; + + SSL_get0_next_proto_negotiated(ss->ssl_, &npn_proto, &npn_proto_len); + + if (!npn_proto) { + return False(); + } + + return String::New((const char*) npn_proto, npn_proto_len); + } else { + return ss->selectedNPNProto_; + } +} + +Handle Connection::SetNPNProtocols(const Arguments& args) { + HandleScope scope; + + Connection *ss = Connection::Unwrap(args); + + if (args.Length() < 1 || !Buffer::HasInstance(args[0])) { + return ThrowException(Exception::Error(String::New( + "Must give a Buffer as first argument"))); + } + + // Release old handle + if (!ss->npnProtos_.IsEmpty()) { + ss->npnProtos_.Dispose(); + } + ss->npnProtos_ = Persistent::New(args[0]->ToObject()); + + return True(); +}; +#endif + static void HexEncode(unsigned char *md_value, int md_len, diff --git a/src/node_crypto.h b/src/node_crypto.h index 2cbffc2348..6432654be2 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -23,6 +23,7 @@ #define SRC_NODE_CRYPTO_H_ #include + #include #include @@ -33,6 +34,10 @@ #include #include +#ifdef OPENSSL_NPN_NEGOTIATED +#include +#endif + #define EVP_F_EVP_DECRYPTFINAL 101 @@ -94,6 +99,11 @@ class Connection : ObjectWrap { public: static void Initialize(v8::Handle target); +#ifdef OPENSSL_NPN_NEGOTIATED + v8::Persistent npnProtos_; + v8::Persistent selectedNPNProto_; +#endif + protected: static v8::Handle New(const v8::Arguments& args); static v8::Handle EncIn(const v8::Arguments& args); @@ -111,6 +121,20 @@ class Connection : ObjectWrap { static v8::Handle Start(const v8::Arguments& args); static v8::Handle Close(const v8::Arguments& args); +#ifdef OPENSSL_NPN_NEGOTIATED + // NPN + static v8::Handle GetNegotiatedProto(const v8::Arguments& args); + static v8::Handle SetNPNProtocols(const v8::Arguments& args); + static int AdvertiseNextProtoCallback_(SSL *s, + const unsigned char **data, + unsigned int *len, + void *arg); + static int SelectNextProtoCallback_(SSL *s, + unsigned char **out, unsigned char *outlen, + const unsigned char* in, + unsigned int inlen, void *arg); +#endif + int HandleBIOError(BIO *bio, const char* func, int rv); int HandleSSLError(const char* func, int rv); @@ -139,6 +163,7 @@ class Connection : ObjectWrap { BIO *bio_read_; BIO *bio_write_; SSL *ssl_; + bool is_server_; /* coverity[member_decl] */ }; diff --git a/test/simple/test-tls-npn-server-client.js b/test/simple/test-tls-npn-server-client.js new file mode 100644 index 0000000000..0c3d257604 --- /dev/null +++ b/test/simple/test-tls-npn-server-client.js @@ -0,0 +1,101 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var NPN_ENABLED = process.binding('constants').NPN_ENABLED; + +if (!process.versions.openssl || !NPN_ENABLED) { + console.error("Skipping because node compiled without OpenSSL or " + + "with old OpenSSL version."); + process.exit(0); +} + +var common = require('../common'), + assert = require('assert'), + fs = require('fs'), + tls = require('tls'); + +function filenamePEM(n) { + return require('path').join(common.fixturesDir, 'keys', n + ".pem"); +} + +function loadPEM(n) { + return fs.readFileSync(filenamePEM(n)); +} + +var serverOptions = { + key: loadPEM('agent2-key'), + cert: loadPEM('agent2-cert'), + crl: loadPEM('ca2-crl'), + NPNProtocols: ['a', 'b', 'c'] +}; + +var clientsOptions = [{ + key: serverOptions.key, + cert: serverOptions.cert, + crl: serverOptions.crl, + NPNProtocols: ['a', 'b', 'c'] +},{ + key: serverOptions.key, + cert: serverOptions.cert, + crl: serverOptions.crl, + NPNProtocols: ['c', 'b', 'e'] +},{ + key: serverOptions.key, + cert: serverOptions.cert, + crl: serverOptions.crl, + NPNProtocols: ['first-priority-unsupported', 'x', 'y'] +}]; + +var serverPort = common.PORT; + +var serverResults = [], + clientsResults = []; + +var server = tls.createServer(serverOptions, function(c) { + serverResults.push(c.npnProtocol); +}); +server.listen(serverPort, startTest); + +function startTest() { + function connectClient(options, callback) { + var client = tls.connect(serverPort, 'localhost', options, function() { + clientsResults.push(client.npnProtocol); + client.destroy(); + + callback(); + }); + }; + + connectClient(clientsOptions[0], function() { + connectClient(clientsOptions[1], function() { + connectClient(clientsOptions[2], function() { + server.close(); + }); + }); + }); +}; + +process.on('exit', function() { + assert.equal(serverResults[0], clientsResults[0]); + assert.equal(serverResults[1], clientsResults[1]); + assert.equal(serverResults[2], 'first-priority-unsupported'); + assert.equal(clientsResults[2], false); +});