tls: implement clientCertEngine option

Add an option 'clientCertEngine' to `tls.createSecureContext()` which gets
wired up to OpenSSL function `SSL_CTX_set_client_cert_engine`. The option
is passed through from `https.request()` as well. This allows using a custom
OpenSSL engine to provide the client certificate.
This commit is contained in:
joelostrowski 2016-04-15 16:49:36 +02:00 committed by Rich Trott
parent f7436ba135
commit 6ee985f311
11 changed files with 305 additions and 15 deletions

View File

@ -653,6 +653,12 @@ Used when `Console` is instantiated without `stdout` stream or when `stdout` or
Used when the native call from `process.cpuUsage` cannot be processed properly.
<a id="ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED"></a>
### ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED
Used when a client certificate engine is requested that is not supported by the
version of OpenSSL being used.
<a id="ERR_CRYPTO_ECDH_INVALID_FORMAT"></a>
### ERR_CRYPTO_ECDH_INVALID_FORMAT

View File

@ -208,6 +208,18 @@ exports.createSecureContext = function createSecureContext(options, context) {
c.context.setFreeListLength(0);
}
if (typeof options.clientCertEngine === 'string') {
if (c.context.setClientCertEngine)
c.context.setClientCertEngine(options.clientCertEngine);
else
throw new errors.Error('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED');
} else if (options.clientCertEngine != null) {
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
'options.clientCertEngine',
['string', 'null', 'undefined'],
options.clientCertEngine);
}
return c;
};

View File

@ -816,6 +816,7 @@ function tlsConnectionListener(rawSocket) {
// - rejectUnauthorized. Boolean, default to true.
// - key. string.
// - cert: string.
// - clientCertEngine: string.
// - ca: string or array of strings.
// - sessionTimeout: integer.
//
@ -859,6 +860,7 @@ function Server(options, listener) {
key: this.key,
passphrase: this.passphrase,
cert: this.cert,
clientCertEngine: this.clientCertEngine,
ca: this.ca,
ciphers: this.ciphers,
ecdhCurve: this.ecdhCurve,
@ -931,6 +933,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;

View File

@ -160,6 +160,10 @@ Agent.prototype.getName = function getName(options) {
if (options.cert)
name += options.cert;
name += ':';
if (options.clientCertEngine)
name += options.clientCertEngine;
name += ':';
if (options.ciphers)
name += options.ciphers;

View File

@ -232,6 +232,8 @@ E('ERR_CHILD_CLOSED_BEFORE_REPLY', 'Child closed before reply received');
E('ERR_CONSOLE_WRITABLE_STREAM',
'Console expects a writable stream instance for %s');
E('ERR_CPU_USAGE', 'Unable to obtain cpu usage %s');
E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED',
'Custom engines not supported by this OpenSSL');
E('ERR_CRYPTO_ECDH_INVALID_FORMAT', 'Invalid ECDH format: %s');
E('ERR_CRYPTO_ENGINE_UNKNOWN', 'Engine "%s" was not found');
E('ERR_CRYPTO_FIPS_FORCED',

View File

@ -354,6 +354,41 @@ static int PasswordCallback(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(const char* engine_id, char (*errmsg)[1024]) {
MarkPopErrorOnReturn mark_pop_error_on_return;
ENGINE* engine = ENGINE_by_id(engine_id);
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) {
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);
}
}
return engine;
}
#endif // !OPENSSL_NO_ENGINE
// This callback is used to avoid the default passphrase callback in OpenSSL
// which will typically prompt for the passphrase. The prompting is designed
@ -498,6 +533,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);
@ -1295,6 +1334,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.
if (sc->client_cert_engine_provided_) {
return env->ThrowError(
"Multiple calls to SetClientCertEngine are not allowed");
}
const node::Utf8Value engine_id(env->isolate(), args[0]);
char errmsg[1024];
ENGINE* engine = LoadEngineById(*engine_id, &errmsg);
if (engine == nullptr) {
return env->ThrowError(errmsg);
}
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());
}
sc->client_cert_engine_provided_ = true;
}
#endif // !OPENSSL_NO_ENGINE
void SecureContext::GetTicketKeys(const FunctionCallbackInfo<Value>& args) {
#if !defined(OPENSSL_NO_TLSEXT) && defined(SSL_CTX_get_tlsext_ticket_keys)
@ -6093,20 +6172,10 @@ void SetEngine(const FunctionCallbackInfo<Value>& args) {
ClearErrorOnReturn clear_error_on_return;
// Load engine.
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;
}
}
}
char errmsg[1024];
ENGINE* engine = LoadEngineById(*engine_id, &errmsg);
if (engine == nullptr) {
int err = ERR_get_error();

View File

@ -93,6 +93,9 @@ class SecureContext : public BaseObject {
SSL_CTX* ctx_;
X509* cert_;
X509* issuer_;
#ifndef OPENSSL_NO_ENGINE
bool client_cert_engine_provided_ = false;
#endif // !OPENSSL_NO_ENGINE
static const int kMaxSessionSize = 10 * 1024;
@ -135,6 +138,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(

View File

@ -0,0 +1,24 @@
{
'targets': [
{
'target_name': 'testengine',
'type': 'none',
'conditions': [
['OS=="mac" and '
'node_use_openssl=="true" and '
'node_shared=="false" and '
'node_shared_openssl=="false"', {
'type': 'shared_library',
'sources': [ 'testengine.cc' ],
'product_extension': 'engine',
'include_dirs': ['../../../deps/openssl/openssl/include'],
'link_settings': {
'libraries': [
'../../../../out/<(PRODUCT_DIR)/<(OPENSSL_PRODUCT)'
]
},
}]
]
}
]
}

View File

@ -0,0 +1,62 @@
'use strict';
const common = require('../../common');
const fixture = require('../../common/fixtures');
if (!common.hasCrypto)
common.skip('missing crypto');
const fs = require('fs');
const path = require('path');
const engine = path.join(__dirname,
`/build/${common.buildType}/testengine.engine`);
if (!fs.existsSync(engine))
common.skip('no client cert engine');
const assert = require('assert');
const https = require('https');
const agentKey = fs.readFileSync(fixture.path('/keys/agent1-key.pem'));
const agentCert = fs.readFileSync(fixture.path('/keys/agent1-cert.pem'));
const agentCa = fs.readFileSync(fixture.path('/keys/ca1-cert.pem'));
const port = common.PORT;
const serverOptions = {
key: agentKey,
cert: agentCert,
ca: agentCa,
requestCert: true,
rejectUnauthorized: true
};
const server = https.createServer(serverOptions, (req, res) => {
res.writeHead(200);
res.end('hello world');
}).listen(port, common.localhostIPv4, () => {
const 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: {}
};
const req = https.request(clientOptions, common.mustCall(function(response) {
let body = '';
response.setEncoding('utf8');
response.on('data', function(chunk) {
body += chunk;
});
response.on('end', common.mustCall(function() {
assert.strictEqual(body, 'hello world');
server.close();
}));
}));
req.end();
});

View File

@ -0,0 +1,100 @@
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/engine.h>
#include <openssl/pem.h>
#include <fstream>
#include <iterator>
#include <string>
#ifndef ENGINE_CMD_BASE
# error did not get engine.h
#endif
#define TEST_ENGINE_ID "testengine"
#define TEST_ENGINE_NAME "dummy test engine"
#define AGENT_KEY "test/fixtures/keys/agent1-key.pem"
#define AGENT_CERT "test/fixtures/keys/agent1-cert.pem"
namespace {
int EngineInit(ENGINE* engine) {
return 1;
}
int EngineFinish(ENGINE* engine) {
return 1;
}
int EngineDestroy(ENGINE* engine) {
return 1;
}
std::string LoadFile(const char* filename) {
std::ifstream file(filename);
return std::string(std::istreambuf_iterator<char>(file),
std::istreambuf_iterator<char>());
}
int EngineLoadSSLClientCert(ENGINE* engine,
SSL* ssl,
STACK_OF(X509_NAME)* ca_dn,
X509** ppcert,
EVP_PKEY** ppkey,
STACK_OF(X509)** pother,
UI_METHOD* ui_method,
void* callback_data) {
if (ppcert != nullptr) {
std::string cert = LoadFile(AGENT_CERT);
if (cert.empty()) {
return 0;
}
BIO* bio = BIO_new_mem_buf(cert.data(), cert.size());
*ppcert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr);
BIO_vfree(bio);
if (*ppcert == nullptr) {
printf("Could not read certificate\n");
return 0;
}
}
if (ppkey != nullptr) {
std::string key = LoadFile(AGENT_KEY);
if (key.empty()) {
return 0;
}
BIO* bio = BIO_new_mem_buf(key.data(), key.size());
*ppkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
BIO_vfree(bio);
if (*ppkey == nullptr) {
printf("Could not read private key\n");
return 0;
}
}
return 1;
}
int bind_fn(ENGINE* engine, const char* id) {
ENGINE_set_id(engine, TEST_ENGINE_ID);
ENGINE_set_name(engine, TEST_ENGINE_NAME);
ENGINE_set_init_function(engine, EngineInit);
ENGINE_set_finish_function(engine, EngineFinish);
ENGINE_set_destroy_function(engine, EngineDestroy);
ENGINE_set_load_ssl_client_cert_function(engine, EngineLoadSSLClientCert);
return 1;
}
extern "C" {
IMPLEMENT_DYNAMIC_CHECK_FN();
IMPLEMENT_DYNAMIC_BIND_FN(bind_fn);
}
} // anonymous namespace

View File

@ -12,7 +12,7 @@ const agent = new https.Agent();
// empty options
assert.strictEqual(
agent.getName({}),
'localhost::::::::::'
'localhost:::::::::::'
);
// pass all options arguments
@ -31,5 +31,5 @@ const options = {
assert.strictEqual(
agent.getName(options),
'0.0.0.0:443:192.168.1.1:ca:cert:ciphers:key:pfx:false:localhost:'
'0.0.0.0:443:192.168.1.1:ca:cert::ciphers:key:pfx:false:localhost:'
);