Skip to content

Commit ceae0bc

Browse files
author
Stéphan Kochen
committed
tls: Add PSK support
Add the `pskCallback` client/server option, which resolves an identity or identity (hint) to a pre-shared key. The function signature on client and server is compatible. Add the `pskIdentity` server option to set the identity hint for the ServerKeyExchange message. Based on work by: Chris Osborn <chris.osborn@sitelier.com>
1 parent 87a039d commit ceae0bc

File tree

9 files changed

+514
-4
lines changed

9 files changed

+514
-4
lines changed

doc/api/tls.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const tls = require('tls');
1212

1313
## TLS/SSL Concepts
1414

15-
The TLS/SSL is a public/private key infrastructure (PKI). For most common
16-
cases, each client and server must have a *private key*.
15+
In most common cases, TLS/SSL uses a public/private key infrastructure (PKI).
16+
Each client and server must have a *private key*.
1717

1818
Private keys can be generated in multiple ways. The example below illustrates
1919
use of the OpenSSL command-line interface to generate a 2048-bit RSA private
@@ -118,6 +118,26 @@ handshake extensions:
118118
*Note*: Use of ALPN is recommended over NPN. The NPN extension has never been
119119
formally defined or documented and generally not recommended for use.
120120

121+
### Pre-shared keys
122+
123+
<!-- type=misc -->
124+
125+
TLS-PSK support is also available as an alternative to normal certificate-based
126+
authentication. TLS-PSK uses a pre-shared key instead of certificates to
127+
authenticate a TLS connection, providing mutual authentication.
128+
129+
TLS-PSK and public key infrastructure are not mutually exclusive; clients and
130+
servers can accommodate both, with the variety determined by the normal cipher
131+
negotiation step.
132+
133+
Note that TLS-PSK is only a good choice where means exist to securely share a
134+
key with every connecting machine, so it does not replace PKI for the majority
135+
of TLS uses.
136+
137+
The TLS-PSK implementation in OpenSSL has also seen many security flaws in
138+
recent years, mostly because it is used only by a minority of applications.
139+
Please consider all alternative solutions before switching to PSK ciphers.
140+
121141
### Client-initiated renegotiation attack mitigation
122142

123143
<!-- type=misc -->
@@ -773,6 +793,14 @@ changes:
773793
against the list of supplied CAs. An `'error'` event is emitted if
774794
verification fails; `err.code` contains the OpenSSL error code. Defaults to
775795
`true`.
796+
* `pskCallback(hint)` {Function} When negotiating TLS-PSK, this optional
797+
function is called with the identity hint provided by the server. If the
798+
client should continue to negotiate PSK ciphers, the return value of this
799+
function must be an object in the form `{psk: <string|buffer>, identity:
800+
<string>}`. Note that PSK ciphers are disabled by default, and using
801+
TLS-PSK thus requires explicitly specifying a cipher suite with the
802+
`ciphers` option. Additionally, it may be necessary to disable
803+
`rejectUnauthorized` when not intending to use certificates.
776804
* `NPNProtocols` {string[]|Buffer[]} An array of strings or `Buffer`s
777805
containing supported NPN protocols. `Buffer`s should have the format
778806
`[len][name][len][name]...` e.g. `0x05hello0x05world`, where the first
@@ -1036,7 +1064,8 @@ changes:
10361064
session tickets on multiple instances of the TLS server. *Note* that this is
10371065
automatically shared between `cluster` module workers.
10381066
* ...: Any [`tls.createSecureContext()`][] options can be provided. For
1039-
servers, the identity options (`pfx` or `key`/`cert`) are usually required.
1067+
servers, the identity options (`pfx`, `key`/`cert` or `pskCallback`) are
1068+
usually required.
10401069
* `secureConnectionListener` {Function}
10411070

10421071
Creates a new [tls.Server][]. The `secureConnectionListener`, if provided, is
@@ -1193,6 +1222,14 @@ changes:
11931222
* `rejectUnauthorized` {boolean} `true` to specify whether a server should
11941223
automatically reject clients with invalid certificates. Only applies when
11951224
`isServer` is `true`.
1225+
* `pskCallback(identity)` {Function} When negotiating TLS-PSK, this optional
1226+
function is called with the identity provided by the client. If the server
1227+
should continue to negotiate PSK ciphers, the return value of this function
1228+
must be an object in the form `{psk: <string|buffer>}`. Note that PSK ciphers
1229+
are disabled by default, and using TLS-PSK thus requires explicitly
1230+
specifying a cipher suite with the `ciphers` option.
1231+
* `pskIdentity`: {string} The identity hint to send to clients when
1232+
negotiating TLS-PSK.
11961233
* `options`
11971234
* `secureContext`: An optional TLS context object from
11981235
[`tls.createSecureContext()`][]

lib/_tls_common.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,46 @@ exports.createSecureContext = function createSecureContext(options, context) {
111111
}
112112
}
113113

114+
if (options.pskCallback && c.context.enablePskCallback) {
115+
c.context.onpskexchange = (identity, maxPskLen, maxIdentLen) => {
116+
let ret = options.pskCallback(identity);
117+
if (typeof ret !== 'object') {
118+
ret = undefined;
119+
}
120+
121+
if (ret) {
122+
ret = { psk: ret.psk, identity: ret.identity };
123+
124+
if (typeof ret.psk === 'string') {
125+
ret.psk = Buffer.from(ret.psk);
126+
} else if (!Buffer.isBuffer(ret.psk)) {
127+
throw new TypeError('Pre-shared key is not a string or buffer');
128+
}
129+
if (ret.psk.length > maxPskLen) {
130+
throw new TypeError(`Pre-shared key exceed ${maxPskLen} bytes`);
131+
}
132+
133+
// Only set for the client callback, which must provide an identity.
134+
if (maxIdentLen) {
135+
if (typeof ret.identity === 'string') {
136+
ret.identity = Buffer.from(ret.identity + '\0');
137+
} else {
138+
throw new TypeError('PSK identity is not a string');
139+
}
140+
if (ret.identity.length > maxIdentLen) {
141+
throw new TypeError(`PSK identity exceeds ${maxIdentLen} bytes`);
142+
}
143+
}
144+
}
145+
146+
return ret;
147+
};
148+
c.context.enablePskCallback();
149+
150+
if (options.pskIdentity)
151+
c.context.setPskIdentity(options.pskIdentity);
152+
}
153+
114154
if (options.sessionIdContext) {
115155
c.context.setSessionIdContext(options.sessionIdContext);
116156
}

lib/_tls_wrap.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ TLSSocket.prototype.getProtocol = function() {
681681
return null;
682682
};
683683

684-
// TODO: support anonymous (nocert) and PSK
684+
// TODO: support anonymous (nocert)
685685

686686

687687
// AUTHENTICATION MODES
@@ -780,6 +780,8 @@ function Server(options, listener) {
780780
dhparam: self.dhparam,
781781
secureProtocol: self.secureProtocol,
782782
secureOptions: self.secureOptions,
783+
pskCallback: self.pskCallback,
784+
pskIdentity: self.pskIdentity,
783785
honorCipherOrder: self.honorCipherOrder,
784786
crl: self.crl,
785787
sessionIdContext: self.sessionIdContext
@@ -920,6 +922,8 @@ Server.prototype.setOptions = function(options) {
920922
else
921923
this.honorCipherOrder = true;
922924
if (secureOptions) this.secureOptions = secureOptions;
925+
if (options.pskIdentity) this.pskIdentity = options.pskIdentity;
926+
if (options.pskCallback) this.pskCallback = options.pskCallback;
923927
if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this);
924928
if (options.ALPNProtocols)
925929
tls.convertALPNProtocols(options.ALPNProtocols, this);

src/env.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ namespace node {
9191
V(emitting_top_level_domain_error_string, "_emittingTopLevelDomainError") \
9292
V(exchange_string, "exchange") \
9393
V(enumerable_string, "enumerable") \
94+
V(identity_string, "identity") \
9495
V(idle_string, "idle") \
9596
V(irq_string, "irq") \
9697
V(encoding_string, "encoding") \
@@ -155,6 +156,7 @@ namespace node {
155156
V(onnewsession_string, "onnewsession") \
156157
V(onnewsessiondone_string, "onnewsessiondone") \
157158
V(onocspresponse_string, "onocspresponse") \
159+
V(onpskexchange_string, "onpskexchange") \
158160
V(onread_string, "onread") \
159161
V(onreadstart_string, "onreadstart") \
160162
V(onreadstop_string, "onreadstop") \
@@ -175,6 +177,7 @@ namespace node {
175177
V(preference_string, "preference") \
176178
V(priority_string, "priority") \
177179
V(produce_cached_data_string, "produceCachedData") \
180+
V(psk_string, "psk") \
178181
V(raw_string, "raw") \
179182
V(readable_string, "readable") \
180183
V(received_shutdown_string, "receivedShutdown") \

src/node_crypto.cc

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,13 @@ void SecureContext::Initialize(Environment* env, Local<Object> target) {
305305
env->SetProtoMethod(t, "getCertificate", SecureContext::GetCertificate<true>);
306306
env->SetProtoMethod(t, "getIssuer", SecureContext::GetCertificate<false>);
307307

308+
#ifdef OPENSSL_PSK_SUPPORT
309+
env->SetProtoMethod(t, "setPskIdentity", SecureContext::SetPskIdentity);
310+
env->SetProtoMethod(t,
311+
"enablePskCallback",
312+
SecureContext::EnablePskCallback);
313+
#endif
314+
308315
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kTicketKeyReturnIndex"),
309316
Integer::NewFromUnsigned(env->isolate(), kTicketKeyReturnIndex));
310317
t->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "kTicketKeyHMACIndex"),
@@ -1287,6 +1294,120 @@ void SecureContext::GetCertificate(const FunctionCallbackInfo<Value>& args) {
12871294
}
12881295

12891296

1297+
#ifdef OPENSSL_PSK_SUPPORT
1298+
1299+
void SecureContext::SetPskIdentity(const FunctionCallbackInfo<Value>& args) {
1300+
SecureContext* wrap = Unwrap<SecureContext>(args.Holder());
1301+
1302+
if (args.Length() != 1 || !args[0]->IsString()) {
1303+
return wrap->env()->ThrowTypeError("Argument must be a string");
1304+
}
1305+
1306+
String::Utf8Value identity(args[0]);
1307+
if (!SSL_CTX_use_psk_identity_hint(wrap->ctx_, *identity)) {
1308+
return wrap->env()->ThrowError("Failed to set PSK identity hint");
1309+
}
1310+
}
1311+
1312+
void SecureContext::EnablePskCallback(
1313+
const FunctionCallbackInfo<Value>& args) {
1314+
SecureContext* wrap = Unwrap<SecureContext>(args.Holder());
1315+
1316+
SSL_CTX_set_psk_server_callback(wrap->ctx_,
1317+
SecureContext::PskServerCallback);
1318+
SSL_CTX_set_psk_client_callback(wrap->ctx_,
1319+
SecureContext::PskClientCallback);
1320+
}
1321+
1322+
unsigned int SecureContext::PskServerCallback(SSL *ssl,
1323+
const char *identity,
1324+
unsigned char *psk,
1325+
unsigned int max_psk_len) {
1326+
SecureContext* sc = static_cast<SecureContext*>(
1327+
SSL_CTX_get_app_data(ssl->ctx));
1328+
1329+
Environment* env = sc->env();
1330+
Isolate* isolate = env->isolate();
1331+
1332+
Local<Value> argv[] = {
1333+
String::NewFromUtf8(isolate, identity),
1334+
Integer::NewFromUnsigned(isolate, max_psk_len),
1335+
Integer::NewFromUnsigned(isolate, 0)
1336+
};
1337+
Local<Value> ret = node::MakeCallback(env,
1338+
sc->object(),
1339+
env->onpskexchange_string(),
1340+
arraysize(argv),
1341+
argv);
1342+
1343+
// The result is expected to be an object. If it isn't, then return 0,
1344+
// indicating the identity wasn't found.
1345+
if (!ret->IsObject()) {
1346+
return 0;
1347+
}
1348+
Local<Object> obj = ret.As<Object>();
1349+
1350+
Local<Value> psk_buf = obj->Get(env->psk_string());
1351+
assert(Buffer::HasInstance(psk_buf));
1352+
size_t psk_len = Buffer::Length(psk_buf);
1353+
assert(psk_len <= max_psk_len);
1354+
memcpy(psk, Buffer::Data(psk_buf), max_psk_len);
1355+
1356+
return psk_len;
1357+
}
1358+
1359+
unsigned int SecureContext::PskClientCallback(SSL *ssl,
1360+
const char *hint,
1361+
char *identity,
1362+
unsigned int max_identity_len,
1363+
unsigned char *psk,
1364+
unsigned int max_psk_len) {
1365+
SecureContext* sc = static_cast<SecureContext*>(
1366+
SSL_CTX_get_app_data(ssl->ctx));
1367+
1368+
Environment* env = sc->env();
1369+
Isolate* isolate = env->isolate();
1370+
1371+
Local<Value> argv[] = {
1372+
Null(isolate),
1373+
Integer::NewFromUnsigned(isolate, max_psk_len),
1374+
Integer::NewFromUnsigned(isolate, max_identity_len)
1375+
};
1376+
if (hint != nullptr) {
1377+
argv[0] = String::NewFromUtf8(isolate, hint);
1378+
}
1379+
Local<Value> ret = node::MakeCallback(env,
1380+
sc->object(),
1381+
env->onpskexchange_string(),
1382+
arraysize(argv),
1383+
argv);
1384+
1385+
// The result is expected to be an object. If it isn't, then return 0,
1386+
// indicating the identity wasn't found.
1387+
if (!ret->IsObject()) {
1388+
return 0;
1389+
}
1390+
Local<Object> obj = ret.As<Object>();
1391+
1392+
Local<Value> psk_buf = obj->Get(env->psk_string());
1393+
assert(Buffer::HasInstance(psk_buf));
1394+
size_t psk_len = Buffer::Length(psk_buf);
1395+
assert(psk_len <= max_psk_len);
1396+
memcpy(psk, Buffer::Data(psk_buf), psk_len);
1397+
1398+
Local<Value> identity_buf = obj->Get(env->identity_string());
1399+
assert(Buffer::HasInstance(identity_buf));
1400+
size_t identity_len = Buffer::Length(identity_buf);
1401+
assert(identity_len <= max_identity_len);
1402+
memcpy(identity, Buffer::Data(identity_buf), identity_len);
1403+
assert(identity[identity_len - 1] == '\0');
1404+
1405+
return psk_len;
1406+
}
1407+
1408+
#endif
1409+
1410+
12901411
template <class Base>
12911412
void SSLWrap<Base>::AddMethods(Environment* env, Local<FunctionTemplate> t) {
12921413
HandleScope scope(env->isolate());

src/node_crypto.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@
3838
# define NODE__HAVE_TLSEXT_STATUS_CB
3939
#endif // !defined(OPENSSL_NO_TLSEXT) && defined(SSL_CTX_set_tlsext_status_cb)
4040

41+
// TLS-PSK support requires OpenSSL v1.0.0 or later built with PSK enabled
42+
#if OPENSSL_VERSION_NUMBER >= 0x10000000L
43+
#ifndef OPENSSL_NO_PSK
44+
#define OPENSSL_PSK_SUPPORT
45+
#endif
46+
#endif
47+
4148
namespace node {
4249
namespace crypto {
4350

@@ -123,6 +130,23 @@ class SecureContext : public BaseObject {
123130
template <bool primary>
124131
static void GetCertificate(const v8::FunctionCallbackInfo<v8::Value>& args);
125132

133+
#ifdef OPENSSL_PSK_SUPPORT
134+
static void SetPskIdentity(const v8::FunctionCallbackInfo<v8::Value>& args);
135+
static void EnablePskCallback(
136+
const v8::FunctionCallbackInfo<v8::Value>& args);
137+
138+
static unsigned int PskServerCallback(SSL *ssl,
139+
const char *identity,
140+
unsigned char *psk,
141+
unsigned int max_psk_len);
142+
static unsigned int PskClientCallback(SSL *ssl,
143+
const char *hint,
144+
char *identity,
145+
unsigned int max_identity_len,
146+
unsigned char *psk,
147+
unsigned int max_psk_len);
148+
#endif
149+
126150
static int TicketKeyCallback(SSL* ssl,
127151
unsigned char* name,
128152
unsigned char* iv,

0 commit comments

Comments
 (0)