Skip to content

Commit bfccc00

Browse files
lundibunditargos
authored andcommitted
tls: add PSK support
Add the `pskCallback` client/server option, which resolves an identity or identity hint to a pre-shared key. Add the `pskIdentityHint` server option to set the identity hint for the ServerKeyExchange message. Co-authored-by: Chris Osborn <chris.osborn@sitelier.com> Co-authored-by: stephank <gh@stephank.nl> Co-authored-by: Taylor Zane Glaeser <tzglaeser@gmail.com> PR-URL: #23188 Reviewed-By: Sam Roberts <vieuxtech@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent 1f0a1d5 commit bfccc00

12 files changed

Lines changed: 646 additions & 9 deletions

File tree

‎doc/api/errors.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1849,6 +1849,11 @@ vector for denial-of-service attacks.
18491849
An attempt was made to issue Server Name Indication from a TLS server-side
18501850
socket, which is only valid from a client.
18511851

1852+
<aid="ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED"></a>
1853+
### ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED
1854+
1855+
Failed to set PSK identity hint. Hint may be too long.
1856+
18521857
<aid="ERR_TRACE_EVENTS_CATEGORY_REQUIRED"></a>
18531858
### `ERR_TRACE_EVENTS_CATEGORY_REQUIRED`
18541859

‎doc/api/tls.md‎

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,40 @@ SNI (Server Name Indication) are TLS handshake extensions:
118118
* SNI: Allows the use of one TLS server for multiple hostnames with different
119119
SSL certificates.
120120

121+
### Pre-shared keys
122+
123+
<!-- type=misc -->
124+
125+
TLS-PSK support is available as an alternative to normal certificate-based
126+
authentication. It uses a pre-shared key instead of certificates to
127+
authenticate a TLS connection, providing mutual authentication.
128+
TLS-PSK and public key infrastructure are not mutually exclusive. Clients and
129+
servers can accommodate both, choosing either of them during the normal cipher
130+
negotiation step.
131+
132+
TLS-PSK is only a good choice where means exist to securely share a
133+
key with every connecting machine, so it does not replace PKI
134+
(Public Key Infrastructure) for the majority of TLS uses.
135+
The TLS-PSK implementation in OpenSSL has seen many security flaws in
136+
recent years, mostly because it is used only by a minority of applications.
137+
Please consider all alternative solutions before switching to PSK ciphers.
138+
Upon generating PSK it is of critical importance to use sufficient entropy as
139+
discussed in [RFC 4086][]. Deriving a shared secret from a password or other
140+
low-entropy sources is not secure.
141+
142+
PSK ciphers are disabled by default, and using TLS-PSK thus requires explicitly
143+
specifying a cipher suite with the `ciphers` option. The list of available
144+
ciphers can be retrieved via `openssl ciphers -v 'PSK'`. All TLS 1.3
145+
ciphers are eligible for PSK but currently only those that use SHA256 digest are
146+
supported they can be retrieved via `openssl ciphers -v -s -tls1_3 -psk`.
147+
148+
According to the [RFC 4279][], PSK identities up to 128 bytes in length and
149+
PSKs up to 64 bytes in length must be supported. As of OpenSSL 1.1.0
150+
maximum identity size is 128 bytes, and maximum PSK length is 256 bytes.
151+
152+
The current implementation doesn't support asynchronous PSK callbacks due to the
153+
limitations of the underlying OpenSSL API.
154+
121155
### Client-initiated renegotiation attack mitigation
122156

123157
<!-- type=misc -->
@@ -1207,6 +1241,9 @@ being issued by trusted CA (`options.ca`).
12071241
<!-- YAML
12081242
added: v0.11.3
12091243
changes:
1244+
- version: REPLACEME
1245+
pr-url: https://github.com/nodejs/node/pull/23188
1246+
description: The `pskCallback` option is now supported.
12101247
- version: v12.9.0
12111248
pr-url: https://github.com/nodejs/node/pull/27836
12121249
description: Support the `allowHalfOpen` option.
@@ -1258,6 +1295,23 @@ changes:
12581295
verified against the list of supplied CAs. An `'error'` event is emitted if
12591296
verification fails; `err.code` contains the OpenSSL error code. **Default:**
12601297
`true`.
1298+
*`pskCallback` {Function}
1299+
* hint: {string} optional message sent from the server to help client
1300+
decide which identity to use during negotiation.
1301+
Always `null` if TLS 1.3 is used.
1302+
* Returns: {Object} in the form
1303+
`{ psk: <Buffer|TypedArray|DataView>, identity: <string> }`
1304+
or `null` to stop the negotiation process. `psk` must be
1305+
compatible with the selected cipher's digest.
1306+
`identity` must use UTF-8 encoding.
1307+
When negotiating TLS-PSK (pre-shared keys), this function is called
1308+
with optional identity `hint` provided by the server or `null`
1309+
in case of TLS 1.3 where `hint` was removed.
1310+
It will be necessary to provide a custom `tls.checkServerIdentity()`
1311+
for the connection as the default one will try to check hostname/IP
1312+
of the server against the certificate but that's not applicable for PSK
1313+
because there won't be a certificate present.
1314+
More information can be found in the [RFC 4279][].
12611315
*`ALPNProtocols`: {string[]|Buffer[]|TypedArray[]|DataView[]|Buffer|
12621316
TypedArray|DataView}
12631317
An array of strings, `Buffer`s or `TypedArray`s or `DataView`s, or a
@@ -1593,8 +1647,30 @@ changes:
15931647
provided the default callback with high-level API will be used (see below).
15941648
*`ticketKeys`: {Buffer} 48-bytes of cryptographically strong pseudo-random
15951649
data. See [Session Resumption][] for more information.
1650+
*`pskCallback` {Function}
1651+
* socket: {tls.TLSSocket} the server [`tls.TLSSocket`][] instance for
1652+
this connection.
1653+
* identity: {string} identity parameter sent from the client.
1654+
* Returns: {Buffer|TypedArray|DataView} pre-shared key that must either be
1655+
a buffer or `null` to stop the negotiation process. Returned PSK must be
1656+
compatible with the selected cipher's digest.
1657+
When negotiating TLS-PSK (pre-shared keys), this function is called
1658+
with the identity provided by the client.
1659+
If the return value is `null` the negotiation process will stop and an
1660+
"unknown_psk_identity" alert message will be sent to the other party.
1661+
If the server wishes to hide the fact that the PSK identity was not known,
1662+
the callback must provide some random data as `psk` to make the connection
1663+
fail with "decrypt_error" before negotiation is finished.
1664+
PSK ciphers are disabled by default, and using TLS-PSK thus
1665+
requires explicitly specifying a cipher suite with the `ciphers` option.
1666+
More information can be found in the [RFC 4279][].
1667+
*`pskIdentityHint` {string} optional hint to send to a client to help
1668+
with selecting the identity during TLS-PSK negotiation. Will be ignored
1669+
in TLS 1.3. Upon failing to set pskIdentityHint `'tlsClientError'` will be
1670+
emitted with `'ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED'` code.
15961671
* ...: Any [`tls.createSecureContext()`][] option can be provided. For
1597-
servers, the identity options (`pfx` or `key`/`cert`) are usually required.
1672+
servers, the identity options (`pfx`, `key`/`cert` or `pskCallback`)
1673+
are usually required.
15981674
* ...: Any [`net.createServer()`][] option can be provided.
15991675
*`secureConnectionListener` {Function}
16001676
* Returns: {tls.Server}
@@ -1870,3 +1946,5 @@ where `secureSocket` has the same API as `pair.cleartext`.
18701946
[cipher list format]: https://www.openssl.org/docs/man1.1.1/man1/ciphers.html#CIPHER-LIST-FORMAT
18711947
[modifying the default cipher suite]: #tls_modifying_the_default_tls_cipher_suite
18721948
[specific attacks affecting larger AES key sizes]: https://www.schneier.com/blog/archives/2009/07/another_new_aes.html
1949+
[RFC 4279]: https://tools.ietf.org/html/rfc4279
1950+
[RFC 4086]: https://tools.ietf.org/html/rfc4086

‎lib/_tls_wrap.js‎

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ const { TCP, constants: TCPConstants } = internalBinding('tcp_wrap');
4343
consttls_wrap=internalBinding('tls_wrap');
4444
const{ Pipe,constants: PipeConstants}=internalBinding('pipe_wrap');
4545
const{ owner_symbol }=require('internal/async_hooks').symbols;
46+
const{ isArrayBufferView }=require('internal/util/types');
4647
const{SecureContext: NativeSecureContext}=internalBinding('crypto');
4748
const{ connResetException, codes }=require('internal/errors');
4849
const{
4950
ERR_INVALID_ARG_TYPE,
51+
ERR_INVALID_ARG_VALUE,
5052
ERR_INVALID_CALLBACK,
5153
ERR_MULTIPLE_CALLBACK,
5254
ERR_SOCKET_CLOSED,
@@ -58,8 +60,9 @@ const {
5860
ERR_TLS_SESSION_ATTACK,
5961
ERR_TLS_SNI_FROM_SERVER
6062
}=codes;
63+
const{onpskexchange: kOnPskExchange}=internalBinding('symbols');
6164
const{ getOptionValue }=require('internal/options');
62-
const{ validateString }=require('internal/validators');
65+
const{ validateString, validateBuffer}=require('internal/validators');
6366
consttraceTls=getOptionValue('--trace-tls');
6467
consttlsKeylog=getOptionValue('--tls-keylog');
6568
const{ appendFile }=require('fs');
@@ -70,6 +73,8 @@ const kHandshakeTimeout = Symbol('handshake-timeout');
7073
constkRes=Symbol('res');
7174
constkSNICallback=Symbol('snicallback');
7275
constkEnableTrace=Symbol('enableTrace');
76+
constkPskCallback=Symbol('pskcallback');
77+
constkPskIdentityHint=Symbol('pskidentityhint');
7378

7479
constnoop=()=>{};
7580

@@ -289,6 +294,67 @@ function onnewsession(sessionId, session) {
289294
done();
290295
}
291296

297+
functiononPskServerCallback(identity,maxPskLen){
298+
constowner=this[owner_symbol];
299+
constret=owner[kPskCallback](owner,identity);
300+
if(ret==null)
301+
returnundefined;
302+
303+
letpsk;
304+
if(isArrayBufferView(ret)){
305+
psk=ret;
306+
}else{
307+
if(typeofret!=='object'){
308+
thrownewERR_INVALID_ARG_TYPE(
309+
'ret',
310+
['Object','Buffer','TypedArray','DataView'],
311+
ret
312+
);
313+
}
314+
psk=ret.psk;
315+
validateBuffer(psk,'psk');
316+
}
317+
318+
if(psk.length>maxPskLen){
319+
thrownewERR_INVALID_ARG_VALUE(
320+
'psk',
321+
psk,
322+
`Pre-shared key exceeds ${maxPskLen} bytes`
323+
);
324+
}
325+
326+
returnpsk;
327+
}
328+
329+
functiononPskClientCallback(hint,maxPskLen,maxIdentityLen){
330+
constowner=this[owner_symbol];
331+
constret=owner[kPskCallback](hint);
332+
if(ret==null)
333+
returnundefined;
334+
335+
if(typeofret!=='object')
336+
thrownewERR_INVALID_ARG_TYPE('ret','Object',ret);
337+
338+
validateBuffer(ret.psk,'psk');
339+
if(ret.psk.length>maxPskLen){
340+
thrownewERR_INVALID_ARG_VALUE(
341+
'psk',
342+
ret.psk,
343+
`Pre-shared key exceeds ${maxPskLen} bytes`
344+
);
345+
}
346+
347+
validateString(ret.identity,'identity');
348+
if(Buffer.byteLength(ret.identity)>maxIdentityLen){
349+
thrownewERR_INVALID_ARG_VALUE(
350+
'identity',
351+
ret.identity,
352+
`PSK identity exceeds ${maxIdentityLen} bytes`
353+
);
354+
}
355+
356+
return{psk: ret.psk,identity: ret.identity};
357+
}
292358

293359
functiononkeylogclient(line){
294360
debug('client onkeylog');
@@ -687,6 +753,32 @@ TLSSocket.prototype._init = function(socket, wrap) {
687753
ssl.setALPNProtocols(ssl._secureContext.alpnBuffer);
688754
}
689755

756+
if(options.pskCallback&&ssl.enablePskCallback){
757+
if(typeofoptions.pskCallback!=='function'){
758+
thrownewERR_INVALID_ARG_TYPE('pskCallback',
759+
'function',
760+
options.pskCallback);
761+
}
762+
763+
ssl[kOnPskExchange]=options.isServer ?
764+
onPskServerCallback : onPskClientCallback;
765+
766+
this[kPskCallback]=options.pskCallback;
767+
ssl.enablePskCallback();
768+
769+
if(options.pskIdentityHint){
770+
if(typeofoptions.pskIdentityHint!=='string'){
771+
thrownewERR_INVALID_ARG_TYPE(
772+
'options.pskIdentityHint',
773+
'string',
774+
options.pskIdentityHint
775+
);
776+
}
777+
ssl.setPskIdentityHint(options.pskIdentityHint);
778+
}
779+
}
780+
781+
690782
if(options.handshakeTimeout>0)
691783
this.setTimeout(options.handshakeTimeout,this._handleTimeout);
692784

@@ -898,7 +990,7 @@ function makeSocketMethodProxy(name) {
898990
TLSSocket.prototype[method]=makeSocketMethodProxy(method);
899991
});
900992

901-
// TODO: support anonymous (nocert) and PSK
993+
// TODO: support anonymous (nocert)
902994

903995

904996
functiononServerSocketSecure(){
@@ -954,6 +1046,8 @@ function tlsConnectionListener(rawSocket) {
9541046
SNICallback: this[kSNICallback]||SNICallback,
9551047
enableTrace: this[kEnableTrace],
9561048
pauseOnConnect: this.pauseOnConnect,
1049+
pskCallback: this[kPskCallback],
1050+
pskIdentityHint: this[kPskIdentityHint],
9571051
});
9581052

9591053
socket.on('secure',onServerSocketSecure);
@@ -1058,6 +1152,8 @@ function Server(options, listener) {
10581152

10591153
this[kHandshakeTimeout]=options.handshakeTimeout||(120*1000);
10601154
this[kSNICallback]=options.SNICallback;
1155+
this[kPskCallback]=options.pskCallback;
1156+
this[kPskIdentityHint]=options.pskIdentityHint;
10611157

10621158
if(typeofthis[kHandshakeTimeout]!=='number'){
10631159
thrownewERR_INVALID_ARG_TYPE(
@@ -1069,6 +1165,18 @@ function Server(options, listener) {
10691165
'options.SNICallback','function',options.SNICallback);
10701166
}
10711167

1168+
if(this[kPskCallback]&&typeofthis[kPskCallback]!=='function'){
1169+
thrownewERR_INVALID_ARG_TYPE(
1170+
'options.pskCallback','function',options.pskCallback);
1171+
}
1172+
if(this[kPskIdentityHint]&&typeofthis[kPskIdentityHint]!=='string'){
1173+
thrownewERR_INVALID_ARG_TYPE(
1174+
'options.pskIdentityHint',
1175+
'string',
1176+
options.pskIdentityHint
1177+
);
1178+
}
1179+
10721180
// constructor call
10731181
net.Server.call(this,options,tlsConnectionListener);
10741182

@@ -1265,6 +1373,8 @@ Server.prototype.setOptions = deprecate(function(options) {
12651373
.digest('hex')
12661374
.slice(0,32);
12671375
}
1376+
if(options.pskCallback)this[kPskCallback]=options.pskCallback;
1377+
if(options.pskIdentityHint)this[kPskIdentityHint]=options.pskIdentityHint;
12681378
},'Server.prototype.setOptions() is deprecated','DEP0122');
12691379

12701380
// SNI Contexts High-Level API
@@ -1440,7 +1550,8 @@ exports.connect = function connect(...args) {
14401550
session: options.session,
14411551
ALPNProtocols: options.ALPNProtocols,
14421552
requestOCSP: options.requestOCSP,
1443-
enableTrace: options.enableTrace
1553+
enableTrace: options.enableTrace,
1554+
pskCallback: options.pskCallback,
14441555
});
14451556

14461557
tlssock[kConnectOptions]=options;

‎src/env.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,12 @@ constexpr size_t kFsStatsBufferLength =
160160

161161
// Symbols are per-isolate primitives but Environment proxies them
162162
// for the sake of convenience.
163-
#definePER_ISOLATE_SYMBOL_PROPERTIES(V) \
164-
V(handle_onclose_symbol, "handle_onclose") \
165-
V(no_message_symbol, "no_message_symbol") \
166-
V(oninit_symbol, "oninit") \
167-
V(owner_symbol, "owner") \
163+
#definePER_ISOLATE_SYMBOL_PROPERTIES(V) \
164+
V(handle_onclose_symbol, "handle_onclose") \
165+
V(no_message_symbol, "no_message_symbol") \
166+
V(oninit_symbol, "oninit") \
167+
V(owner_symbol, "owner") \
168+
V(onpskexchange_symbol, "onpskexchange") \
168169

169170
// Strings are per-isolate primitives but Environment proxies them
170171
// for the sake of convenience. Strings should be ASCII-only.
@@ -254,6 +255,7 @@ constexpr size_t kFsStatsBufferLength =
254255
V(host_string, "host") \
255256
V(hostmaster_string, "hostmaster") \
256257
V(http_1_1_string, "http/1.1") \
258+
V(identity_string, "identity") \
257259
V(ignore_string, "ignore") \
258260
V(import_string, "import") \
259261
V(infoaccess_string, "infoAccess") \
@@ -325,6 +327,7 @@ constexpr size_t kFsStatsBufferLength =
325327
V(priority_string, "priority") \
326328
V(process_string, "process") \
327329
V(promise_string, "promise") \
330+
V(psk_string, "psk") \
328331
V(pubkey_string, "pubkey") \
329332
V(query_string, "query") \
330333
V(raw_string, "raw") \

‎src/node_crypto.cc‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2620,6 +2620,16 @@ void SSLWrap<Base>::VerifyError(const FunctionCallbackInfo<Value>& args) {
26202620
if (X509* peer_cert = SSL_get_peer_certificate(w->ssl_.get())) {
26212621
X509_free(peer_cert);
26222622
x509_verify_error = SSL_get_verify_result(w->ssl_.get());
2623+
} else {
2624+
constSSL_CIPHER* curr_cipher = SSL_get_current_cipher(w->ssl_.get());
2625+
constSSL_SESSION* sess = SSL_get_session(w->ssl_.get());
2626+
// Allow no-cert for PSK authentication in TLS1.2 and lower.
2627+
// In TLS1.3 check that session was reused because TLS1.3 PSK
2628+
// looks like session resumption. Is there a better way?
2629+
if (SSL_CIPHER_get_auth_nid(curr_cipher) == NID_auth_psk ||
2630+
(SSL_SESSION_get_protocol_version(sess) == TLS1_3_VERSION &&
2631+
SSL_session_reused(w->ssl_.get())))
2632+
return args.GetReturnValue().SetNull();
26232633
}
26242634

26252635
if (x509_verify_error == X509_V_OK)

‎src/node_errors.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ void PrintErrorString(const char* format, ...);
5858
V(ERR_STRING_TOO_LONG, Error) \
5959
V(ERR_TLS_INVALID_PROTOCOL_METHOD, TypeError) \
6060
V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, TypeError) \
61+
V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, Error) \
6162

6263
#defineV(code, type) \
6364
inline v8::Local<v8::Value> code(v8::Isolate* isolate, \
@@ -101,6 +102,7 @@ void PrintErrorString(const char* format, ...);
101102
"Script execution was interrupted by `SIGINT`") \
102103
V(ERR_TRANSFERRING_EXTERNALIZED_SHAREDARRAYBUFFER, \
103104
"Cannot serialize externalized SharedArrayBuffer") \
105+
V(ERR_TLS_PSK_SET_IDENTIY_HINT_FAILED, "Failed to set PSK identity hint") \
104106

105107
#defineV(code, message) \
106108
inline v8::Local<v8::Value> code(v8::Isolate* isolate) { \

0 commit comments

Comments
 (0)