Skip to content

Commit

Permalink
tls: expose SSL_export_keying_material
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
simllll authored and targos committed Apr 28, 2020
1 parent 02de66a commit 6348fae
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 2 deletions.
9 changes: 9 additions & 0 deletions doc/api/errors.md
Expand Up @@ -1850,6 +1850,15 @@ added: v12.16.0

The context must be a `SecureContext`.

<a id="ERR_TLS_INVALID_STATE"></a>
### `ERR_TLS_INVALID_STATE`
<!-- YAML
added: REPLACEME
-->

The TLS socket must be connected and securily established. Ensure the 'secure'
event is emitted, before you continue.

<a id="ERR_TLS_INVALID_PROTOCOL_METHOD"></a>
### `ERR_TLS_INVALID_PROTOCOL_METHOD`

Expand Down
34 changes: 34 additions & 0 deletions doc/api/tls.md
Expand Up @@ -1094,6 +1094,39 @@ See
[SSL_get_shared_sigalgs](https://www.openssl.org/docs/man1.1.1/man3/SSL_get_shared_sigalgs.html)
for more information.

### `tlsSocket.exportKeyingMaterial(length, label[, context])`
<!-- YAML
added: REPLACEME
-->

* `length` {number} number of bytes to retrieve from keying material
* `label` {string} an application specific label, typically this will be a
value from the
[IANA Exporter Label Registry](https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#exporter-labels).
* `context` {Buffer} Optionally provide a context.

* Returns: {Buffer} requested bytes of the keying material

Keying material is used for validations to prevent different kind of attacks in
network protocols, for example in the specifications of IEEE 802.1X.

Example

```js
const keyingMaterial = tlsSocket.exportKeyingMaterial(
128,
'client finished');

/**
Example return value of keyingMaterial:
<Buffer 76 26 af 99 c5 56 8e 42 09 91 ef 9f 93 cb ad 6c 7b 65 f8 53 f1 d8 d9
12 5a 33 b8 b5 25 df 7b 37 9f e0 e2 4f b8 67 83 a3 2f cd 5d 41 42 4c 91
74 ef 2c ... 78 more bytes>
*/
```
See the OpenSSL [`SSL_export_keying_material`][] documentation for more
information.

### `tlsSocket.getTLSTicket()`
<!-- YAML
added: v0.11.4
Expand Down Expand Up @@ -1899,6 +1932,7 @@ where `secureSocket` has the same API as `pair.cleartext`.
[`'session'`]: #tls_event_session
[`--tls-cipher-list`]: cli.html#cli_tls_cipher_list_list
[`NODE_OPTIONS`]: cli.html#cli_node_options_options
[`SSL_export_keying_material`]: https://www.openssl.org/docs/man1.1.1/man3/SSL_export_keying_material.html
[`SSL_get_version`]: https://www.openssl.org/docs/man1.1.1/man3/SSL_get_version.html
[`crypto.getCurves()`]: crypto.html#crypto_crypto_getcurves
[`net.createServer()`]: net.html#net_net_createserver_options_connectionlistener
Expand Down
21 changes: 19 additions & 2 deletions lib/_tls_wrap.js
Expand Up @@ -65,11 +65,16 @@ const {
ERR_TLS_RENEGOTIATION_DISABLED,
ERR_TLS_REQUIRED_SERVER_NAME,
ERR_TLS_SESSION_ATTACK,
ERR_TLS_SNI_FROM_SERVER
ERR_TLS_SNI_FROM_SERVER,
ERR_TLS_INVALID_STATE
} = codes;
const { onpskexchange: kOnPskExchange } = internalBinding('symbols');
const { getOptionValue } = require('internal/options');
const { validateString, validateBuffer } = require('internal/validators');
const {
validateString,
validateBuffer,
validateUint32
} = require('internal/validators');
const traceTls = getOptionValue('--trace-tls');
const tlsKeylog = getOptionValue('--tls-keylog');
const { appendFile } = require('fs');
Expand Down Expand Up @@ -859,6 +864,18 @@ TLSSocket.prototype.renegotiate = function(options, callback) {
return true;
};

TLSSocket.prototype.exportKeyingMaterial = function(length, label, context) {
validateUint32(length, 'length', true);
validateString(label, 'label');
if (context !== undefined)
validateBuffer(context, 'context');

if (!this._secureEstablished)
throw new ERR_TLS_INVALID_STATE();

return this._handle.exportKeyingMaterial(length, label, context);
};

TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment(size) {
return this._handle.setMaxSendFragment(size) === 1;
};
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Expand Up @@ -1308,6 +1308,8 @@ E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) {
E('ERR_TLS_DH_PARAM_SIZE', 'DH parameter size %s is less than 2048', Error);
E('ERR_TLS_HANDSHAKE_TIMEOUT', 'TLS handshake timeout', Error);
E('ERR_TLS_INVALID_CONTEXT', '%s must be a SecureContext', TypeError),
E('ERR_TLS_INVALID_STATE', 'TLS socket connection must be securely established',
Error),
E('ERR_TLS_INVALID_PROTOCOL_VERSION',
'%j is not a valid %s TLS protocol version', TypeError);
E('ERR_TLS_PROTOCOL_VERSION_CONFLICT',
Expand Down
40 changes: 40 additions & 0 deletions src/node_crypto.cc
Expand Up @@ -1701,6 +1701,8 @@ void SSLWrap<Base>::AddMethods(Environment* env, Local<FunctionTemplate> t) {
env->SetProtoMethodNoSideEffect(t, "verifyError", VerifyError);
env->SetProtoMethodNoSideEffect(t, "getCipher", GetCipher);
env->SetProtoMethodNoSideEffect(t, "getSharedSigalgs", GetSharedSigalgs);
env->SetProtoMethodNoSideEffect(
t, "exportKeyingMaterial", ExportKeyingMaterial);
env->SetProtoMethod(t, "endParser", EndParser);
env->SetProtoMethod(t, "certCbDone", CertCbDone);
env->SetProtoMethod(t, "renegotiate", Renegotiate);
Expand Down Expand Up @@ -2216,6 +2218,44 @@ void SSLWrap<Base>::GetSharedSigalgs(const FunctionCallbackInfo<Value>& args) {
Array::New(env->isolate(), ret_arr.out(), ret_arr.length()));
}

template <class Base>
void SSLWrap<Base>::ExportKeyingMaterial(
const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsInt32());
CHECK(args[1]->IsString());

Base* w;
ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder());
Environment* env = w->ssl_env();

uint32_t olen = args[0].As<Uint32>()->Value();
node::Utf8Value label(env->isolate(), args[1]);

AllocatedBuffer out = env->AllocateManaged(olen);

ByteSource key;

int useContext = 0;
if (!args[2]->IsNull() && Buffer::HasInstance(args[2])) {
key = ByteSource::FromBuffer(args[2]);

useContext = 1;
}

if (SSL_export_keying_material(w->ssl_.get(),
reinterpret_cast<unsigned char*>(out.data()),
olen,
*label,
label.length(),
reinterpret_cast<const unsigned char*>(
key.get()),
key.size(),
useContext) != 1) {
return ThrowCryptoError(env, ERR_get_error(), "SSL_export_keying_material");
}

args.GetReturnValue().Set(out.ToBuffer().ToLocalChecked());
}

template <class Base>
void SSLWrap<Base>::GetProtocol(const FunctionCallbackInfo<Value>& args) {
Expand Down
2 changes: 2 additions & 0 deletions src/node_crypto.h
Expand Up @@ -251,6 +251,8 @@ class SSLWrap {
static void VerifyError(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetCipher(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetSharedSigalgs(const v8::FunctionCallbackInfo<v8::Value>& args);
static void ExportKeyingMaterial(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void EndParser(const v8::FunctionCallbackInfo<v8::Value>& args);
static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Renegotiate(const v8::FunctionCallbackInfo<v8::Value>& args);
Expand Down
102 changes: 102 additions & 0 deletions test/parallel/test-tls-exportkeyingmaterial.js
@@ -0,0 +1,102 @@
'use strict';

// Test return value of tlsSocket.exportKeyingMaterial

const common = require('../common');

if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const net = require('net');
const tls = require('tls');
const fixtures = require('../common/fixtures');

const key = fixtures.readKey('agent1-key.pem');
const cert = fixtures.readKey('agent1-cert.pem');

const server = net.createServer(common.mustCall((s) => {
const tlsSocket = new tls.TLSSocket(s, {
isServer: true,
server: server,
secureContext: tls.createSecureContext({ key, cert })
});

assert.throws(() => {
tlsSocket.exportKeyingMaterial(128, 'label');
}, {
name: 'Error',
message: 'TLS socket connection must be securely established',
code: 'ERR_TLS_INVALID_STATE'
});

tlsSocket.on('secure', common.mustCall(() => {
const label = 'client finished';

const validKeyingMaterial = tlsSocket.exportKeyingMaterial(128, label);
assert.strictEqual(validKeyingMaterial.length, 128);

const validKeyingMaterialWithContext = tlsSocket
.exportKeyingMaterial(128, label, Buffer.from([0, 1, 2, 3]));
assert.strictEqual(validKeyingMaterialWithContext.length, 128);

// Ensure providing a context results in a different key than without
assert.notStrictEqual(validKeyingMaterial, validKeyingMaterialWithContext);

const validKeyingMaterialWithEmptyContext = tlsSocket
.exportKeyingMaterial(128, label, Buffer.from([]));
assert.strictEqual(validKeyingMaterialWithEmptyContext.length, 128);

assert.throws(() => {
tlsSocket.exportKeyingMaterial(128, label, 'stringAsContextNotSupported');
}, {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE'
});

assert.throws(() => {
tlsSocket.exportKeyingMaterial(128, label, 1234);
}, {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE'
});

assert.throws(() => {
tlsSocket.exportKeyingMaterial(10, null);
}, {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE'
});

assert.throws(() => {
tlsSocket.exportKeyingMaterial('length', 1234);
}, {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE'
});

assert.throws(() => {
tlsSocket.exportKeyingMaterial(-3, 'a');
}, {
name: 'RangeError',
code: 'ERR_OUT_OF_RANGE'
});

assert.throws(() => {
tlsSocket.exportKeyingMaterial(0, 'a');
}, {
name: 'RangeError',
code: 'ERR_OUT_OF_RANGE'
});

tlsSocket.end();
server.close();
}));
})).listen(0, () => {
const opts = {
port: server.address().port,
rejectUnauthorized: false
};

tls.connect(opts, common.mustCall(function() { this.end(); }));
});

0 comments on commit 6348fae

Please sign in to comment.