Skip to content

Commit acb3aff

Browse files
simllllcodebytere
authored andcommitted
tls: expose SSL_export_keying_material
Fixes: #31802 PR-URL: #31814 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Tobias Nießen <tniessen@tnie.de>
1 parent 2046652 commit acb3aff

File tree

7 files changed

+208
-2
lines changed

7 files changed

+208
-2
lines changed

doc/api/errors.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,6 +1858,15 @@ added: v13.3.0
18581858

18591859
The context must be a `SecureContext`.
18601860

1861+
<a id="ERR_TLS_INVALID_STATE"></a>
1862+
### `ERR_TLS_INVALID_STATE`
1863+
<!-- YAML
1864+
added: REPLACEME
1865+
-->
1866+
1867+
The TLS socket must be connected and securily established. Ensure the 'secure'
1868+
event is emitted, before you continue.
1869+
18611870
<a id="ERR_TLS_INVALID_PROTOCOL_METHOD"></a>
18621871
### `ERR_TLS_INVALID_PROTOCOL_METHOD`
18631872

doc/api/tls.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,39 @@ See
10941094
[SSL_get_shared_sigalgs](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_shared_sigalgs.html)
10951095
for more information.
10961096

1097+
### `tlsSocket.exportKeyingMaterial(length, label[, context])`
1098+
<!-- YAML
1099+
added: REPLACEME
1100+
-->
1101+
1102+
* `length` {number} number of bytes to retrieve from keying material
1103+
* `label` {string} an application specific label, typically this will be a
1104+
value from the
1105+
[IANA Exporter Label Registry](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#exporter-labels).
1106+
* `context` {Buffer} Optionally provide a context.
1107+
1108+
* Returns: {Buffer} requested bytes of the keying material
1109+
1110+
Keying material is used for validations to prevent different kind of attacks in
1111+
network protocols, for example in the specifications of IEEE 802.1X.
1112+
1113+
Example
1114+
1115+
```js
1116+
const keyingMaterial = tlsSocket.exportKeyingMaterial(
1117+
128,
1118+
'client finished');
1119+
1120+
/**
1121+
Example return value of keyingMaterial:
1122+
<Buffer 76 26 af 99 c5 56 8e 42 09 91 ef 9f 93 cb ad 6c 7b 65 f8 53 f1 d8 d9
1123+
12 5a 33 b8 b5 25 df 7b 37 9f e0 e2 4f b8 67 83 a3 2f cd 5d 41 42 4c 91
1124+
74 ef 2c ... 78 more bytes>
1125+
*/
1126+
```
1127+
See the OpenSSL [`SSL_export_keying_material`][] documentation for more
1128+
information.
1129+
10971130
### `tlsSocket.getTLSTicket()`
10981131
<!-- YAML
10991132
added: v0.11.4
@@ -1899,6 +1932,7 @@ where `secureSocket` has the same API as `pair.cleartext`.
18991932
[`'session'`]: #tls_event_session
19001933
[`--tls-cipher-list`]: cli.html#cli_tls_cipher_list_list
19011934
[`NODE_OPTIONS`]: cli.html#cli_node_options_options
1935+
[`SSL_export_keying_material`]: https://www.openssl.org/docs/man1.1.1/man3/SSL_export_keying_material.html
19021936
[`SSL_get_version`]: https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html
19031937
[`crypto.getCurves()`]: crypto.html#crypto_crypto_getcurves
19041938
[`net.createServer()`]: net.html#net_net_createserver_options_connectionlistener

lib/_tls_wrap.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,16 @@ const {
6666
ERR_TLS_RENEGOTIATION_DISABLED,
6767
ERR_TLS_REQUIRED_SERVER_NAME,
6868
ERR_TLS_SESSION_ATTACK,
69-
ERR_TLS_SNI_FROM_SERVER
69+
ERR_TLS_SNI_FROM_SERVER,
70+
ERR_TLS_INVALID_STATE
7071
} = codes;
7172
const { onpskexchange: kOnPskExchange } = internalBinding('symbols');
7273
const { getOptionValue } = require('internal/options');
73-
const { validateString, validateBuffer } = require('internal/validators');
74+
const {
75+
validateString,
76+
validateBuffer,
77+
validateUint32
78+
} = require('internal/validators');
7479
const traceTls = getOptionValue('--trace-tls');
7580
const tlsKeylog = getOptionValue('--tls-keylog');
7681
const { appendFile } = require('fs');
@@ -860,6 +865,18 @@ TLSSocket.prototype.renegotiate = function(options, callback) {
860865
return true;
861866
};
862867

868+
TLSSocket.prototype.exportKeyingMaterial = function(length, label, context) {
869+
validateUint32(length, 'length', true);
870+
validateString(label, 'label');
871+
if (context !== undefined)
872+
validateBuffer(context, 'context');
873+
874+
if (!this._secureEstablished)
875+
throw new ERR_TLS_INVALID_STATE();
876+
877+
return this._handle.exportKeyingMaterial(length, label, context);
878+
};
879+
863880
TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment(size) {
864881
return this._handle.setMaxSendFragment(size) === 1;
865882
};

lib/internal/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,8 @@ E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {
13141314
E('ERR_TLS_DH_PARAM_SIZE', 'DH parameter size %s is less than 2048', Error);
13151315
E('ERR_TLS_HANDSHAKE_TIMEOUT', 'TLS handshake timeout', Error);
13161316
E('ERR_TLS_INVALID_CONTEXT', '%s must be a SecureContext', TypeError),
1317+
E('ERR_TLS_INVALID_STATE', 'TLS socket connection must be securely established',
1318+
Error),
13171319
E('ERR_TLS_INVALID_PROTOCOL_VERSION',
13181320
'%j is not a valid %s TLS protocol version', TypeError);
13191321
E('ERR_TLS_PROTOCOL_VERSION_CONFLICT',

src/node_crypto.cc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,8 @@ void SSLWrap<Base>::AddMethods(Environment* env, Local<FunctionTemplate> t) {
17431743
env->SetProtoMethodNoSideEffect(t, "verifyError", VerifyError);
17441744
env->SetProtoMethodNoSideEffect(t, "getCipher", GetCipher);
17451745
env->SetProtoMethodNoSideEffect(t, "getSharedSigalgs", GetSharedSigalgs);
1746+
env->SetProtoMethodNoSideEffect(
1747+
t, "exportKeyingMaterial", ExportKeyingMaterial);
17461748
env->SetProtoMethod(t, "endParser", EndParser);
17471749
env->SetProtoMethod(t, "certCbDone", CertCbDone);
17481750
env->SetProtoMethod(t, "renegotiate", Renegotiate);
@@ -2772,6 +2774,44 @@ void SSLWrap<Base>::GetSharedSigalgs(const FunctionCallbackInfo<Value>& args) {
27722774
Array::New(env->isolate(), ret_arr.out(), ret_arr.length()));
27732775
}
27742776

2777+
template <class Base>
2778+
void SSLWrap<Base>::ExportKeyingMaterial(
2779+
const FunctionCallbackInfo<Value>& args) {
2780+
CHECK(args[0]->IsInt32());
2781+
CHECK(args[1]->IsString());
2782+
2783+
Base* w;
2784+
ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder());
2785+
Environment* env = w->ssl_env();
2786+
2787+
uint32_t olen = args[0].As<Uint32>()->Value();
2788+
node::Utf8Value label(env->isolate(), args[1]);
2789+
2790+
AllocatedBuffer out = env->AllocateManaged(olen);
2791+
2792+
ByteSource key;
2793+
2794+
int useContext = 0;
2795+
if (!args[2]->IsNull() && Buffer::HasInstance(args[2])) {
2796+
key = ByteSource::FromBuffer(args[2]);
2797+
2798+
useContext = 1;
2799+
}
2800+
2801+
if (SSL_export_keying_material(w->ssl_.get(),
2802+
reinterpret_cast<unsigned char*>(out.data()),
2803+
olen,
2804+
*label,
2805+
label.length(),
2806+
reinterpret_cast<const unsigned char*>(
2807+
key.get()),
2808+
key.size(),
2809+
useContext) != 1) {
2810+
return ThrowCryptoError(env, ERR_get_error(), "SSL_export_keying_material");
2811+
}
2812+
2813+
args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked());
2814+
}
27752815

27762816
template <class Base>
27772817
void SSLWrap<Base>::GetProtocol(const FunctionCallbackInfo<Value>& args) {

src/node_crypto.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ class SSLWrap {
263263
static void VerifyError(const v8::FunctionCallbackInfo<v8::Value>& args);
264264
static void GetCipher(const v8::FunctionCallbackInfo<v8::Value>& args);
265265
static void GetSharedSigalgs(const v8::FunctionCallbackInfo<v8::Value>& args);
266+
static void ExportKeyingMaterial(
267+
const v8::FunctionCallbackInfo<v8::Value>& args);
266268
static void EndParser(const v8::FunctionCallbackInfo<v8::Value>& args);
267269
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
268270
static void Renegotiate(const v8::FunctionCallbackInfo<v8::Value>& args);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict';
2+
3+
// Test return value of tlsSocket.exportKeyingMaterial
4+
5+
const common = require('../common');
6+
7+
if (!common.hasCrypto)
8+
common.skip('missing crypto');
9+
10+
const assert = require('assert');
11+
const net = require('net');
12+
const tls = require('tls');
13+
const fixtures = require('../common/fixtures');
14+
15+
const key = fixtures.readKey('agent1-key.pem');
16+
const cert = fixtures.readKey('agent1-cert.pem');
17+
18+
const server = net.createServer(common.mustCall((s) => {
19+
const tlsSocket = new tls.TLSSocket(s, {
20+
isServer: true,
21+
server: server,
22+
secureContext: tls.createSecureContext({ key, cert })
23+
});
24+
25+
assert.throws(() => {
26+
tlsSocket.exportKeyingMaterial(128, 'label');
27+
}, {
28+
name: 'Error',
29+
message: 'TLS socket connection must be securely established',
30+
code: 'ERR_TLS_INVALID_STATE'
31+
});
32+
33+
tlsSocket.on('secure', common.mustCall(() => {
34+
const label = 'client finished';
35+
36+
const validKeyingMaterial = tlsSocket.exportKeyingMaterial(128, label);
37+
assert.strictEqual(validKeyingMaterial.length, 128);
38+
39+
const validKeyingMaterialWithContext = tlsSocket
40+
.exportKeyingMaterial(128, label, Buffer.from([0, 1, 2, 3]));
41+
assert.strictEqual(validKeyingMaterialWithContext.length, 128);
42+
43+
// Ensure providing a context results in a different key than without
44+
assert.notStrictEqual(validKeyingMaterial, validKeyingMaterialWithContext);
45+
46+
const validKeyingMaterialWithEmptyContext = tlsSocket
47+
.exportKeyingMaterial(128, label, Buffer.from([]));
48+
assert.strictEqual(validKeyingMaterialWithEmptyContext.length, 128);
49+
50+
assert.throws(() => {
51+
tlsSocket.exportKeyingMaterial(128, label, 'stringAsContextNotSupported');
52+
}, {
53+
name: 'TypeError',
54+
code: 'ERR_INVALID_ARG_TYPE'
55+
});
56+
57+
assert.throws(() => {
58+
tlsSocket.exportKeyingMaterial(128, label, 1234);
59+
}, {
60+
name: 'TypeError',
61+
code: 'ERR_INVALID_ARG_TYPE'
62+
});
63+
64+
assert.throws(() => {
65+
tlsSocket.exportKeyingMaterial(10, null);
66+
}, {
67+
name: 'TypeError',
68+
code: 'ERR_INVALID_ARG_TYPE'
69+
});
70+
71+
assert.throws(() => {
72+
tlsSocket.exportKeyingMaterial('length', 1234);
73+
}, {
74+
name: 'TypeError',
75+
code: 'ERR_INVALID_ARG_TYPE'
76+
});
77+
78+
assert.throws(() => {
79+
tlsSocket.exportKeyingMaterial(-3, 'a');
80+
}, {
81+
name: 'RangeError',
82+
code: 'ERR_OUT_OF_RANGE'
83+
});
84+
85+
assert.throws(() => {
86+
tlsSocket.exportKeyingMaterial(0, 'a');
87+
}, {
88+
name: 'RangeError',
89+
code: 'ERR_OUT_OF_RANGE'
90+
});
91+
92+
tlsSocket.end();
93+
server.close();
94+
}));
95+
})).listen(0, () => {
96+
const opts = {
97+
port: server.address().port,
98+
rejectUnauthorized: false
99+
};
100+
101+
tls.connect(opts, common.mustCall(function() { this.end(); }));
102+
});

0 commit comments

Comments
 (0)