Skip to content

Commit 6ee985f

Browse files
joelostrowskiTrott
authored andcommitted
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.
1 parent f7436ba commit 6ee985f

File tree

11 files changed

+305
-15
lines changed

11 files changed

+305
-15
lines changed

doc/api/errors.md

+6
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,12 @@ Used when `Console` is instantiated without `stdout` stream or when `stdout` or
653653

654654
Used when the native call from `process.cpuUsage` cannot be processed properly.
655655

656+
<a id="ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED"></a>
657+
### ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED
658+
659+
Used when a client certificate engine is requested that is not supported by the
660+
version of OpenSSL being used.
661+
656662
<a id="ERR_CRYPTO_ECDH_INVALID_FORMAT"></a>
657663
### ERR_CRYPTO_ECDH_INVALID_FORMAT
658664

lib/_tls_common.js

+12
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,18 @@ exports.createSecureContext = function createSecureContext(options, context) {
208208
c.context.setFreeListLength(0);
209209
}
210210

211+
if (typeof options.clientCertEngine === 'string') {
212+
if (c.context.setClientCertEngine)
213+
c.context.setClientCertEngine(options.clientCertEngine);
214+
else
215+
throw new errors.Error('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED');
216+
} else if (options.clientCertEngine != null) {
217+
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
218+
'options.clientCertEngine',
219+
['string', 'null', 'undefined'],
220+
options.clientCertEngine);
221+
}
222+
211223
return c;
212224
};
213225

lib/_tls_wrap.js

+4
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ function tlsConnectionListener(rawSocket) {
816816
// - rejectUnauthorized. Boolean, default to true.
817817
// - key. string.
818818
// - cert: string.
819+
// - clientCertEngine: string.
819820
// - ca: string or array of strings.
820821
// - sessionTimeout: integer.
821822
//
@@ -859,6 +860,7 @@ function Server(options, listener) {
859860
key: this.key,
860861
passphrase: this.passphrase,
861862
cert: this.cert,
863+
clientCertEngine: this.clientCertEngine,
862864
ca: this.ca,
863865
ciphers: this.ciphers,
864866
ecdhCurve: this.ecdhCurve,
@@ -931,6 +933,8 @@ Server.prototype.setOptions = function(options) {
931933
if (options.key) this.key = options.key;
932934
if (options.passphrase) this.passphrase = options.passphrase;
933935
if (options.cert) this.cert = options.cert;
936+
if (options.clientCertEngine)
937+
this.clientCertEngine = options.clientCertEngine;
934938
if (options.ca) this.ca = options.ca;
935939
if (options.secureProtocol) this.secureProtocol = options.secureProtocol;
936940
if (options.crl) this.crl = options.crl;

lib/https.js

+4
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ Agent.prototype.getName = function getName(options) {
160160
if (options.cert)
161161
name += options.cert;
162162

163+
name += ':';
164+
if (options.clientCertEngine)
165+
name += options.clientCertEngine;
166+
163167
name += ':';
164168
if (options.ciphers)
165169
name += options.ciphers;

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ E('ERR_CHILD_CLOSED_BEFORE_REPLY', 'Child closed before reply received');
232232
E('ERR_CONSOLE_WRITABLE_STREAM',
233233
'Console expects a writable stream instance for %s');
234234
E('ERR_CPU_USAGE', 'Unable to obtain cpu usage %s');
235+
E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED',
236+
'Custom engines not supported by this OpenSSL');
235237
E('ERR_CRYPTO_ECDH_INVALID_FORMAT', 'Invalid ECDH format: %s');
236238
E('ERR_CRYPTO_ENGINE_UNKNOWN', 'Engine "%s" was not found');
237239
E('ERR_CRYPTO_FIPS_FORCED',

src/node_crypto.cc

+82-13
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,41 @@ static int PasswordCallback(char *buf, int size, int rwflag, void *u) {
354354
return 0;
355355
}
356356

357+
// Loads OpenSSL engine by engine id and returns it. The loaded engine
358+
// gets a reference so remember the corresponding call to ENGINE_free.
359+
// In case of error the appropriate js exception is scheduled
360+
// and nullptr is returned.
361+
#ifndef OPENSSL_NO_ENGINE
362+
static ENGINE* LoadEngineById(const char* engine_id, char (*errmsg)[1024]) {
363+
MarkPopErrorOnReturn mark_pop_error_on_return;
364+
365+
ENGINE* engine = ENGINE_by_id(engine_id);
366+
367+
if (engine == nullptr) {
368+
// Engine not found, try loading dynamically.
369+
engine = ENGINE_by_id("dynamic");
370+
if (engine != nullptr) {
371+
if (!ENGINE_ctrl_cmd_string(engine, "SO_PATH", engine_id, 0) ||
372+
!ENGINE_ctrl_cmd_string(engine, "LOAD", nullptr, 0)) {
373+
ENGINE_free(engine);
374+
engine = nullptr;
375+
}
376+
}
377+
}
378+
379+
if (engine == nullptr) {
380+
int err = ERR_get_error();
381+
if (err != 0) {
382+
ERR_error_string_n(err, *errmsg, sizeof(*errmsg));
383+
} else {
384+
snprintf(*errmsg, sizeof(*errmsg),
385+
"Engine \"%s\" was not found", engine_id);
386+
}
387+
}
388+
389+
return engine;
390+
}
391+
#endif // !OPENSSL_NO_ENGINE
357392

358393
// This callback is used to avoid the default passphrase callback in OpenSSL
359394
// which will typically prompt for the passphrase. The prompting is designed
@@ -498,6 +533,10 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
498533
SecureContext::SetSessionTimeout);
499534
env->SetProtoMethod(t, "close", SecureContext::Close);
500535
env->SetProtoMethod(t, "loadPKCS12", SecureContext::LoadPKCS12);
536+
#ifndef OPENSSL_NO_ENGINE
537+
env->SetProtoMethod(t, "setClientCertEngine",
538+
SecureContext::SetClientCertEngine);
539+
#endif // !OPENSSL_NO_ENGINE
501540
env->SetProtoMethod(t, "getTicketKeys", SecureContext::GetTicketKeys);
502541
env->SetProtoMethod(t, "setTicketKeys", SecureContext::SetTicketKeys);
503542
env->SetProtoMethod(t, "setFreeListLength", SecureContext::SetFreeListLength);
@@ -1295,6 +1334,46 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
12951334
}
12961335

12971336

1337+
#ifndef OPENSSL_NO_ENGINE
1338+
void SecureContext::SetClientCertEngine(
1339+
const FunctionCallbackInfo<Value>& args) {
1340+
Environment* env = Environment::GetCurrent(args);
1341+
CHECK_EQ(args.Length(), 1);
1342+
CHECK(args[0]->IsString());
1343+
1344+
SecureContext* sc = Unwrap<SecureContext>(args.This());
1345+
1346+
MarkPopErrorOnReturn mark_pop_error_on_return;
1347+
1348+
// SSL_CTX_set_client_cert_engine does not itself support multiple
1349+
// calls by cleaning up before overwriting the client_cert_engine
1350+
// internal context variable.
1351+
// Instead of trying to fix up this problem we in turn also do not
1352+
// support multiple calls to SetClientCertEngine.
1353+
if (sc->client_cert_engine_provided_) {
1354+
return env->ThrowError(
1355+
"Multiple calls to SetClientCertEngine are not allowed");
1356+
}
1357+
1358+
const node::Utf8Value engine_id(env->isolate(), args[0]);
1359+
char errmsg[1024];
1360+
ENGINE* engine = LoadEngineById(*engine_id, &errmsg);
1361+
1362+
if (engine == nullptr) {
1363+
return env->ThrowError(errmsg);
1364+
}
1365+
1366+
int r = SSL_CTX_set_client_cert_engine(sc->ctx_, engine);
1367+
// Free reference (SSL_CTX_set_client_cert_engine took it via ENGINE_init).
1368+
ENGINE_free(engine);
1369+
if (r == 0) {
1370+
return ThrowCryptoError(env, ERR_get_error());
1371+
}
1372+
sc->client_cert_engine_provided_ = true;
1373+
}
1374+
#endif // !OPENSSL_NO_ENGINE
1375+
1376+
12981377
void SecureContext::GetTicketKeys(const FunctionCallbackInfo<Value>& args) {
12991378
#if !defined(OPENSSL_NO_TLSEXT) && defined(SSL_CTX_get_tlsext_ticket_keys)
13001379

@@ -6093,20 +6172,10 @@ void SetEngine(const FunctionCallbackInfo<Value>& args) {
60936172

60946173
ClearErrorOnReturn clear_error_on_return;
60956174

6175+
// Load engine.
60966176
const node::Utf8Value engine_id(env->isolate(), args[0]);
6097-
ENGINE* engine = ENGINE_by_id(*engine_id);
6098-
6099-
// Engine not found, try loading dynamically
6100-
if (engine == nullptr) {
6101-
engine = ENGINE_by_id("dynamic");
6102-
if (engine != nullptr) {
6103-
if (!ENGINE_ctrl_cmd_string(engine, "SO_PATH", *engine_id, 0) ||
6104-
!ENGINE_ctrl_cmd_string(engine, "LOAD", nullptr, 0)) {
6105-
ENGINE_free(engine);
6106-
engine = nullptr;
6107-
}
6108-
}
6109-
}
6177+
char errmsg[1024];
6178+
ENGINE* engine = LoadEngineById(*engine_id, &errmsg);
61106179

61116180
if (engine == nullptr) {
61126181
int err = ERR_get_error();

src/node_crypto.h

+7
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ class SecureContext : public BaseObject {
9393
SSL_CTX* ctx_;
9494
X509* cert_;
9595
X509* issuer_;
96+
#ifndef OPENSSL_NO_ENGINE
97+
bool client_cert_engine_provided_ = false;
98+
#endif // !OPENSSL_NO_ENGINE
9699

97100
static const int kMaxSessionSize = 10 * 1024;
98101

@@ -135,6 +138,10 @@ class SecureContext : public BaseObject {
135138
const v8::FunctionCallbackInfo<v8::Value>& args);
136139
static void Close(const v8::FunctionCallbackInfo<v8::Value>& args);
137140
static void LoadPKCS12(const v8::FunctionCallbackInfo<v8::Value>& args);
141+
#ifndef OPENSSL_NO_ENGINE
142+
static void SetClientCertEngine(
143+
const v8::FunctionCallbackInfo<v8::Value>& args);
144+
#endif // !OPENSSL_NO_ENGINE
138145
static void GetTicketKeys(const v8::FunctionCallbackInfo<v8::Value>& args);
139146
static void SetTicketKeys(const v8::FunctionCallbackInfo<v8::Value>& args);
140147
static void SetFreeListLength(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'testengine',
5+
'type': 'none',
6+
'conditions': [
7+
['OS=="mac" and '
8+
'node_use_openssl=="true" and '
9+
'node_shared=="false" and '
10+
'node_shared_openssl=="false"', {
11+
'type': 'shared_library',
12+
'sources': [ 'testengine.cc' ],
13+
'product_extension': 'engine',
14+
'include_dirs': ['../../../deps/openssl/openssl/include'],
15+
'link_settings': {
16+
'libraries': [
17+
'../../../../out/<(PRODUCT_DIR)/<(OPENSSL_PRODUCT)'
18+
]
19+
},
20+
}]
21+
]
22+
}
23+
]
24+
}
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}/testengine.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 port = common.PORT;
25+
26+
const serverOptions = {
27+
key: agentKey,
28+
cert: agentCert,
29+
ca: agentCa,
30+
requestCert: true,
31+
rejectUnauthorized: true
32+
};
33+
34+
const server = https.createServer(serverOptions, (req, res) => {
35+
res.writeHead(200);
36+
res.end('hello world');
37+
}).listen(port, common.localhostIPv4, () => {
38+
const clientOptions = {
39+
method: 'GET',
40+
host: common.localhostIPv4,
41+
port: port,
42+
path: '/test',
43+
clientCertEngine: engine, // engine will provide key+cert
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+
});

0 commit comments

Comments
 (0)