Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Openssl clientcertengine support2 #6569

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/api/https.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ The following options from [`tls.connect()`][] can also be specified. However, a
- `key`: Private key to use for SSL. Default `null`.
- `passphrase`: A string of passphrase for the private key or pfx. Default `null`.
- `cert`: Public x509 certificate to use. Default `null`.
- `clientCertEngine`: A string containing the name of an OpenSSL engine for
providing the client certificate.
- `ca`: A string, [`Buffer`][] or array of strings or [`Buffer`][]s of trusted
certificates in PEM format. If this is omitted several well known "root"
CAs will be used, like VeriSign. These are used to authorize connections.
Expand Down
4 changes: 4 additions & 0 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,8 @@ added: v0.11.13
* `passphrase` {string} A string containing the passphrase for the private key
or pfx.
* `cert` {string} A string containing the PEM encoded certificate
* `clientCertEngine` {string} A string with the name of an OpenSSL engine
which can provide the client certificate.
* `ca`{string|string[]|Buffer|Buffer[]} A string, `Buffer`, array of strings,
or array of `Buffer`s of trusted certificates in PEM format. If omitted,
several well known "root" CAs (like VeriSign) will be used. These are used
Expand Down Expand Up @@ -971,6 +973,8 @@ added: v0.3.2
or array of `Buffer`s of trusted certificates in PEM format. If this is
omitted several well known "root" CAs (like VeriSign) will be used. These
are used to authorize connections.
* `clientCertEngine` {string} A string with the name of an OpenSSL engine
which can provide the client certificate.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency and greppability, I'd use the same description here; it's slightly different from the other two.

* `crl` {string|string[]} Either a string or array of strings of PEM encoded
CRLs (Certificate Revocation List).
* `ciphers` {string} A string describing the ciphers to use or exclude,
Expand Down
7 changes: 7 additions & 0 deletions lib/_tls_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ exports.createSecureContext = function createSecureContext(options, context) {
c.context.setFreeListLength(0);
}

if (options.clientCertEngine) {
if (c.context.setClientCertEngine)
c.context.setClientCertEngine(options.clientCertEngine);
else
throw new Error('Custom engines not supported by this OpenSSL');
}

return c;
};

Expand Down
4 changes: 4 additions & 0 deletions lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,7 @@ TLSSocket.prototype.getProtocol = function() {
// - rejectUnauthorized. Boolean, default to true.
// - key. string.
// - cert: string.
// - clientCertEngine: string.
// - ca: string or array of strings.
// - sessionTimeout: integer.
//
Expand Down Expand Up @@ -751,6 +752,7 @@ function Server(/* [options], listener */) {
key: self.key,
passphrase: self.passphrase,
cert: self.cert,
clientCertEngine: self.clientCertEngine,
ca: self.ca,
ciphers: self.ciphers,
ecdhCurve: self.ecdhCurve,
Expand Down Expand Up @@ -882,6 +884,8 @@ Server.prototype.setOptions = function(options) {
if (options.key) this.key = options.key;
if (options.passphrase) this.passphrase = options.passphrase;
if (options.cert) this.cert = options.cert;
if (options.clientCertEngine)
this.clientCertEngine = options.clientCertEngine;
if (options.ca) this.ca = options.ca;
if (options.secureProtocol) this.secureProtocol = options.secureProtocol;
if (options.crl) this.crl = options.crl;
Expand Down
4 changes: 4 additions & 0 deletions lib/https.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ Agent.prototype.getName = function(options) {
if (options.cert)
name += options.cert;

name += ':';
if (options.clientCertEngine)
name += options.clientCertEngine;

name += ':';
if (options.ciphers)
name += options.ciphers;
Expand Down
107 changes: 85 additions & 22 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,43 @@ static int CryptoPemCallback(char *buf, int size, int rwflag, void *u) {
return 0;
}

// Loads OpenSSL engine by engine id and returns it. The loaded engine
// gets a reference so remember the corresponding call to ENGINE_free.
// In case of error the appropriate js exception is scheduled
// and nullptr is returned.
#ifndef OPENSSL_NO_ENGINE
static ENGINE* LoadEngineById(Environment* env, const char* engine_id) {
ENGINE* engine = ENGINE_by_id(engine_id);

MarkPopErrorOnReturn mark_pop_error_on_return;

if (engine == nullptr) {
// Engine not found, try loading dynamically.
engine = ENGINE_by_id("dynamic");
if (engine != nullptr) {
if (!ENGINE_ctrl_cmd_string(engine, "SO_PATH", engine_id, 0) ||
!ENGINE_ctrl_cmd_string(engine, "LOAD", nullptr, 0)) {
ENGINE_free(engine);
engine = nullptr;
}
}
}

if (engine == nullptr) {
char errmsg[1024];
int err = ERR_get_error();
if (err != 0) {
ERR_error_string_n(err, errmsg, sizeof(errmsg));
} else {
snprintf(errmsg, sizeof(errmsg),
"Engine \"%s\" was not found", engine_id);
}
env->ThrowError(errmsg);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think moving the ThrowError out of this function is aesthetically nicer but I'll let it rest.

}

return engine;
}
#endif // !OPENSSL_NO_ENGINE

void ThrowCryptoError(Environment* env,
unsigned long err, // NOLINT(runtime/int)
Expand Down Expand Up @@ -289,6 +326,10 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
SecureContext::SetSessionTimeout);
env->SetProtoMethod(t, "close", SecureContext::Close);
env->SetProtoMethod(t, "loadPKCS12", SecureContext::LoadPKCS12);
#ifndef OPENSSL_NO_ENGINE
env->SetProtoMethod(t, "setClientCertEngine",
SecureContext::SetClientCertEngine);
#endif // !OPENSSL_NO_ENGINE
env->SetProtoMethod(t, "getTicketKeys", SecureContext::GetTicketKeys);
env->SetProtoMethod(t, "setTicketKeys", SecureContext::SetTicketKeys);
env->SetProtoMethod(t, "setFreeListLength", SecureContext::SetFreeListLength);
Expand Down Expand Up @@ -1024,6 +1065,46 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
}


#ifndef OPENSSL_NO_ENGINE
void SecureContext::SetClientCertEngine(
const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsString());

SecureContext* sc = Unwrap<SecureContext>(args.This());

MarkPopErrorOnReturn mark_pop_error_on_return;

// SSL_CTX_set_client_cert_engine does not itself 'support' multiple
// calls by cleaning up before overwriting the client_cert_engine
// internal context variable.
// Instead of trying to fix up this problem we in turn also do not
// support multiple calls to SetClientCertEngine. As this may surprise
// a developer we throw him an exception.
if (sc->ctx_->client_cert_engine != nullptr) {
return env->ThrowError(
"Multiple calls to SetClientCertEngine are not allowed");
}

const node::Utf8Value engine_id(env->isolate(), args[0]);
ENGINE* engine = LoadEngineById(env, *engine_id);

if (engine == nullptr) {
// Load failed... appropriate js exception has been scheduled.
return;
}

int r = SSL_CTX_set_client_cert_engine(sc->ctx_, engine);
// Free reference (SSL_CTX_set_client_cert_engine took it via ENGINE_init).
ENGINE_free(engine);
if (r == 0) {
return ThrowCryptoError(env, ERR_get_error());
}
}
#endif // !OPENSSL_NO_ENGINE


void SecureContext::GetTicketKeys(const FunctionCallbackInfo<Value>& args) {
#if !defined(OPENSSL_NO_TLSEXT) && defined(SSL_CTX_get_tlsext_ticket_keys)

Expand Down Expand Up @@ -5815,32 +5896,14 @@ void SetEngine(const FunctionCallbackInfo<Value>& args) {
unsigned int flags = args[1]->Uint32Value();

ClearErrorOnReturn clear_error_on_return;
(void) &clear_error_on_return; // Silence compiler warning.

// Load engine
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: please punctuate comments. I'll stop pointing that out from now on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still unfixed. It's a bit on the superfluous side as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still unfixed. :-)

const node::Utf8Value engine_id(env->isolate(), args[0]);
ENGINE* engine = ENGINE_by_id(*engine_id);

// Engine not found, try loading dynamically
if (engine == nullptr) {
engine = ENGINE_by_id("dynamic");
if (engine != nullptr) {
if (!ENGINE_ctrl_cmd_string(engine, "SO_PATH", *engine_id, 0) ||
!ENGINE_ctrl_cmd_string(engine, "LOAD", nullptr, 0)) {
ENGINE_free(engine);
engine = nullptr;
}
}
}
ENGINE* engine = LoadEngineById(env, *engine_id);

if (engine == nullptr) {
int err = ERR_get_error();
if (err == 0) {
char tmp[1024];
snprintf(tmp, sizeof(tmp), "Engine \"%s\" was not found", *engine_id);
return env->ThrowError(tmp);
} else {
return ThrowCryptoError(env, err);
}
// Load failed... appropriate js exception has been scheduled
return;
}

int r = ENGINE_set_default(engine, flags);
Expand Down
4 changes: 4 additions & 0 deletions src/node_crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ class SecureContext : public BaseObject {
const v8::FunctionCallbackInfo<v8::Value>& args);
static void Close(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoadPKCS12(const v8::FunctionCallbackInfo<v8::Value>& args);
#ifndef OPENSSL_NO_ENGINE
static void SetClientCertEngine(
const v8::FunctionCallbackInfo<v8::Value>& args);
#endif // !OPENSSL_NO_ENGINE
static void GetTicketKeys(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetTicketKeys(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetFreeListLength(
Expand Down
11 changes: 11 additions & 0 deletions test/addons/openssl-client-cert-engine/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
'targets': [
{
'target_name': 'testengine',
'type': 'shared_library',
'product_extension': 'engine',
'sources': [ 'testengine.cc' ],
'include_dirs': [ '../../../deps/openssl/openssl/include' ]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this perhaps cater support for shared openssl permutations?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for the purposes of testing it should be good to do it right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bnoordhuis did the windows build get fixed?

Copy link
Member

@bnoordhuis bnoordhuis May 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbergstroem is referring to ./configure --shared-openssl builds. I'm fine with leaving it like this, we can address it if/when we add testing for that to the CI matrix.

Apropos the Windows build, no, I'm still working on that.

EDIT 20160819: For posterity, support for linking against the bundled openssl on windows landed a few weeks ago.

}
]
}
55 changes: 55 additions & 0 deletions test/addons/openssl-client-cert-engine/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const https = require('https');
const fs = require('fs');
const path = require('path');

const engine = path.join(__dirname, '/build/Release/testengine.engine');

const agentKey = fs.readFileSync(path.join(common.fixturesDir,
'/keys/agent1-key.pem'));
const agentCert = fs.readFileSync(path.join(common.fixturesDir,
'/keys/agent1-cert.pem'));
const agentCa = fs.readFileSync(path.join(common.fixturesDir,
'/keys/ca1-cert.pem'));

const port = common.PORT;

const serverOptions = {
key: agentKey,
cert: agentCert,
ca: agentCa,
requestCert: true,
rejectUnauthorized: true
};

var server = https.createServer(serverOptions, (req, res) => {
res.writeHead(200);
res.end('hello world');
}).listen(port, common.localhostIPv4, () => {
var clientOptions = {
method: 'GET',
host: common.localhostIPv4,
port: port,
path: '/test',
clientCertEngine: engine, // engine will provide key+cert
rejectUnauthorized: false, // prevent failing on self-signed certificates
headers: {}
};

var req = https.request(clientOptions, common.mustCall(function(response) {
var body = '';
response.setEncoding('utf8');
response.on('data', common.mustCall(function(chunk) {
body += chunk;
}));

response.on('end', common.mustCall(function() {
assert.strictEqual(body, 'hello world');
server.close();
}));
}));

req.end();
});
Loading