tls: add option for private keys for OpenSSL engines
Add `privateKeyIdentifier` and `privateKeyEngine` options to get private key from an OpenSSL engine in tls.createSecureContext(). PR-URL: https://github.com/nodejs/node/pull/28973 Reviewed-By: Rod Vagg <rod@vagg.org> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
This commit is contained in:
parent
3de5eae6db
commit
c2ce8d0547
@ -1358,6 +1358,10 @@ argument.
|
|||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v0.11.13
|
added: v0.11.13
|
||||||
changes:
|
changes:
|
||||||
|
- version: REPLACEME
|
||||||
|
pr-url: https://github.com/nodejs/node/pull/28973
|
||||||
|
description: Added `privateKeyIdentifier` and `privateKeyEngine` options
|
||||||
|
to get private key from an OpenSSL engine.
|
||||||
- version: v12.11.0
|
- version: v12.11.0
|
||||||
pr-url: https://github.com/nodejs/node/pull/29598
|
pr-url: https://github.com/nodejs/node/pull/29598
|
||||||
description: Added `sigalgs` option to override supported signature
|
description: Added `sigalgs` option to override supported signature
|
||||||
@ -1462,6 +1466,12 @@ changes:
|
|||||||
occur in an array. `object.passphrase` is optional. Encrypted keys will be
|
occur in an array. `object.passphrase` is optional. Encrypted keys will be
|
||||||
decrypted with `object.passphrase` if provided, or `options.passphrase` if
|
decrypted with `object.passphrase` if provided, or `options.passphrase` if
|
||||||
it is not.
|
it is not.
|
||||||
|
* `privateKeyEngine` {string} Name of an OpenSSL engine to get private key
|
||||||
|
from. Should be used together with `privateKeyIdentifier`.
|
||||||
|
* `privateKeyIdentifier` {string} Identifier of a private key managed by
|
||||||
|
an OpenSSL engine. Should be used together with `privateKeyEngine`.
|
||||||
|
Should not be set together with `key`, because both options define a
|
||||||
|
private key in different ways.
|
||||||
* `maxVersion` {string} Optionally set the maximum TLS version to allow. One
|
* `maxVersion` {string} Optionally set the maximum TLS version to allow. One
|
||||||
of `TLSv1.3`, `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified
|
of `TLSv1.3`, `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified
|
||||||
along with the `secureProtocol` option, use one or the other.
|
along with the `secureProtocol` option, use one or the other.
|
||||||
|
@ -166,6 +166,36 @@ exports.createSecureContext = function createSecureContext(options) {
|
|||||||
c.context.setSigalgs(sigalgs);
|
c.context.setSigalgs(sigalgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { privateKeyIdentifier, privateKeyEngine } = options;
|
||||||
|
if (privateKeyIdentifier !== undefined) {
|
||||||
|
if (privateKeyEngine === undefined) {
|
||||||
|
// Engine is required when privateKeyIdentifier is present
|
||||||
|
throw new ERR_INVALID_OPT_VALUE('privateKeyEngine',
|
||||||
|
privateKeyEngine);
|
||||||
|
}
|
||||||
|
if (key) {
|
||||||
|
// Both data key and engine key can't be set at the same time
|
||||||
|
throw new ERR_INVALID_OPT_VALUE('privateKeyIdentifier',
|
||||||
|
privateKeyIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof privateKeyIdentifier === 'string' &&
|
||||||
|
typeof privateKeyEngine === 'string') {
|
||||||
|
if (c.context.setEngineKey)
|
||||||
|
c.context.setEngineKey(privateKeyIdentifier, privateKeyEngine);
|
||||||
|
else
|
||||||
|
throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED();
|
||||||
|
} else if (typeof privateKeyIdentifier !== 'string') {
|
||||||
|
throw new ERR_INVALID_ARG_TYPE('options.privateKeyIdentifier',
|
||||||
|
['string', 'undefined'],
|
||||||
|
privateKeyIdentifier);
|
||||||
|
} else {
|
||||||
|
throw new ERR_INVALID_ARG_TYPE('options.privateKeyEngine',
|
||||||
|
['string', 'undefined'],
|
||||||
|
privateKeyEngine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options.ciphers && typeof options.ciphers !== 'string') {
|
if (options.ciphers && typeof options.ciphers !== 'string') {
|
||||||
throw new ERR_INVALID_ARG_TYPE(
|
throw new ERR_INVALID_ARG_TYPE(
|
||||||
'options.ciphers', 'string', options.ciphers);
|
'options.ciphers', 'string', options.ciphers);
|
||||||
|
@ -471,6 +471,9 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
|
|||||||
|
|
||||||
env->SetProtoMethod(t, "init", Init);
|
env->SetProtoMethod(t, "init", Init);
|
||||||
env->SetProtoMethod(t, "setKey", SetKey);
|
env->SetProtoMethod(t, "setKey", SetKey);
|
||||||
|
#ifndef OPENSSL_NO_ENGINE
|
||||||
|
env->SetProtoMethod(t, "setEngineKey", SetEngineKey);
|
||||||
|
#endif // !OPENSSL_NO_ENGINE
|
||||||
env->SetProtoMethod(t, "setCert", SetCert);
|
env->SetProtoMethod(t, "setCert", SetCert);
|
||||||
env->SetProtoMethod(t, "addCACert", AddCACert);
|
env->SetProtoMethod(t, "addCACert", AddCACert);
|
||||||
env->SetProtoMethod(t, "addCRL", AddCRL);
|
env->SetProtoMethod(t, "addCRL", AddCRL);
|
||||||
@ -764,6 +767,56 @@ void SecureContext::SetSigalgs(const FunctionCallbackInfo<Value>& args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef OPENSSL_NO_ENGINE
|
||||||
|
// Helpers for the smart pointer.
|
||||||
|
void ENGINE_free_fn(ENGINE* engine) { ENGINE_free(engine); }
|
||||||
|
|
||||||
|
void ENGINE_finish_and_free_fn(ENGINE* engine) {
|
||||||
|
ENGINE_finish(engine);
|
||||||
|
ENGINE_free(engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SecureContext::SetEngineKey(const FunctionCallbackInfo<Value>& args) {
|
||||||
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
|
||||||
|
SecureContext* sc;
|
||||||
|
ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
|
||||||
|
|
||||||
|
CHECK_EQ(args.Length(), 2);
|
||||||
|
|
||||||
|
char errmsg[1024];
|
||||||
|
const node::Utf8Value engine_id(env->isolate(), args[1]);
|
||||||
|
std::unique_ptr<ENGINE, std::function<void(ENGINE*)>> e =
|
||||||
|
{ LoadEngineById(*engine_id, &errmsg),
|
||||||
|
ENGINE_free_fn };
|
||||||
|
if (e.get() == nullptr) {
|
||||||
|
return env->ThrowError(errmsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ENGINE_init(e.get())) {
|
||||||
|
return env->ThrowError("ENGINE_init");
|
||||||
|
}
|
||||||
|
|
||||||
|
e.get_deleter() = ENGINE_finish_and_free_fn;
|
||||||
|
|
||||||
|
const node::Utf8Value key_name(env->isolate(), args[0]);
|
||||||
|
EVPKeyPointer key(ENGINE_load_private_key(e.get(), *key_name,
|
||||||
|
nullptr, nullptr));
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return ThrowCryptoError(env, ERR_get_error(), "ENGINE_load_private_key");
|
||||||
|
}
|
||||||
|
|
||||||
|
int rv = SSL_CTX_use_PrivateKey(sc->ctx_.get(), key.get());
|
||||||
|
|
||||||
|
if (rv == 0) {
|
||||||
|
return ThrowCryptoError(env, ERR_get_error(), "SSL_CTX_use_PrivateKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
sc->private_key_engine_ = std::move(e);
|
||||||
|
}
|
||||||
|
#endif // !OPENSSL_NO_ENGINE
|
||||||
|
|
||||||
int SSL_CTX_get_issuer(SSL_CTX* ctx, X509* cert, X509** issuer) {
|
int SSL_CTX_get_issuer(SSL_CTX* ctx, X509* cert, X509** issuer) {
|
||||||
X509_STORE* store = SSL_CTX_get_cert_store(ctx);
|
X509_STORE* store = SSL_CTX_get_cert_store(ctx);
|
||||||
DeleteFnPtr<X509_STORE_CTX, X509_STORE_CTX_free> store_ctx(
|
DeleteFnPtr<X509_STORE_CTX, X509_STORE_CTX_free> store_ctx(
|
||||||
@ -1438,9 +1491,6 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
|
|||||||
|
|
||||||
|
|
||||||
#ifndef OPENSSL_NO_ENGINE
|
#ifndef OPENSSL_NO_ENGINE
|
||||||
// Helper for the smart pointer.
|
|
||||||
void ENGINE_free_fn(ENGINE* engine) { ENGINE_free(engine); }
|
|
||||||
|
|
||||||
void SecureContext::SetClientCertEngine(
|
void SecureContext::SetClientCertEngine(
|
||||||
const FunctionCallbackInfo<Value>& args) {
|
const FunctionCallbackInfo<Value>& args) {
|
||||||
Environment* env = Environment::GetCurrent(args);
|
Environment* env = Environment::GetCurrent(args);
|
||||||
|
@ -97,6 +97,7 @@ class SecureContext : public BaseObject {
|
|||||||
X509Pointer issuer_;
|
X509Pointer issuer_;
|
||||||
#ifndef OPENSSL_NO_ENGINE
|
#ifndef OPENSSL_NO_ENGINE
|
||||||
bool client_cert_engine_provided_ = false;
|
bool client_cert_engine_provided_ = false;
|
||||||
|
std::unique_ptr<ENGINE, std::function<void(ENGINE*)>> private_key_engine_;
|
||||||
#endif // !OPENSSL_NO_ENGINE
|
#endif // !OPENSSL_NO_ENGINE
|
||||||
|
|
||||||
static const int kMaxSessionSize = 10 * 1024;
|
static const int kMaxSessionSize = 10 * 1024;
|
||||||
@ -119,6 +120,9 @@ class SecureContext : public BaseObject {
|
|||||||
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);
|
static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
static void SetKey(const v8::FunctionCallbackInfo<v8::Value>& args);
|
static void SetKey(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
|
#ifndef OPENSSL_NO_ENGINE
|
||||||
|
static void SetEngineKey(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
|
#endif // !OPENSSL_NO_ENGINE
|
||||||
static void SetCert(const v8::FunctionCallbackInfo<v8::Value>& args);
|
static void SetCert(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
static void AddCACert(const v8::FunctionCallbackInfo<v8::Value>& args);
|
static void AddCACert(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
static void AddCRL(const v8::FunctionCallbackInfo<v8::Value>& args);
|
static void AddCRL(const v8::FunctionCallbackInfo<v8::Value>& args);
|
||||||
|
25
test/addons/openssl-key-engine/binding.gyp
Normal file
25
test/addons/openssl-key-engine/binding.gyp
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
'targets': [
|
||||||
|
{
|
||||||
|
'target_name': 'testkeyengine',
|
||||||
|
'type': 'none',
|
||||||
|
'includes': ['../common.gypi'],
|
||||||
|
'conditions': [
|
||||||
|
['OS=="mac" and '
|
||||||
|
'node_use_openssl=="true" and '
|
||||||
|
'node_shared=="false" and '
|
||||||
|
'node_shared_openssl=="false"', {
|
||||||
|
'type': 'shared_library',
|
||||||
|
'sources': [ 'testkeyengine.cc' ],
|
||||||
|
'product_extension': 'engine',
|
||||||
|
'include_dirs': ['../../../deps/openssl/openssl/include'],
|
||||||
|
'link_settings': {
|
||||||
|
'libraries': [
|
||||||
|
'../../../../out/<(PRODUCT_DIR)/<(openssl_product)'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
62
test/addons/openssl-key-engine/test.js
Normal file
62
test/addons/openssl-key-engine/test.js
Normal 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}/testkeyengine.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 serverOptions = {
|
||||||
|
key: agentKey,
|
||||||
|
cert: agentCert,
|
||||||
|
ca: agentCa,
|
||||||
|
requestCert: true,
|
||||||
|
rejectUnauthorized: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = https.createServer(serverOptions, common.mustCall((req, res) => {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end('hello world');
|
||||||
|
})).listen(0, common.localhostIPv4, common.mustCall(() => {
|
||||||
|
const clientOptions = {
|
||||||
|
method: 'GET',
|
||||||
|
host: common.localhostIPv4,
|
||||||
|
port: server.address().port,
|
||||||
|
path: '/test',
|
||||||
|
privateKeyEngine: engine,
|
||||||
|
privateKeyIdentifier: 'dummykey',
|
||||||
|
cert: agentCert,
|
||||||
|
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();
|
||||||
|
}));
|
73
test/addons/openssl-key-engine/testkeyengine.cc
Normal file
73
test/addons/openssl-key-engine/testkeyengine.cc
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#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 "testkeyengine"
|
||||||
|
#define TEST_ENGINE_NAME "dummy test key engine"
|
||||||
|
|
||||||
|
#define PRIVATE_KEY "test/fixtures/keys/agent1-key.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>());
|
||||||
|
}
|
||||||
|
|
||||||
|
static EVP_PKEY* EngineLoadPrivkey(ENGINE* engine, const char* name,
|
||||||
|
UI_METHOD* ui_method, void* callback_data) {
|
||||||
|
if (strcmp(name, "dummykey") == 0) {
|
||||||
|
std::string key = LoadFile(PRIVATE_KEY);
|
||||||
|
BIO* bio = BIO_new_mem_buf(key.data(), key.size());
|
||||||
|
EVP_PKEY* ret = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
|
||||||
|
|
||||||
|
BIO_vfree(bio);
|
||||||
|
if (ret != nullptr) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_privkey_function(engine, EngineLoadPrivkey);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
IMPLEMENT_DYNAMIC_CHECK_FN();
|
||||||
|
IMPLEMENT_DYNAMIC_BIND_FN(bind_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // anonymous namespace
|
23
test/parallel/test-tls-keyengine-invalid-arg-type.js
Normal file
23
test/parallel/test-tls-keyengine-invalid-arg-type.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
|
||||||
|
const tls = require('tls');
|
||||||
|
|
||||||
|
common.expectsError(
|
||||||
|
() => {
|
||||||
|
tls.createSecureContext({ privateKeyEngine: 0,
|
||||||
|
privateKeyIdentifier: 'key' });
|
||||||
|
},
|
||||||
|
{ code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
message: / Received type number$/ });
|
||||||
|
|
||||||
|
common.expectsError(
|
||||||
|
() => {
|
||||||
|
tls.createSecureContext({ privateKeyEngine: 'engine',
|
||||||
|
privateKeyIdentifier: 0 });
|
||||||
|
},
|
||||||
|
{ code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
message: / Received type number$/ });
|
34
test/parallel/test-tls-keyengine-unsupported.js
Normal file
34
test/parallel/test-tls-keyengine-unsupported.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Flags: --expose-internals
|
||||||
|
'use strict';
|
||||||
|
const common = require('../common');
|
||||||
|
|
||||||
|
if (!common.hasCrypto)
|
||||||
|
common.skip('missing crypto');
|
||||||
|
|
||||||
|
// Monkey-patch SecureContext
|
||||||
|
const { internalBinding } = require('internal/test/binding');
|
||||||
|
const binding = internalBinding('crypto');
|
||||||
|
const NativeSecureContext = binding.SecureContext;
|
||||||
|
|
||||||
|
binding.SecureContext = function() {
|
||||||
|
const rv = new NativeSecureContext();
|
||||||
|
rv.setEngineKey = undefined;
|
||||||
|
return rv;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tls = require('tls');
|
||||||
|
|
||||||
|
{
|
||||||
|
common.expectsError(
|
||||||
|
() => {
|
||||||
|
tls.createSecureContext({
|
||||||
|
privateKeyEngine: 'engine',
|
||||||
|
privateKeyIdentifier: 'key'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED',
|
||||||
|
message: 'Custom engines not supported by this OpenSSL'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user