Skip to content

Commit 6348fae

Browse files
simlllltargos
authored andcommitted
tls: expose SSL_export_keying_material
Fixes: #31802 PR-URL: #31814 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Tobias Nießen <[email protected]>
1 parent 02de66a commit 6348fae

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
@@ -1850,6 +1850,15 @@ added: v12.16.0
18501850

18511851
The context must be a `SecureContext`.
18521852

1853+
<a id="ERR_TLS_INVALID_STATE"></a>
1854+
### `ERR_TLS_INVALID_STATE`
1855+
<!-- YAML
1856+
added: REPLACEME
1857+
-->
1858+
1859+
The TLS socket must be connected and securily established. Ensure the 'secure'
1860+
event is emitted, before you continue.
1861+
18531862
<a id="ERR_TLS_INVALID_PROTOCOL_METHOD"></a>
18541863
### `ERR_TLS_INVALID_PROTOCOL_METHOD`
18551864

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
@@ -65,11 +65,16 @@ const {
6565
ERR_TLS_RENEGOTIATION_DISABLED,
6666
ERR_TLS_REQUIRED_SERVER_NAME,
6767
ERR_TLS_SESSION_ATTACK,
68-
ERR_TLS_SNI_FROM_SERVER
68+
ERR_TLS_SNI_FROM_SERVER,
69+
ERR_TLS_INVALID_STATE
6970
} = codes;
7071
const { onpskexchange: kOnPskExchange } = internalBinding('symbols');
7172
const { getOptionValue } = require('internal/options');
72-
const { validateString, validateBuffer } = require('internal/validators');
73+
const {
74+
validateString,
75+
validateBuffer,
76+
validateUint32
77+
} = require('internal/validators');
7378
const traceTls = getOptionValue('--trace-tls');
7479
const tlsKeylog = getOptionValue('--tls-keylog');
7580
const { appendFile } = require('fs');
@@ -859,6 +864,18 @@ TLSSocket.prototype.renegotiate = function(options, callback) {
859864
return true;
860865
};
861866

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

lib/internal/errors.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,6 +1308,8 @@ E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {
13081308
E('ERR_TLS_DH_PARAM_SIZE', 'DH parameter size %s is less than 2048', Error);
13091309
E('ERR_TLS_HANDSHAKE_TIMEOUT', 'TLS handshake timeout', Error);
13101310
E('ERR_TLS_INVALID_CONTEXT', '%s must be a SecureContext', TypeError),
1311+
E('ERR_TLS_INVALID_STATE', 'TLS socket connection must be securely established',
1312+
Error),
13111313
E('ERR_TLS_INVALID_PROTOCOL_VERSION',
13121314
'%j is not a valid %s TLS protocol version', TypeError);
13131315
E('ERR_TLS_PROTOCOL_VERSION_CONFLICT',

src/node_crypto.cc

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1701,6 +1701,8 @@ void SSLWrap<Base>::AddMethods(Environment* env, Local<FunctionTemplate> t) {
17011701
env->SetProtoMethodNoSideEffect(t, "verifyError", VerifyError);
17021702
env->SetProtoMethodNoSideEffect(t, "getCipher", GetCipher);
17031703
env->SetProtoMethodNoSideEffect(t, "getSharedSigalgs", GetSharedSigalgs);
1704+
env->SetProtoMethodNoSideEffect(
1705+
t, "exportKeyingMaterial", ExportKeyingMaterial);
17041706
env->SetProtoMethod(t, "endParser", EndParser);
17051707
env->SetProtoMethod(t, "certCbDone", CertCbDone);
17061708
env->SetProtoMethod(t, "renegotiate", Renegotiate);
@@ -2216,6 +2218,44 @@ void SSLWrap<Base>::GetSharedSigalgs(const FunctionCallbackInfo<Value>& args) {
22162218
Array::New(env->isolate(), ret_arr.out(), ret_arr.length()));
22172219
}
22182220

2221+
template <class Base>
2222+
void SSLWrap<Base>::ExportKeyingMaterial(
2223+
const FunctionCallbackInfo<Value>& args) {
2224+
CHECK(args[0]->IsInt32());
2225+
CHECK(args[1]->IsString());
2226+
2227+
Base* w;
2228+
ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder());
2229+
Environment* env = w->ssl_env();
2230+
2231+
uint32_t olen = args[0].As<Uint32>()->Value();
2232+
node::Utf8Value label(env->isolate(), args[1]);
2233+
2234+
AllocatedBuffer out = env->AllocateManaged(olen);
2235+
2236+
ByteSource key;
2237+
2238+
int useContext = 0;
2239+
if (!args[2]->IsNull() && Buffer::HasInstance(args[2])) {
2240+
key = ByteSource::FromBuffer(args[2]);
2241+
2242+
useContext = 1;
2243+
}
2244+
2245+
if (SSL_export_keying_material(w->ssl_.get(),
2246+
reinterpret_cast<unsigned char*>(out.data()),
2247+
olen,
2248+
*label,
2249+
label.length(),
2250+
reinterpret_cast<const unsigned char*>(
2251+
key.get()),
2252+
key.size(),
2253+
useContext) != 1) {
2254+
return ThrowCryptoError(env, ERR_get_error(), "SSL_export_keying_material");
2255+
}
2256+
2257+
args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked());
2258+
}
22192259

22202260
template <class Base>
22212261
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
@@ -251,6 +251,8 @@ class SSLWrap {
251251
static void VerifyError(const v8::FunctionCallbackInfo<v8::Value>& args);
252252
static void GetCipher(const v8::FunctionCallbackInfo<v8::Value>& args);
253253
static void GetSharedSigalgs(const v8::FunctionCallbackInfo<v8::Value>& args);
254+
static void ExportKeyingMaterial(
255+
const v8::FunctionCallbackInfo<v8::Value>& args);
254256
static void EndParser(const v8::FunctionCallbackInfo<v8::Value>& args);
255257
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
256258
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)