Skip to content

Commit 33c1e8b

Browse files
joelostrowskiMylesBorins
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 3b1db7f commit 33c1e8b

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
@@ -631,6 +631,12 @@ Used when `Console` is instantiated without `stdout` stream or when `stdout` or
631631

632632
Used when the native call from `process.cpuUsage` cannot be processed properly.
633633

634+
<a id="ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED"></a>
635+
### ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED
636+
637+
Used when a client certificate engine is requested that is not supported by the
638+
version of OpenSSL being used.
639+
634640
<a id="ERR_CRYPTO_ECDH_INVALID_FORMAT"></a>
635641
### ERR_CRYPTO_ECDH_INVALID_FORMAT
636642

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
@@ -155,6 +155,8 @@ E('ERR_CHILD_CLOSED_BEFORE_REPLY', 'Child closed before reply received');
155155
E('ERR_CONSOLE_WRITABLE_STREAM',
156156
'Console expects a writable stream instance for %s');
157157
E('ERR_CPU_USAGE', 'Unable to obtain cpu usage %s');
158+
E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED',
159+
'Custom engines not supported by this OpenSSL');
158160
E('ERR_CRYPTO_ECDH_INVALID_FORMAT', 'Invalid ECDH format: %s');
159161
E('ERR_CRYPTO_ENGINE_UNKNOWN', 'Engine "%s" was not found');
160162
E('ERR_CRYPTO_FIPS_FORCED',

src/node_crypto.cc

+82-13
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,41 @@ static int PasswordCallback(char *buf, int size, int rwflag, void *u) {
361361
return 0;
362362
}
363363

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

365400
// This callback is used to avoid the default passphrase callback in OpenSSL
366401
// which will typically prompt for the passphrase. The prompting is designed
@@ -505,6 +540,10 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
505540
SecureContext::SetSessionTimeout);
506541
env->SetProtoMethod(t, "close", SecureContext::Close);
507542
env->SetProtoMethod(t, "loadPKCS12", SecureContext::LoadPKCS12);
543+
#ifndef OPENSSL_NO_ENGINE
544+
env->SetProtoMethod(t, "setClientCertEngine",
545+
SecureContext::SetClientCertEngine);
546+
#endif // !OPENSSL_NO_ENGINE
508547
env->SetProtoMethod(t, "getTicketKeys", SecureContext::GetTicketKeys);
509548
env->SetProtoMethod(t, "setTicketKeys", SecureContext::SetTicketKeys);
510549
env->SetProtoMethod(t, "setFreeListLength", SecureContext::SetFreeListLength);
@@ -1302,6 +1341,46 @@ void SecureContext::LoadPKCS12(const FunctionCallbackInfo<Value>& args) {
13021341
}
13031342

13041343

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

@@ -6124,20 +6203,10 @@ void SetEngine(const FunctionCallbackInfo<Value>& args) {
61246203

61256204
ClearErrorOnReturn clear_error_on_return;
61266205

6206+
// Load engine.
61276207
const node::Utf8Value engine_id(env->isolate(), args[0]);
6128-
ENGINE* engine = ENGINE_by_id(*engine_id);
6129-
6130-
// Engine not found, try loading dynamically
6131-
if (engine == nullptr) {
6132-
engine = ENGINE_by_id("dynamic");
6133-
if (engine != nullptr) {
6134-
if (!ENGINE_ctrl_cmd_string(engine, "SO_PATH", *engine_id, 0) ||
6135-
!ENGINE_ctrl_cmd_string(engine, "LOAD", nullptr, 0)) {
6136-
ENGINE_free(engine);
6137-
engine = nullptr;
6138-
}
6139-
}
6140-
}
6208+
char errmsg[1024];
6209+
ENGINE* engine = LoadEngineById(*engine_id, &errmsg);
61416210

61426211
if (engine == nullptr) {
61436212
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)