From 4bad6d15cdba249176c5cca9d9e0db13e8a52c8f Mon Sep 17 00:00:00 2001 From: Chris Osborn Date: Tue, 31 May 2011 19:21:38 -0400 Subject: [PATCH 1/2] Added TLS-PSK support. Support for authenticating TLS connections via pre-shared keys. RFC 4279: http://www.ietf.org/rfc/rfc4279.txt --- doc/api/tls.markdown | 69 +++++++--- lib/crypto.js | 11 +- lib/tls.js | 126 ++++++++++++++----- src/node_crypto.cc | 188 +++++++++++++++++++++++++++- src/node_crypto.h | 24 ++++ test/simple/test-tls-psk-circuit.js | 135 ++++++++++++++++++++ test/simple/test-tls-psk-client.js | 178 ++++++++++++++++++++++++++ test/simple/test-tls-psk-server.js | 171 +++++++++++++++++++++++++ 8 files changed, 852 insertions(+), 50 deletions(-) create mode 100644 test/simple/test-tls-psk-circuit.js create mode 100644 test/simple/test-tls-psk-client.js create mode 100644 test/simple/test-tls-psk-server.js diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index e4ded41a775f..da6a14e32029 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -5,12 +5,12 @@ Use `require('tls')` to access this module. The `tls` module uses OpenSSL to provide Transport Layer Security and/or Secure Socket Layer: encrypted stream communication. -TLS/SSL is a public/private key infrastructure. Each client and each +Traditional TLS/SSL is a public/private key infrastructure. Each client and each server must have a private key. A private key is created like this openssl genrsa -out ryans-key.pem 1024 -All severs and some clients need to have a certificate. Certificates are public +All servers and some clients need to have a certificate. Certificates are public keys signed by a Certificate Authority or self-signed. The first step to getting a certificate is to create a "Certificate Signing Request" (CSR) file. This is done with: @@ -26,29 +26,56 @@ Alternatively you can send the CSR to a Certificate Authority for signing. (TODO: docs on creating a CA, for now interested users should just look at `test/fixtures/keys/Makefile` in the Node source code) +If node is compiled with OpenSSL v1.0.0 or later then TLS-PSK (RFC 4279) support is +available as an alternative to normal certificate-based authentication. PSK uses +a pre-shared key instead of certificates to authenticate a TLS connection, providing +mutual authentication (normal cert-based TLS is usually only one-way authentication). +PSK and certificate auth are not mutually exclusive; one server can accommodate both, +with the variety used determined by the normal cipher negotiation step. +Note that PSK is only a good choice where means exist to securely share a key with +every connecting machine, so it does not replace PKI for the majority of TLS uses. ### s = tls.connect(port, [host], [options], callback) Creates a new client connection to the given `port` and `host`. (If `host` defaults to `localhost`.) `options` should be an object which specifies - - `key`: A string or `Buffer` containing the private key of the server in - PEM format. (Required) + - `key`: A string or `Buffer` containing the client's private key in PEM format. - - `cert`: A string or `Buffer` containing the certificate key of the server in - PEM format. + - `cert`: A string or `Buffer` containing the client's certificate in PEM format. - `ca`: An array of strings or `Buffer`s of trusted certificates. If this is omitted several well known "root" CAs will be used, like VeriSign. These are used to authorize connections. + - `pskIdentity`: A string containing the TLS-PSK identity to use when connecting. + + - `pskKey`: A `Buffer` containing the binary pre-shared key corresponding + to the `pskIdentity`. + + - `pskCallback`: A callback that may be provided if you wish to select the identity + and key based on the "hint" provided by the server. The callback receives the hint + as its sole argument and must return an object with `pskIdentity` and `pskKey` + attributes as described above. This option is ignored if `pskIdentity` and `pskKey` + are provided outright. + + - `ciphers`: An OpenSSL-style cipher priority list (eg `RC4-SHA:AES128-SHA:AES256-SHA`). + Optional, but note that the cipher negotiation between client and server determines + how the TLS handshake will proceed; for PSK both client and server must be configured + to use PSK ciphers. For ease of use, if PSK identity options are provided but `ciphers` + is not, it will be configured with a default set of PSK ciphers. + `tls.connect()` returns a cleartext `CryptoStream` object. -After the TLS/SSL handshake the `callback` is called. The `callback` will be -called no matter if the server's certificate was authorized or not. It is up -to the user to test `s.authorized` to see if the server certificate was -signed by one of the specified CAs. If `s.authorized === false` then the error -can be found in `s.authorizationError`. +After the TLS/SSL handshake the `callback` is called. If using normal certificate-based +TLS, The `callback` will be called even if the server's certificate was not authorized. +It is up to the user to test `s.authorized` to see if the server certificate was signed +by one of the specified CAs. If `s.authorized === false` then the error can be found in +`s.authorizationError`. If using PSK, the handshake will only complete if the pre-shared +keys match; `s.authorized` will be `true` and `s.pskIdentity` will be clientss identity +string. If the keys do not match then the handshake will fail. The callback will not be +called but the cleartext stream will emit an `error` event. + ### STARTTLS @@ -98,10 +125,10 @@ This is a constructor for the `tls.Server` class. The options object has these possibilities: - `key`: A string or `Buffer` containing the private key of the server in - PEM format. (Required) + PEM format. (Required, unless using PSK) - `cert`: A string or `Buffer` containing the certificate key of the server in - PEM format. (Required) + PEM format. (Required, unless using PSK) - `ca`: An array of strings or `Buffer`s of trusted certificates. If this is omitted several well known "root" CAs will be used, like VeriSign. @@ -115,6 +142,20 @@ has these possibilities: which is not authorized with the list of supplied CAs. This option only has an effect if `requestCert` is `true`. Default: `false`. + - `pskCallback`: A function that receives a TLS-PSK identity string sent by + the connecting client and should synchronously return a `Buffer` containing + that user's key, or null if the user isn't recognized. (Required if using PSK) + + - `pskHint`: Optional "hint" string sent to each connecting client to help the + client determine which identity to use. By default, no hint is sent. See RFC + 4279 for details. + + - `ciphers`: An OpenSSL-style cipher priority list (eg `RC4-SHA:AES128-SHA:AES256-SHA`). + Optional, but note that the cipher negotiation between client and server determines + how the TLS handshake will proceed; for PSK both client and server must be configured + to use PSK ciphers. For ease of use, if a PSK callback is provided but `ciphers` + is not, it will be configured with a default set of PSK ciphers. + #### Event: 'secureConnection' @@ -129,7 +170,7 @@ client has verified by one of the supplied certificate authorities for the server. If `cleartextStream.authorized` is false, then `cleartextStream.authorizationError` is set to describe how authorization failed. Implied but worth mentioning: depending on the settings of the TLS -server, you unauthorized connections may be accepted. +server, your unauthorized connections may be accepted. #### server.listen(port, [host], [callback]) diff --git a/lib/crypto.js b/lib/crypto.js index d7f403ed100e..c03eaef94a88 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -101,6 +101,14 @@ exports.createCredentials = function(options, context) { } } + if (options.pskServerCallback) { + c.context.setPskServerCallback(options.pskServerCallback); + } + + if (options.pskServerHint) { + c.context.setPskHint(options.pskServerHint); + } + return c; }; @@ -158,5 +166,4 @@ exports.createDiffieHellman = function(size_or_key, enc) { } else { return new DiffieHellman(size_or_key, enc); } - -} +}; diff --git a/lib/tls.js b/lib/tls.js index 49c1ce84f8e5..cdf53888277e 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -36,6 +36,8 @@ if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) { debug = function() { }; } +// ciphers to use if a server or client is configured for TLS-PSK but does not specify a cipher list +var DefaultPSKCiphers = 'PSK-AES256-CBC-SHA:PSK-3DES-EDE-CBC-SHA:PSK-AES128-CBC-SHA:PSK-RC4-SHA'; var Connection = null; try { @@ -69,7 +71,7 @@ function convertNPNProtocols(NPNProtocols, out) { if (Buffer.isBuffer(NPNProtocols)) { out.NPNProtocols = NPNProtocols; } -}; +} // Base class of both CleartextStream and EncryptedStream function CryptoStream(pair) { @@ -478,15 +480,17 @@ EncryptedStream.prototype._pusher = function(pool, offset, length) { */ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized, - NPNProtocols) { + NPNProtocols, pskCallback) { if (!(this instanceof SecurePair)) { return new SecurePair(credentials, isServer, requestCert, rejectUnauthorized, - NPNProtocols); + NPNProtocols, + pskCallback); } + var self = this; events.EventEmitter.call(this); @@ -523,6 +527,10 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized, this.npnProtocol = null; } + if (pskCallback) { + this.ssl.setPskClientCallback(pskCallback); + } + /* Acts as a r/w stream to the cleartext side of the stream. */ this.cleartext = new CleartextStream(this); @@ -541,11 +549,14 @@ util.inherits(SecurePair, events.EventEmitter); exports.createSecurePair = function(credentials, isServer, requestCert, - rejectUnauthorized) { + rejectUnauthorized, + pskCallback) { var pair = new SecurePair(credentials, isServer, requestCert, - rejectUnauthorized); + rejectUnauthorized, + null, + pskCallback); return pair; }; @@ -675,7 +686,7 @@ SecurePair.prototype.error = function() { } }; -// TODO: support anonymous (nocert) and PSK +// TODO: support anonymous (nocert) // AUTHENTICATION MODES @@ -774,11 +785,11 @@ function Server(/* [options], listener */) { ciphers: self.ciphers, secureProtocol: self.secureProtocol, secureOptions: self.secureOptions, - crl: self.crl + crl: self.crl, + pskServerCallback: self.pskCallback, + pskServerHint: self.pskHint }); - sharedCreds.context.setCiphers('RC4-SHA:AES128-SHA:AES256-SHA'); - // constructor call net.Server.call(this, function(socket) { var creds = crypto.createCredentials(null, sharedCreds.context); @@ -795,25 +806,35 @@ function Server(/* [options], listener */) { pair.on('secure', function() { pair.cleartext.authorized = false; pair.cleartext.npnProtocol = pair.npnProtocol; - if (!self.requestCert) { + + // if using PSK, then handshake completion implies authorization + if (pair.ssl.pskIdentity) { + pair.cleartext.pskIdentity = pair.ssl.pskIdentity; + pair.cleartext.authorized = true; cleartext._controlReleased = true; self.emit('secureConnection', pair.cleartext, pair.encrypted); - } else { - var verifyError = pair.ssl.verifyError(); - if (verifyError) { - pair.cleartext.authorizationError = verifyError; - - if (self.rejectUnauthorized) { - socket.destroy(); - pair.destroy(); + } + else { + // otherwise, might need to check for cert verification errors + if (!self.requestCert) { + cleartext._controlReleased = true; + self.emit('secureConnection', pair.cleartext, pair.encrypted); + } else { + var verifyError = pair.ssl.verifyError(); + if (verifyError) { + pair.cleartext.authorizationError = verifyError; + if (self.rejectUnauthorized) { + socket.destroy(); + pair.destroy(); + } else { + cleartext._controlReleased = true; + self.emit('secureConnection', pair.cleartext, pair.encrypted); + } } else { + pair.cleartext.authorized = true; cleartext._controlReleased = true; self.emit('secureConnection', pair.cleartext, pair.encrypted); } - } else { - pair.cleartext.authorized = true; - cleartext._controlReleased = true; - self.emit('secureConnection', pair.cleartext, pair.encrypted); } } }); @@ -847,6 +868,12 @@ Server.prototype.setOptions = function(options) { this.rejectUnauthorized = false; } + if (options.pskCallback && !options.ciphers) { + // set default PSK ciphers if it looks like we're trying to use + // PSK but no preference has been specified + options.ciphers = DefaultPSKCiphers; + } + if (options.key) this.key = options.key; if (options.cert) this.cert = options.cert; if (options.ca) this.ca = options.ca; @@ -856,6 +883,8 @@ Server.prototype.setOptions = function(options) { if (options.secureProtocol) this.secureProtocol = options.secureProtocol; if (options.secureOptions) this.secureOptions = options.secureOptions; if (options.NPNProtocols) convertNPNProtocols(options.NPNProtocols, this); + if (options.pskCallback) this.pskCallback = options.pskCallback; + if (options.pskHint) this.pskHint = options.pskHint; }; @@ -893,6 +922,23 @@ exports.connect = function(port /* host, options, cb */) { } } + // if a PSK identity/key pair was provided, translate it into the + // callback expected by SecurePair + if (options.pskIdentity && options.pskKey) { + options.pskClientCallback = function(hint) { + return { + identity: options.pskIdentity, + key: options.pskKey + } + }; + } + + if (options.pskClientCallback && !options.ciphers) { + // set default PSK ciphers if it looks like we're trying to use + // PSK but no preference has been specified + options.ciphers = DefaultPSKCiphers; + } + var socket = new net.Stream(); var sslcontext = crypto.createCredentials(options); @@ -900,24 +946,42 @@ exports.connect = function(port /* host, options, cb */) { convertNPNProtocols(options.NPNProtocols, this); var pair = new SecurePair(sslcontext, false, true, false, - this.NPNProtocols); + this.NPNProtocols, options.pskClientCallback); var cleartext = pipe(pair, socket); + // This handler runs if the socket closes before the TLS handshake completes, + // either because the remote host aborted a normal TLS handshake for some reason + // or, in the case of TLS-PSK, if the identity and/or secret is invalid. + function onSocketClose() { + cleartext.emit('error', new Error('remote host closed the connection')); + } + socket.on('close', onSocketClose); + socket.connect(port, host); pair.on('secure', function() { - var verifyError = pair.ssl.verifyError(); - + // the handshake is complete, so remove the close listener; + // a close hereafter is just a close, not an error + socket.removeListener('close', onSocketClose); cleartext.npnProtocol = pair.npnProtocol; - - if (verifyError) { - cleartext.authorized = false; - cleartext.authorizationError = verifyError; - } else { + if (pair.ssl.pskIdentity) { + // if there is a PSK identity, then we're authorized, since PSK inherently + // provides mutual authentication + cleartext.pskIdentity = pair.ssl.pskIdentity; cleartext.authorized = true; + } else { + // otherwise check the ssl verifyError and make it + // available for the client code to inspect + var verifyError = pair.ssl.verifyError(); + if (verifyError) { + cleartext.authorized = false; + cleartext.authorizationError = verifyError; + } else { + cleartext.authorized = true; + } } - + // call the callback, indicating that the handshake is complete if (cb) cb(); }); diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 13749a83fec4..58395653a166 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -76,6 +76,11 @@ void SecureContext::Initialize(Handle target) { NODE_SET_PROTOTYPE_METHOD(t, "setOptions", SecureContext::SetOptions); NODE_SET_PROTOTYPE_METHOD(t, "close", SecureContext::Close); +#ifdef OPENSSL_PSK_SUPPORT + NODE_SET_PROTOTYPE_METHOD(t, "setPskHint", SecureContext::SetPskHint); + NODE_SET_PROTOTYPE_METHOD(t, "setPskServerCallback", SecureContext::SetPskServerCallback); +#endif + target->Set(String::NewSymbol("SecureContext"), t->GetFunction()); } @@ -145,6 +150,11 @@ Handle SecureContext::Init(const Arguments& args) { // SSL_CTX_set_session_cache_mode(sc->ctx_,SSL_SESS_CACHE_OFF); sc->ca_store_ = NULL; + + // Establish a back link from the SSL_CTX struct to the SecureContext object + // using OpenSSL's application-specific data storage. Used by PSK support. + SSL_CTX_set_app_data(sc->ctx_, sc); + return True(); } @@ -197,6 +207,75 @@ static X509* LoadX509 (Handle v) { } +#ifdef OPENSSL_PSK_SUPPORT + +unsigned int SecureContext::PskServerCallback_(SSL *ssl, + const char *identity, + unsigned char *psk, + unsigned int max_psk_len) { + + // translate back from the connection to the context + SSL_CTX *ctx = SSL_get_SSL_CTX(ssl); + + // then from the SSL_CTX to the SecureContext + SecureContext *sc = static_cast(SSL_CTX_get_app_data(ctx)); + + Local argv[1]; + argv[0] = Local::New(String::New(identity)); + + Local result = sc->psk_server_cb_->Call(Context::GetCurrent()->Global(), 1, argv); + + // The result is expected to be a buffer containing the key. If this + // isn't the case then return 0, indicating that the identity isn't found. + if (Buffer::HasInstance(result)) { + + // write the key into the buffer provided + Local keyBuffer = result->ToObject(); + int len = Buffer::Length(keyBuffer); + if (len <= max_psk_len) { // avoids buffer overrun, but should have better error handling + memcpy (psk, Buffer::Data(keyBuffer), len); + return len; + } + + // TODO: is it possible to throw an exception here and have it show up in JS? + // That would allow for better error reporting. + } + + return 0; +} + +Handle SecureContext::SetPskHint(const Arguments& args) { + HandleScope scope; + + SecureContext *sc = ObjectWrap::Unwrap(args.Holder()); + + if (args.Length() != 1 || !args[0]->IsString()) { + return ThrowException(Exception::TypeError(String::New("Bad parameter"))); + } + + String::Utf8Value hint(args[0]->ToString()); + + return SSL_CTX_use_psk_identity_hint(sc->ctx_, *hint) ? True() : False(); +} + +Handle SecureContext::SetPskServerCallback(const v8::Arguments& args) { + HandleScope scope; + + if (args.Length() != 1 || !args[0]->IsFunction()) { + return ThrowException(Exception::TypeError(String::New("Single function parameter required"))); + } + + SecureContext *sc = ObjectWrap::Unwrap(args.Holder()); + + sc->psk_server_cb_ = Persistent::New(Local::Cast(args[0])); + + SSL_CTX_set_psk_server_callback(sc->ctx_, SecureContext::PskServerCallback_); + + return True(); +} + +#endif // OPENSSL_PSK_SUPPORT + Handle SecureContext::SetKey(const Arguments& args) { HandleScope scope; @@ -289,7 +368,6 @@ int SSL_CTX_use_certificate_chain(SSL_CTX *ctx, BIO *in) { return ret; } - Handle SecureContext::SetCert(const Arguments& args) { HandleScope scope; @@ -582,6 +660,10 @@ void Connection::Initialize(Handle target) { NODE_SET_PROTOTYPE_METHOD(t, "setNPNProtocols", Connection::SetNPNProtocols); #endif +#ifdef OPENSSL_PSK_SUPPORT + NODE_SET_PROTOTYPE_METHOD(t, "setPskClientCallback", Connection::SetPskClientCallback); +#endif + target->Set(String::NewSymbol("Connection"), t->GetFunction()); } @@ -721,8 +803,13 @@ Handle Connection::New(const Arguments& args) { p->bio_read_ = BIO_new(BIO_s_mem()); p->bio_write_ = BIO_new(BIO_s_mem()); -#ifdef OPENSSL_NPN_NEGOTIATED + // Establish a back link from the SSL struct to the Connection object + // using OpenSSL's application-specific data storage. + // Used by both NPN and PSK support. SSL_set_app_data(p->ssl_, p); + + +#ifdef OPENSSL_NPN_NEGOTIATED if (is_server) { // Server should advertise NPN protocols SSL_CTX_set_next_protos_advertised_cb(sc->ctx_, @@ -1110,7 +1197,21 @@ Handle Connection::IsInitFinished(const Arguments& args) { Connection *ss = Connection::Unwrap(args); if (ss->ssl_ == NULL) return False(); - return SSL_is_init_finished(ss->ssl_) ? True() : False(); + + if (SSL_is_init_finished(ss->ssl_)) { +#ifdef OPENSSL_PSK_SUPPORT + // if we're using PSK, set the pskIdentity string on + // the Connection for use by Javascript + const char* pskId = SSL_get_psk_identity(ss->ssl_); + if (pskId) { + ss->handle_->Set(String::New("pskIdentity"), String::New(pskId)); + } +#endif + return True(); + } + else { + return False(); + } } @@ -1331,6 +1432,87 @@ Handle Connection::SetNPNProtocols(const Arguments& args) { #endif +#ifdef OPENSSL_PSK_SUPPORT +Handle Connection::SetPskClientCallback(const v8::Arguments& args) { + HandleScope scope; + + if (args.Length() != 1 || !args[0]->IsFunction()) { + return ThrowException(Exception::TypeError(String::New("Single function parameter required"))); + } + + Connection *ss = ObjectWrap::Unwrap(args.Holder()); + + ss->psk_client_cb_ = Persistent::New(Local::Cast(args[0])); + + SSL_set_psk_client_callback(ss->ssl_, Connection::PskClientCallback_); + + return True(); +} + + +unsigned int Connection::PskClientCallback_(SSL *ssl, + const char *hint, + char *identity, + unsigned int max_identity_len, + unsigned char *psk, + unsigned int max_psk_len) { + + HandleScope scope; + + Connection *ss = static_cast(SSL_get_app_data(ssl)); + + Local argv[1]; + argv[0] = Local::New(hint ? String::New(hint) : Undefined()); + + if (ss->psk_client_cb_->IsNull()) { + // no callback set + return 0; + } + + // call the JS callback. It's wrapped in a TryCatch because otherwise if it throws + // we wind up with a segfault. Eating the error isn't great; it would be better to + // put the Connection into an error state and hopefully get the message back to the + // application code somehow. + TryCatch tc; + + Local result = ss->psk_client_cb_->Call(Context::GetCurrent()->Global(), 1, argv); + + if (tc.HasCaught()) { + return 0; + } + + // The result is expected to be an object with "identity" string and "key" + // buffer values. If this isn't the case then return 0, indicating that the + // identity isn't found. + if (result->IsObject()) { + Local idAndKey = result->ToObject(); + + Local id = idAndKey->Get(String::New("identity")); + Local key = idAndKey->Get(String::New("key")); + + if (id->IsString() && Buffer::HasInstance(key)) { + + // write the chosen client identity string into the buffer provided + id->ToString()->WriteAscii(identity, 0, max_identity_len); + + // write the id's binary key into the buffer provided + Local keyBuffer = key->ToObject(); + int len = Buffer::Length(keyBuffer); + if (len <= max_psk_len) { // avoids buffer overrun, but should have better error handling + memcpy (psk, Buffer::Data(keyBuffer), len); + return len; + } + } + + // TODO: is it possible to throw an exception here and have it show up in JS? + // That would allow for better error reporting. + } + + return 0; +} +#endif // OPENSSL_PSK_SUPPORT + + static void HexEncode(unsigned char *md_value, int md_len, char** md_hexdigest, diff --git a/src/node_crypto.h b/src/node_crypto.h index 6432654be2d8..ac0062a80aa4 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -38,6 +38,12 @@ #include #endif +// TLS-PSK support requires OpenSSL v1.0.0 or later +#if OPENSSL_VERSION_NUMBER >= 0x10000000L +#define OPENSSL_PSK_SUPPORT +#endif + + #define EVP_F_EVP_DECRYPTFINAL 101 @@ -93,6 +99,15 @@ class SecureContext : ObjectWrap { } private: + +#ifdef OPENSSL_PSK_SUPPORT + v8::Persistent psk_server_cb_; + static v8::Handle SetPskHint(const v8::Arguments& args); + static v8::Handle SetPskServerCallback(const v8::Arguments& args); + static unsigned int PskServerCallback_(SSL *ssl, const char *identity, + unsigned char *psk, unsigned int max_psk_len); +#endif + }; class Connection : ObjectWrap { @@ -165,6 +180,15 @@ class Connection : ObjectWrap { SSL *ssl_; bool is_server_; /* coverity[member_decl] */ + +#ifdef OPENSSL_PSK_SUPPORT + static v8::Handle SetPskClientCallback(const v8::Arguments& args); + static unsigned int PskClientCallback_(SSL *ssl, const char *hint, + char *identity, unsigned int max_identity_len, + unsigned char *psk, unsigned int max_psk_len); + v8::Persistent psk_client_cb_; +#endif + }; void InitCrypto(v8::Handle target); diff --git a/test/simple/test-tls-psk-circuit.js b/test/simple/test-tls-psk-circuit.js new file mode 100644 index 000000000000..6ac8afe48d17 --- /dev/null +++ b/test/simple/test-tls-psk-circuit.js @@ -0,0 +1,135 @@ +// 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. + + +if (!process.versions.openssl) { + console.error("Skipping because node compiled without OpenSSL."); + process.exit(0); +} +if (parseInt(process.versions.openssl[0]) < 1) { + console.error("Skipping because node compiled with old OpenSSL version."); + process.exit(0); +} + +var common = require('../common'); +var assert = require('assert'); +var tls = require('tls'); + +var serverPort = common.PORT; + +var serverResults = []; +var clientResults = []; +var connectingIds = []; + +var users = { + UserA: new Buffer("d731ef57be09e5204f0b205b60627028", 'hex'), + UserB: new Buffer("82072606b502b0f4025e90eb75fe137d", 'hex') +}; + + +var serverOptions = { + // configure a mixed set of cert and PSK ciphers + ciphers: 'RC4-SHA:AES128-SHA:AES256-SHA:PSK-AES256-CBC-SHA:PSK-3DES-EDE-CBC-SHA:PSK-AES128-CBC-SHA:PSK-RC4-SHA', + pskCallback: function(id) { + connectingIds.push(id); + if (id in users) { + return users[id]; + } + } +}; + +var clientOptions = [{ + pskIdentity: 'UserA', + pskKey: users.UserA +},{ + pskIdentity: 'UserB', + pskKey: users.UserB +},{ + pskIdentity: 'UserC', // unrecognized user should fail handshake + pskKey: users.UserB +},{ + pskIdentity: 'UserB', // recognized user but incorrect secret should fail handshake + pskKey: new Buffer("025e90eb75fe137d82072606b502b0f4", 'hex') +},{ + pskIdentity: 'UserB', + pskKey: users.UserB +}]; + +var server = tls.createServer(serverOptions, function(c) { + console.log('%s connected', c.pskIdentity); + serverResults.push(c.pskIdentity + ' ' + (c.authorized ? 'authorized' : 'not authorized')); + c.once('data', function(data) { + assert.equal(data.toString(), 'Hi.'); + c.write('Hi ' + c.pskIdentity); + c.once('data', function(data) { + assert.equal(data.toString(), 'Bye.'); + }); + }); +}); +server.listen(serverPort, startTest); + +function startTest() { + function connectClient(options, callback) { + console.log('connecting as %s', options.pskIdentity); + var client = tls.connect(serverPort, 'localhost', options, function() { + clientResults.push(client.pskIdentity + ' ' + (client.authorized ? 'authorized' : 'not authorized')); + client.write('Hi.'); + client.on('data', function(data) { + assert.equal(data.toString(), 'Hi ' + options.pskIdentity); + client.end('Bye.'); + }); + callback(); + }); + client.on('error', function(err) { + console.log('connection as %s rejected', options.pskIdentity); + clientResults.push(err.message); + callback(err); + }); + } + + function doTestCase(tcnum) { + if (tcnum >= clientOptions.length) { + server.close(); + } else { + connectClient(clientOptions[tcnum], function(err) { + doTestCase(tcnum+1); + }); + } + } + doTestCase(0); + +} + +process.on('exit', function() { + assert.deepEqual(serverResults, ['UserA authorized', + 'UserB authorized', + 'UserB authorized']); + assert.deepEqual(clientResults, ['UserA authorized', + 'UserB authorized', + 'remote host closed the connection', + 'remote host closed the connection', + 'UserB authorized']); + assert.deepEqual(connectingIds, ['UserA', + 'UserB', + 'UserC', + 'UserB', + 'UserB']); +}); diff --git a/test/simple/test-tls-psk-client.js b/test/simple/test-tls-psk-client.js new file mode 100644 index 000000000000..edbdc06fa909 --- /dev/null +++ b/test/simple/test-tls-psk-client.js @@ -0,0 +1,178 @@ +// 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. + +// There is a bug with 'openssl s_server' which makes it not flush certain +// important events to stdout when done over a pipe. Therefore we skip this +// test for all openssl versions less than 1.0.0. +if (!process.versions.openssl) { + console.error("Skipping because node compiled without OpenSSL."); + process.exit(0); +} +if (parseInt(process.versions.openssl[0]) < 1) { + console.error("Skipping because node compiled with old OpenSSL version."); + process.exit(0); +} + + +var common = require('../common'); +var join = require('path').join; +var net = require('net'); +var assert = require('assert'); +var fs = require('fs'); +var crypto = require('crypto'); +var tls = require('tls'); +var spawn = require('child_process').spawn; + +// FIXME: Avoid the common PORT as this test currently hits a C-level +// assertion error with node_g. The program aborts without HUPing +// the openssl s_server thus causing many tests to fail with +// EADDRINUSE. +var PORT = common.PORT + 5; + +var pskey = "d731ef57be09e5204f0b205b60627028"; +var identity = 'Client_identity'; // openssl s_client supports specifying the identity but s_server, oddly, does not + +var PSKCiphers = 'PSK-AES256-CBC-SHA:PSK-3DES-EDE-CBC-SHA:PSK-AES128-CBC-SHA:PSK-RC4-SHA'; + +var server = spawn('openssl', ['s_server', + '-accept', PORT, + '-psk', pskey, + '-psk_hint', "the hint you're looking for", + '-nocert']); +server.stdout.pipe(process.stdout); +server.stderr.pipe(process.stdout); + + +var state = 'WAIT-ACCEPT'; + +var serverStdoutBuffer = ''; +server.stdout.setEncoding('utf8'); +server.stdout.on('data', function(s) { + serverStdoutBuffer += s; + console.error(state); + switch (state) { + case 'WAIT-ACCEPT': + if (/ACCEPT/g.test(serverStdoutBuffer)) { + // Give s_server half a second to start up. + setTimeout(startClient, 500); + state = 'WAIT-HELLO'; + } + break; + + case 'WAIT-HELLO': + if (/hello/g.test(serverStdoutBuffer)) { + + // End the current SSL connection and exit. + // See s_server(1ssl). + server.stdin.write('Q'); + + state = 'WAIT-SERVER-CLOSE'; + } + break; + + default: + break; + } +}); + + +var timeout = setTimeout(function () { + server.kill(); + process.exit(1); +}, 5000); + +var gotWriteCallback = false; +var serverExitCode = -1; + +server.on('exit', function(code) { + serverExitCode = code; + clearTimeout(timeout); +}); + + +function startClient() { + var s = new net.Stream(); + + var sslcontext = crypto.createCredentials({}); + sslcontext.context.setCiphers(PSKCiphers); + + function clientCallback(hint) { + if (hint == "the hint you're looking for") { + return { + identity: identity, + key: new Buffer(pskey, 'hex') + } + } + return null; + } + + var pair = tls.createSecurePair(sslcontext, false, null, null, clientCallback); + + assert.ok(pair.encrypted.writable); + assert.ok(pair.cleartext.writable); + + pair.encrypted.pipe(s); + s.pipe(pair.encrypted); + + s.connect(PORT); + + s.on('connect', function() { + console.log('client connected'); + }); + + pair.on('secure', function() { + console.log('client: connected+secure!'); + console.log('client pair.cleartext.getCipher(): %j', + pair.cleartext.getCipher()); + setTimeout(function() { + pair.cleartext.write('hello\r\n', function () { + gotWriteCallback = true; + }); + }, 500); + }); + + pair.cleartext.on('data', function(d) { + console.log('cleartext: %s', d.toString()); + }); + + s.on('close', function() { + console.log('client close'); + }); + + pair.encrypted.on('error', function(err) { + console.log('encrypted error: ' + err); + }); + + s.on('error', function(err) { + console.log('socket error: ' + err); + }); + + pair.on('error', function(err) { + console.log('secure error: ' + err); + }); +} + + +process.on('exit', function() { + assert.equal(0, serverExitCode); + assert.equal('WAIT-SERVER-CLOSE', state); + assert.ok(gotWriteCallback); +}); diff --git a/test/simple/test-tls-psk-server.js b/test/simple/test-tls-psk-server.js new file mode 100644 index 000000000000..0fb05551bc82 --- /dev/null +++ b/test/simple/test-tls-psk-server.js @@ -0,0 +1,171 @@ +// 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. + +if (!process.versions.openssl) { + console.error("Skipping because node compiled without OpenSSL."); + process.exit(0); +} +if (parseInt(process.versions.openssl[0]) < 1) { + console.error("Skipping because node compiled with old OpenSSL version."); + process.exit(0); +} + + +var common = require('../common'); +var assert = require('assert'); + +var join = require('path').join; +var net = require('net'); +var fs = require('fs'); +var crypto = require('crypto'); +var tls = require('tls'); +var spawn = require('child_process').spawn; + +var connections = 0; +var pskey = "d731ef57be09e5204f0b205b60627028"; +var identity = 'TestUser'; + +var PSKCiphers = 'PSK-AES256-CBC-SHA:PSK-3DES-EDE-CBC-SHA:PSK-AES128-CBC-SHA:PSK-RC4-SHA'; + +function log(a) { + console.error('***server*** ' + a); +} + +var server = net.createServer(function(socket) { + connections++; + log('connection fd=' + socket.fd); + + var sslcontext = crypto.createCredentials({}); + sslcontext.context.setCiphers(PSKCiphers); + + function serverCallback(id) { + if (id == identity) { + return new Buffer(pskey, 'hex'); + } + return null; + } + sslcontext.context.setPskServerCallback(serverCallback); + + var pair = tls.createSecurePair(sslcontext, true); + + assert.ok(pair.encrypted.writable); + assert.ok(pair.cleartext.writable); + + pair.encrypted.pipe(socket); + socket.pipe(pair.encrypted); + + log('i set it secure'); + + pair.on('secure', function() { + log('connected+secure!'); + pair.cleartext.write('hello\r\n'); + log(pair.cleartext.getPeerCertificate()); + log(pair.cleartext.getCipher()); + }); + + pair.cleartext.on('data', function(data) { + log('read bytes ' + data.length); + pair.cleartext.write(data); + }); + + socket.on('end', function() { + log('socket end'); + }); + + pair.cleartext.on('error', function(err) { + log('got error: '); + log(err); + log(err.stack); + socket.destroy(); + }); + + pair.encrypted.on('error', function(err) { + log('encrypted error: '); + log(err); + log(err.stack); + socket.destroy(); + }); + + socket.on('error', function(err) { + log('socket error: '); + log(err); + log(err.stack); + socket.destroy(); + }); + + socket.on('close', function(err) { + log('socket closed'); + }); + + pair.on('error', function(err) { + log('secure error: '); + log(err); + log(err.stack); + socket.destroy(); + }); +}); + +var gotHello = false; +var sentWorld = false; +var gotWorld = false; +var opensslExitCode = -1; + +server.listen(common.PORT, function() { + var client = spawn('openssl', ['s_client', + '-connect', '127.0.0.1:' + common.PORT, + '-psk', pskey, + '-cipher', PSKCiphers, + '-psk_identity', identity]); + + var out = ''; + + client.stdout.setEncoding('utf8'); + client.stdout.on('data', function(d) { + out += d; + + if (!gotHello && /hello/.test(out)) { + gotHello = true; + client.stdin.write('world\r\n'); + sentWorld = true; + } + + if (!gotWorld && /world/.test(out)) { + gotWorld = true; + client.stdin.end(); + } + }); + + client.stdout.pipe(process.stdout, { end: false }); + client.stderr.pipe(process.stderr, { end: false }); + + client.on('exit', function(code) { + opensslExitCode = code; + server.close(); + }); +}); + +process.on('exit', function() { + assert.equal(1, connections); + assert.ok(gotHello); + assert.ok(sentWorld); + assert.ok(gotWorld); + assert.equal(0, opensslExitCode); +}); From 01869152d7bd9db59dc1adb539577796d87c3874 Mon Sep 17 00:00:00 2001 From: Chris Osborn Date: Wed, 22 Jun 2011 17:29:46 -0400 Subject: [PATCH 2/2] Fixes recommended by Paul Querna. --- doc/api/tls.markdown | 9 ++++----- src/node_crypto.cc | 4 ---- src/node_crypto.h | 4 +++- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index da6a14e32029..acab83cc0cdd 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -29,11 +29,10 @@ Alternatively you can send the CSR to a Certificate Authority for signing. If node is compiled with OpenSSL v1.0.0 or later then TLS-PSK (RFC 4279) support is available as an alternative to normal certificate-based authentication. PSK uses a pre-shared key instead of certificates to authenticate a TLS connection, providing -mutual authentication (normal cert-based TLS is usually only one-way authentication). -PSK and certificate auth are not mutually exclusive; one server can accommodate both, -with the variety used determined by the normal cipher negotiation step. -Note that PSK is only a good choice where means exist to securely share a key with -every connecting machine, so it does not replace PKI for the majority of TLS uses. +mutual authentication. PSK and certificate auth are not mutually exclusive; one server +can accommodate both, with the variety used determined by the normal cipher negotiation +step. Note that PSK is only a good choice where means exist to securely share a key +with every connecting machine, so it does not replace PKI for the majority of TLS uses. ### s = tls.connect(port, [host], [options], callback) diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 58395653a166..a620630e811d 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -237,8 +237,6 @@ unsigned int SecureContext::PskServerCallback_(SSL *ssl, return len; } - // TODO: is it possible to throw an exception here and have it show up in JS? - // That would allow for better error reporting. } return 0; @@ -1504,8 +1502,6 @@ unsigned int Connection::PskClientCallback_(SSL *ssl, } } - // TODO: is it possible to throw an exception here and have it show up in JS? - // That would allow for better error reporting. } return 0; diff --git a/src/node_crypto.h b/src/node_crypto.h index ac0062a80aa4..fc215e78e4d9 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -38,10 +38,12 @@ #include #endif -// TLS-PSK support requires OpenSSL v1.0.0 or later +// TLS-PSK support requires OpenSSL v1.0.0 or later built with PSK enabled #if OPENSSL_VERSION_NUMBER >= 0x10000000L +#ifndef OPENSSL_NO_PSK #define OPENSSL_PSK_SUPPORT #endif +#endif #define EVP_F_EVP_DECRYPTFINAL 101