Skip to content

Commit c2ce8d0

Browse files
OYTISTrott
authored andcommitted
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: #28973 Reviewed-By: Rod Vagg <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Sam Roberts <[email protected]>
1 parent 3de5eae commit c2ce8d0

9 files changed

+314
-3
lines changed

doc/api/tls.md

+10
Original file line numberDiff line numberDiff line change
@@ -1358,6 +1358,10 @@ argument.
13581358
<!-- YAML
13591359
added: v0.11.13
13601360
changes:
1361+
- version: REPLACEME
1362+
pr-url: https://github.com/nodejs/node/pull/28973
1363+
description: Added `privateKeyIdentifier` and `privateKeyEngine` options
1364+
to get private key from an OpenSSL engine.
13611365
- version: v12.11.0
13621366
pr-url: https://github.com/nodejs/node/pull/29598
13631367
description: Added `sigalgs` option to override supported signature
@@ -1462,6 +1466,12 @@ changes:
14621466
occur in an array. `object.passphrase` is optional. Encrypted keys will be
14631467
decrypted with `object.passphrase` if provided, or `options.passphrase` if
14641468
it is not.
1469+
* `privateKeyEngine` {string} Name of an OpenSSL engine to get private key
1470+
from. Should be used together with `privateKeyIdentifier`.
1471+
* `privateKeyIdentifier` {string} Identifier of a private key managed by
1472+
an OpenSSL engine. Should be used together with `privateKeyEngine`.
1473+
Should not be set together with `key`, because both options define a
1474+
private key in different ways.
14651475
* `maxVersion` {string} Optionally set the maximum TLS version to allow. One
14661476
of `TLSv1.3`, `TLSv1.2'`, `'TLSv1.1'`, or `'TLSv1'`. Cannot be specified
14671477
along with the `secureProtocol` option, use one or the other.

lib/_tls_common.js

+30
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,36 @@ exports.createSecureContext = function createSecureContext(options) {
166166
c.context.setSigalgs(sigalgs);
167167
}
168168

169+
const { privateKeyIdentifier, privateKeyEngine } = options;
170+
if (privateKeyIdentifier !== undefined) {
171+
if (privateKeyEngine === undefined) {
172+
// Engine is required when privateKeyIdentifier is present
173+
throw new ERR_INVALID_OPT_VALUE('privateKeyEngine',
174+
privateKeyEngine);
175+
}
176+
if (key) {
177+
// Both data key and engine key can't be set at the same time
178+
throw new ERR_INVALID_OPT_VALUE('privateKeyIdentifier',
179+
privateKeyIdentifier);
180+
}
181+
182+
if (typeof privateKeyIdentifier === 'string' &&
183+
typeof privateKeyEngine === 'string') {
184+
if (c.context.setEngineKey)
185+
c.context.setEngineKey(privateKeyIdentifier, privateKeyEngine);
186+
else
187+
throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED();
188+
} else if (typeof privateKeyIdentifier !== 'string') {
189+
throw new ERR_INVALID_ARG_TYPE('options.privateKeyIdentifier',
190+
['string', 'undefined'],
191+
privateKeyIdentifier);
192+
} else {
193+
throw new ERR_INVALID_ARG_TYPE('options.privateKeyEngine',
194+
['string', 'undefined'],
195+
privateKeyEngine);
196+
}
197+
}
198+
169199
if (options.ciphers && typeof options.ciphers !== 'string') {
170200
throw new ERR_INVALID_ARG_TYPE(
171201
'options.ciphers', 'string', options.ciphers);

src/node_crypto.cc

+53-3
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,9 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
471471

472472
env->SetProtoMethod(t, "init", Init);
473473
env->SetProtoMethod(t, "setKey", SetKey);
474+
#ifndef OPENSSL_NO_ENGINE
475+
env->SetProtoMethod(t, "setEngineKey", SetEngineKey);
476+
#endif // !OPENSSL_NO_ENGINE
474477
env->SetProtoMethod(t, "setCert", SetCert);
475478
env->SetProtoMethod(t, "addCACert", AddCACert);
476479
env->SetProtoMethod(t, "addCRL", AddCRL);
@@ -764,6 +767,56 @@ void SecureContext::SetSigalgs(const FunctionCallbackInfo<Value>& args) {
764767
}
765768
}
766769

770+
#ifndef OPENSSL_NO_ENGINE
771+
// Helpers for the smart pointer.
772+
void ENGINE_free_fn(ENGINE* engine) { ENGINE_free(engine); }
773+
774+
void ENGINE_finish_and_free_fn(ENGINE* engine) {
775+
ENGINE_finish(engine);
776+
ENGINE_free(engine);
777+
}
778+
779+
void SecureContext::SetEngineKey(const FunctionCallbackInfo<Value>& args) {
780+
Environment* env = Environment::GetCurrent(args);
781+
782+
SecureContext* sc;
783+
ASSIGN_OR_RETURN_UNWRAP(&sc, args.Holder());
784+
785+
CHECK_EQ(args.Length(), 2);
786+
787+
char errmsg[1024];
788+
const node::Utf8Value engine_id(env->isolate(), args[1]);
789+
std::unique_ptr<ENGINE, std::function<void(ENGINE*)>> e =
790+
{ LoadEngineById(*engine_id, &errmsg),
791+
ENGINE_free_fn };
792+
if (e.get() == nullptr) {
793+
return env->ThrowError(errmsg);
794+
}
795+
796+
if (!ENGINE_init(e.get())) {
797+
return env->ThrowError("ENGINE_init");
798+
}
799+
800+
e.get_deleter() = ENGINE_finish_and_free_fn;
801+
802+
const node::Utf8Value key_name(env->isolate(), args[0]);
803+
EVPKeyPointer key(ENGINE_load_private_key(e.get(), *key_name,
804+
nullptr, nullptr));
805+
806+
if (!key) {
807+
return ThrowCryptoError(env, ERR_get_error(), "ENGINE_load_private_key");
808+
}
809+
810+
int rv = SSL_CTX_use_PrivateKey(sc->ctx_.get(), key.get());
811+
812+
if (rv == 0) {
813+
return ThrowCryptoError(env, ERR_get_error(), "SSL_CTX_use_PrivateKey");
814+
}
815+
816+
sc->private_key_engine_ = std::move(e);
817+
}
818+
#endif // !OPENSSL_NO_ENGINE
819+
767820
int SSL_CTX_get_issuer(SSL_CTX* ctx, X509* cert, X509** issuer) {
768821
X509_STORE* store = SSL_CTX_get_cert_store(ctx);
769822
DeleteFnPtr<X509_STORE_CTX, X509_STORE_CTX_free> store_ctx(
@@ -1438,9 +1491,6 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
14381491

14391492

14401493
#ifndef OPENSSL_NO_ENGINE
1441-
// Helper for the smart pointer.
1442-
void ENGINE_free_fn(ENGINE* engine) { ENGINE_free(engine); }
1443-
14441494
void SecureContext::SetClientCertEngine(
14451495
const FunctionCallbackInfo<Value>& args) {
14461496
Environment* env = Environment::GetCurrent(args);

src/node_crypto.h

+4
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class SecureContext : public BaseObject {
9797
X509Pointer issuer_;
9898
#ifndef OPENSSL_NO_ENGINE
9999
bool client_cert_engine_provided_ = false;
100+
std::unique_ptr<ENGINE, std::function<void(ENGINE*)>> private_key_engine_;
100101
#endif // !OPENSSL_NO_ENGINE
101102

102103
static const int kMaxSessionSize = 10 * 1024;
@@ -119,6 +120,9 @@ class SecureContext : public BaseObject {
119120
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
120121
static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);
121122
static void SetKey(const v8::FunctionCallbackInfo<v8::Value>& args);
123+
#ifndef OPENSSL_NO_ENGINE
124+
static void SetEngineKey(const v8::FunctionCallbackInfo<v8::Value>& args);
125+
#endif // !OPENSSL_NO_ENGINE
122126
static void SetCert(const v8::FunctionCallbackInfo<v8::Value>& args);
123127
static void AddCACert(const v8::FunctionCallbackInfo<v8::Value>& args);
124128
static void AddCRL(const v8::FunctionCallbackInfo<v8::Value>& args);
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'testkeyengine',
5+
'type': 'none',
6+
'includes': ['../common.gypi'],
7+
'conditions': [
8+
['OS=="mac" and '
9+
'node_use_openssl=="true" and '
10+
'node_shared=="false" and '
11+
'node_shared_openssl=="false"', {
12+
'type': 'shared_library',
13+
'sources': [ 'testkeyengine.cc' ],
14+
'product_extension': 'engine',
15+
'include_dirs': ['../../../deps/openssl/openssl/include'],
16+
'link_settings': {
17+
'libraries': [
18+
'../../../../out/<(PRODUCT_DIR)/<(openssl_product)'
19+
]
20+
},
21+
}],
22+
]
23+
}
24+
]
25+
}
+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
const common = require('../../common');
3+
const fixture = require('../../common/fixtures');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const fs = require('fs');
9+
const path = require('path');
10+
11+
const engine = path.join(__dirname,
12+
`/build/${common.buildType}/testkeyengine.engine`);
13+
14+
if (!fs.existsSync(engine))
15+
common.skip('no client cert engine');
16+
17+
const assert = require('assert');
18+
const https = require('https');
19+
20+
const agentKey = fs.readFileSync(fixture.path('/keys/agent1-key.pem'));
21+
const agentCert = fs.readFileSync(fixture.path('/keys/agent1-cert.pem'));
22+
const agentCa = fs.readFileSync(fixture.path('/keys/ca1-cert.pem'));
23+
24+
const serverOptions = {
25+
key: agentKey,
26+
cert: agentCert,
27+
ca: agentCa,
28+
requestCert: true,
29+
rejectUnauthorized: true
30+
};
31+
32+
const server = https.createServer(serverOptions, common.mustCall((req, res) => {
33+
res.writeHead(200);
34+
res.end('hello world');
35+
})).listen(0, common.localhostIPv4, common.mustCall(() => {
36+
const clientOptions = {
37+
method: 'GET',
38+
host: common.localhostIPv4,
39+
port: server.address().port,
40+
path: '/test',
41+
privateKeyEngine: engine,
42+
privateKeyIdentifier: 'dummykey',
43+
cert: agentCert,
44+
rejectUnauthorized: false, // Prevent failing on self-signed certificates
45+
headers: {}
46+
};
47+
48+
const req = https.request(clientOptions, common.mustCall(function(response) {
49+
let body = '';
50+
response.setEncoding('utf8');
51+
response.on('data', function(chunk) {
52+
body += chunk;
53+
});
54+
55+
response.on('end', common.mustCall(function() {
56+
assert.strictEqual(body, 'hello world');
57+
server.close();
58+
}));
59+
}));
60+
61+
req.end();
62+
}));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#include <assert.h>
2+
#include <string.h>
3+
#include <stdlib.h>
4+
5+
#include <openssl/engine.h>
6+
#include <openssl/pem.h>
7+
8+
#include <fstream>
9+
#include <iterator>
10+
#include <string>
11+
12+
#ifndef ENGINE_CMD_BASE
13+
# error did not get engine.h
14+
#endif
15+
16+
#define TEST_ENGINE_ID "testkeyengine"
17+
#define TEST_ENGINE_NAME "dummy test key engine"
18+
19+
#define PRIVATE_KEY "test/fixtures/keys/agent1-key.pem"
20+
21+
namespace {
22+
23+
int EngineInit(ENGINE* engine) {
24+
return 1;
25+
}
26+
27+
int EngineFinish(ENGINE* engine) {
28+
return 1;
29+
}
30+
31+
int EngineDestroy(ENGINE* engine) {
32+
return 1;
33+
}
34+
35+
std::string LoadFile(const char* filename) {
36+
std::ifstream file(filename);
37+
return std::string(std::istreambuf_iterator<char>(file),
38+
std::istreambuf_iterator<char>());
39+
}
40+
41+
static EVP_PKEY* EngineLoadPrivkey(ENGINE* engine, const char* name,
42+
UI_METHOD* ui_method, void* callback_data) {
43+
if (strcmp(name, "dummykey") == 0) {
44+
std::string key = LoadFile(PRIVATE_KEY);
45+
BIO* bio = BIO_new_mem_buf(key.data(), key.size());
46+
EVP_PKEY* ret = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr);
47+
48+
BIO_vfree(bio);
49+
if (ret != nullptr) {
50+
return ret;
51+
}
52+
}
53+
54+
return nullptr;
55+
}
56+
57+
int bind_fn(ENGINE* engine, const char* id) {
58+
ENGINE_set_id(engine, TEST_ENGINE_ID);
59+
ENGINE_set_name(engine, TEST_ENGINE_NAME);
60+
ENGINE_set_init_function(engine, EngineInit);
61+
ENGINE_set_finish_function(engine, EngineFinish);
62+
ENGINE_set_destroy_function(engine, EngineDestroy);
63+
ENGINE_set_load_privkey_function(engine, EngineLoadPrivkey);
64+
65+
return 1;
66+
}
67+
68+
extern "C" {
69+
IMPLEMENT_DYNAMIC_CHECK_FN();
70+
IMPLEMENT_DYNAMIC_BIND_FN(bind_fn);
71+
}
72+
73+
} // anonymous namespace
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const tls = require('tls');
8+
9+
common.expectsError(
10+
() => {
11+
tls.createSecureContext({ privateKeyEngine: 0,
12+
privateKeyIdentifier: 'key' });
13+
},
14+
{ code: 'ERR_INVALID_ARG_TYPE',
15+
message: / Received type number$/ });
16+
17+
common.expectsError(
18+
() => {
19+
tls.createSecureContext({ privateKeyEngine: 'engine',
20+
privateKeyIdentifier: 0 });
21+
},
22+
{ code: 'ERR_INVALID_ARG_TYPE',
23+
message: / Received type number$/ });
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
// Monkey-patch SecureContext
9+
const { internalBinding } = require('internal/test/binding');
10+
const binding = internalBinding('crypto');
11+
const NativeSecureContext = binding.SecureContext;
12+
13+
binding.SecureContext = function() {
14+
const rv = new NativeSecureContext();
15+
rv.setEngineKey = undefined;
16+
return rv;
17+
};
18+
19+
const tls = require('tls');
20+
21+
{
22+
common.expectsError(
23+
() => {
24+
tls.createSecureContext({
25+
privateKeyEngine: 'engine',
26+
privateKeyIdentifier: 'key'
27+
});
28+
},
29+
{
30+
code: 'ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED',
31+
message: 'Custom engines not supported by this OpenSSL'
32+
}
33+
);
34+
}

0 commit comments

Comments
 (0)