Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit cf7b405

Browse filesBrowse files
OYTISBridgeAR
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 <rod@vagg.org> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
1 parent 54ef0fd commit cf7b405
Copy full SHA for cf7b405

File tree

Expand file treeCollapse file tree

9 files changed

+314
-3
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

9 files changed

+314
-3
lines changed
Open diff view settings
Collapse file

‎doc/api/tls.md‎

Copy file name to clipboardExpand all lines: doc/api/tls.md
+10Lines changed: 10 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
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.
Collapse file

‎lib/_tls_common.js‎

Copy file name to clipboardExpand all lines: lib/_tls_common.js
+30Lines changed: 30 additions & 0 deletions
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);
Collapse file

‎src/node_crypto.cc‎

Copy file name to clipboardExpand all lines: src/node_crypto.cc
+53-3Lines changed: 53 additions & 3 deletions
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);
Collapse file

‎src/node_crypto.h‎

Copy file name to clipboardExpand all lines: src/node_crypto.h
+4Lines changed: 4 additions & 0 deletions
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);
Collapse file
+25Lines changed: 25 additions & 0 deletions
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+
}
Collapse file
+62Lines changed: 62 additions & 0 deletions
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+
}));
Collapse file
+73Lines changed: 73 additions & 0 deletions
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
Collapse file
+23Lines changed: 23 additions & 0 deletions
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$/ });
Collapse file
+34Lines changed: 34 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.