diff --git a/doc/api/crypto.markdown b/doc/api/crypto.markdown index 683a7ea402..9700c58d6a 100644 --- a/doc/api/crypto.markdown +++ b/doc/api/crypto.markdown @@ -14,8 +14,10 @@ It also offers a set of wrappers for OpenSSL's hash, hmac, cipher, decipher, sig Creates a credentials object, with the optional details being a dictionary with keys: +* `pfx` : A string or buffer holding the PFX or PKCS12 encoded private key, certificate and CA certificates * `key` : a string holding the PEM encoded private key * `cert` : a string holding the PEM encoded certificate +* `passphrase` : A string of passphrase for the private key or pfx * `ca` : either a string or list of strings of PEM encoded CA certificates to trust. * `ciphers`: a string describing the ciphers to use or exclude. Consult for details diff --git a/doc/api/https.markdown b/doc/api/https.markdown index 32724ca576..9f505a41d2 100644 --- a/doc/api/https.markdown +++ b/doc/api/https.markdown @@ -32,6 +32,19 @@ Example: res.end("hello world\n"); }).listen(8000); +Or + + var https = require('https'); + var fs = require('fs'); + + var options = { + pfx: fs.readFileSync('server.pfx') + }; + + https.createServer(options, function (req, res) { + res.writeHead(200); + res.end("hello world\n"); + }).listen(8000); ## https.request(options, callback) @@ -91,8 +104,9 @@ The options argument has the following options The following options from [tls.connect()](tls.html#tls.connect) can also be specified. However, a [globalAgent](#https.globalAgent) silently ignores these. +- `pfx`: Certificate, Private key and CA certificates to use for SSL. Default `null`. - `key`: Private key to use for SSL. Default `null`. -- `passphrase`: A string of passphrase for the private key. Default `null`. +- `passphrase`: A string of passphrase for the private key or pfx. Default `null`. - `cert`: Public x509 certificate to use. Default `null`. - `ca`: An authority certificate or array of authority certificates to check the remote host against. diff --git a/doc/api/tls.markdown b/doc/api/tls.markdown index c991973c6f..d8ed91ada6 100644 --- a/doc/api/tls.markdown +++ b/doc/api/tls.markdown @@ -28,6 +28,17 @@ 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) +To create .pfx or .p12, do this: + + openssl pkcs12 -export -in agent5-cert.pem -inkey agent5-key.pem \ + -certfile ca-cert.pem -out agent5.pfx + + - `in`: certificate + - `inkey`: private key + - `certfile`: all CA certs concatenated in one file like + `cat ca1-cert.pem ca2-cert.pem > ca-cert.pem` + + ## Client-initiated renegotiation attack mitigation @@ -72,10 +83,14 @@ The `connectionListener` argument is automatically set as a listener for the [secureConnection](#event_secureConnection_) event. The `options` object has these possibilities: + - `pfx`: A string or `Buffer` containing the private key, certificate and + CA certs of the server in PFX or PKCS12 format. (Mutually exclusive with + the `key`, `cert` and `ca` options.) + - `key`: A string or `Buffer` containing the private key of the server in PEM format. (Required) - - `passphrase`: A string of passphrase for the private key. + - `passphrase`: A string of passphrase for the private key or pfx. - `cert`: A string or `Buffer` containing the certificate key of the server in PEM format. (Required) @@ -137,7 +152,29 @@ Here is a simple example echo server: console.log('server bound'); }); +Or + + var tls = require('tls'); + var fs = require('fs'); + + var options = { + pfx: fs.readFileSync('server.pfx'), + // This is necessary only if using the client certificate authentication. + requestCert: true, + + }; + + var server = tls.createServer(options, function(cleartextStream) { + console.log('server connected', + cleartextStream.authorized ? 'authorized' : 'unauthorized'); + cleartextStream.write("welcome!\n"); + cleartextStream.setEncoding('utf8'); + cleartextStream.pipe(cleartextStream); + }); + server.listen(8000, function() { + console.log('server bound'); + }); You can test this server by connecting to it with `openssl s_client`: @@ -149,10 +186,13 @@ You can test this server by connecting to it with `openssl s_client`: Creates a new client connection to the given `port` and `host`. (If `host` defaults to `localhost`.) `options` should be an object which specifies + - `pfx`: A string or `Buffer` containing the private key, certificate and + CA certs of the server in PFX or PKCS12 format. + - `key`: A string or `Buffer` containing the private key of the client in PEM format. - - `passphrase`: A string of passphrase for the private key. + - `passphrase`: A string of passphrase for the private key or pfx. - `cert`: A string or `Buffer` containing the certificate key of the client in PEM format. @@ -206,6 +246,28 @@ Here is an example of a client of echo server as described previously: server.close(); }); +Or + + var tls = require('tls'); + var fs = require('fs'); + + var options = { + pfx: fs.readFileSync('client.pfx') + }; + + var cleartextStream = tls.connect(8000, options, function() { + console.log('client connected', + cleartextStream.authorized ? 'authorized' : 'unauthorized'); + process.stdin.pipe(cleartextStream); + process.stdin.resume(); + }); + cleartextStream.setEncoding('utf8'); + cleartextStream.on('data', function(data) { + console.log(data); + }); + cleartextStream.on('end', function() { + server.close(); + }); ## tls.createSecurePair([credentials], [isServer], [requestCert], [rejectUnauthorized]) diff --git a/lib/crypto.js b/lib/crypto.js index 6244f2c8d4..d1c308dbac 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -114,6 +114,14 @@ exports.createCredentials = function(options, context) { c.context.setSessionIdContext(options.sessionIdContext); } + if (options.pfx) { + if (options.passphrase) { + c.context.loadPKCS12(options.pfx, options.passphrase); + } else { + c.context.loadPKCS12(options.pfx); + } + } + return c; }; diff --git a/lib/tls.js b/lib/tls.js index f9bb63c6a1..96c52269c1 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -912,6 +912,7 @@ function Server(/* [options], listener */) { this.setOptions(options); var sharedCreds = crypto.createCredentials({ + pfx : self.pfx, key: self.key, passphrase: self.passphrase, cert: self.cert, @@ -996,6 +997,7 @@ Server.prototype.setOptions = function(options) { this.rejectUnauthorized = false; } + if (options.pfx) this.pfx = options.pfx; if (options.key) this.key = options.key; if (options.passphrase) this.passphrase = options.passphrase; if (options.cert) this.cert = options.cert; diff --git a/src/node_crypto.cc b/src/node_crypto.cc index bf95d79701..b2858f5088 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -43,6 +43,7 @@ # include #endif + #if OPENSSL_VERSION_NUMBER >= 0x10000000L # define OPENSSL_CONST const #else @@ -169,6 +170,7 @@ void SecureContext::Initialize(Handle target) { NODE_SET_PROTOTYPE_METHOD(t, "setSessionIdContext", SecureContext::SetSessionIdContext); NODE_SET_PROTOTYPE_METHOD(t, "close", SecureContext::Close); + NODE_SET_PROTOTYPE_METHOD(t, "loadPKCS12", SecureContext::LoadPKCS12); target->Set(String::NewSymbol("SecureContext"), t->GetFunction()); } @@ -595,6 +597,83 @@ Handle SecureContext::Close(const Arguments& args) { return False(); } +//Takes .pfx or .p12 and password in string or buffer format +Handle SecureContext::LoadPKCS12(const Arguments& args) { + HandleScope scope; + + BIO* in = NULL; + PKCS12* p12 = NULL; + EVP_PKEY* pkey = NULL; + X509* cert = NULL; + STACK_OF(X509)* extraCerts = NULL; + char* pass = NULL; + bool ret = false; + + SecureContext *sc = ObjectWrap::Unwrap(args.Holder()); + + if (args.Length() < 1) { + return ThrowException(Exception::TypeError( + String::New("Bad parameter"))); + } + + in = LoadBIO(args[0]); + if (in == NULL) { + return ThrowException(Exception::Error( + String::New("Unable to load BIO"))); + } + + if (args.Length() >= 2) { + ASSERT_IS_STRING_OR_BUFFER(args[1]); + + int passlen = DecodeBytes(args[1], BINARY); + if (passlen < 0) { + BIO_free(in); + return ThrowException(Exception::TypeError( + String::New("Bad password"))); + } + pass = new char[passlen + 1]; + int pass_written = DecodeWrite(pass, passlen, args[1], BINARY); + + assert(pass_written == passlen); + pass[passlen] = '\0'; + } + + if (d2i_PKCS12_bio(in, &p12) && + PKCS12_parse(p12, pass, &pkey, &cert, &extraCerts) && + SSL_CTX_use_certificate(sc->ctx_, cert) && + SSL_CTX_use_PrivateKey(sc->ctx_, pkey)) + { + // set extra certs + while (X509* x509 = sk_X509_pop(extraCerts)) { + if (!sc->ca_store_) { + sc->ca_store_ = X509_STORE_new(); + SSL_CTX_set_cert_store(sc->ctx_, sc->ca_store_); + } + + X509_STORE_add_cert(sc->ca_store_, x509); + SSL_CTX_add_client_CA(sc->ctx_, x509); + } + + EVP_PKEY_free(pkey); + X509_free(cert); + sk_X509_free(extraCerts); + + ret = true; + } + + PKCS12_free(p12); + BIO_free(in); + delete[] pass; + + if (!ret) { + unsigned long err = ERR_get_error(); + const char *str = ERR_reason_error_string(err); + return ThrowException(Exception::Error(String::New(str))); + } + + return True(); +} + #ifdef SSL_PRINT_DEBUG # define DEBUG_PRINT(...) fprintf (stderr, __VA_ARGS__) diff --git a/src/node_crypto.h b/src/node_crypto.h index 87a5340147..6fa3de1df2 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -35,6 +35,7 @@ #include #include #include +#include #ifdef OPENSSL_NPN_NEGOTIATED #include @@ -68,6 +69,7 @@ class SecureContext : ObjectWrap { static v8::Handle SetOptions(const v8::Arguments& args); static v8::Handle SetSessionIdContext(const v8::Arguments& args); static v8::Handle Close(const v8::Arguments& args); + static v8::Handle LoadPKCS12(const v8::Arguments& args); SecureContext() : ObjectWrap() { ctx_ = NULL; diff --git a/test/fixtures/test_cert.pfx b/test/fixtures/test_cert.pfx new file mode 100644 index 0000000000..fd28781a05 Binary files /dev/null and b/test/fixtures/test_cert.pfx differ diff --git a/test/simple/test-crypto.js b/test/simple/test-crypto.js index 6bd1e02ca7..dded025cc9 100644 --- a/test/simple/test-crypto.js +++ b/test/simple/test-crypto.js @@ -38,6 +38,7 @@ var path = require('path'); // Test Certificates var caPem = fs.readFileSync(common.fixturesDir + '/test_ca.pem', 'ascii'); var certPem = fs.readFileSync(common.fixturesDir + '/test_cert.pem', 'ascii'); +var certPfx = fs.readFileSync(common.fixturesDir + '/test_cert.pfx'); var keyPem = fs.readFileSync(common.fixturesDir + '/test_key.pem', 'ascii'); var rsaPubPem = fs.readFileSync(common.fixturesDir + '/test_rsa_pubkey.pem', 'ascii'); @@ -54,8 +55,24 @@ try { process.exit(); } -// Test HMAC +// PFX tests +assert.doesNotThrow(function() { + crypto.createCredentials({pfx:certPfx, passphrase:'sample'}); +}); +assert.throws(function() { + crypto.createCredentials({pfx:certPfx}); +}, 'mac verify failure'); + +assert.throws(function() { + crypto.createCredentials({pfx:certPfx, passphrase:'test'}); +}, 'mac verify failure'); + +assert.throws(function() { + crypto.createCredentials({pfx:'sample', passphrase:'test'}); +}, 'not enough data'); + +// Test HMAC var h1 = crypto.createHmac('sha1', 'Node') .update('some data') .update('to hmac')