-
Notifications
You must be signed in to change notification settings - Fork 30k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
tls: Add PSK support (v9.0.0-pre) #14978
Conversation
lib/_tls_common.js
Outdated
if (typeof ret.psk === 'string') { | ||
ret.psk = Buffer.from(ret.psk); | ||
} else if (!Buffer.isBuffer(ret.psk)) { | ||
throw new TypeError('Pre-shared key is not a string or buffer'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if the rest of this file has been converted to use the internal/errors mechanism, but new errors introduced definitely should.
lib/_tls_common.js
Outdated
throw new TypeError('Pre-shared key is not a string or buffer'); | ||
} | ||
if (ret.psk.length > maxPskLen) { | ||
throw new TypeError(`Pre-shared key exceed ${maxPskLen} bytes`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should likely be a RangeError
lib/_tls_common.js
Outdated
throw new TypeError('PSK identity is not a string'); | ||
} | ||
if (ret.identity.length > maxIdentLen) { | ||
throw new TypeError(`PSK identity exceeds ${maxIdentLen} bytes`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RangeError
src/node_crypto.cc
Outdated
void SecureContext::SetPskIdentity(const FunctionCallbackInfo<Value>& args) { | ||
SecureContext* wrap = Unwrap<SecureContext>(args.Holder()); | ||
|
||
if (args.Length() != 1 || !args[0]->IsString()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not convinced that this should error if more than 1 argument is provided. Perhaps just if (!args[0]->IsString()) {
src/node_crypto.cc
Outdated
return wrap->env()->ThrowTypeError("Argument must be a string"); | ||
} | ||
|
||
String::Utf8Value identity(args[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make sure the documentation indicates that the PSK should be UTF8 only.
test/parallel/test-tls-psk-client.js
Outdated
const s = tls.connect(common.PORT, { | ||
ciphers: CIPHERS, | ||
rejectUnauthorized: false, | ||
pskCallback: (hint) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit ... pskCallback(hint) {
I'll get these changes implemented right away, thanks @jasnell. |
/cc @nodejs/crypto |
@jasnell I updated my changes as suggested. I have a few questions though, I'll add comments to my commit. |
lib/_tls_common.js
Outdated
['string', 'buffer']); | ||
} | ||
if (ret.psk.length > maxPskLen) { | ||
throw new errors.RangeError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this the proper error to throw? There wasn't an existing error that satisfied the previous generic RangeError, so I just used ERR_ASSERTION
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, how is the formatting on these lines? It complies with eslint, but it's ugly as sin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! Some of the more code here uses deprecated APIs, it would be nice if we could avoid that. Let me know if you need any help with it!
doc/api/tls.md
Outdated
<string>}`. Note that PSK ciphers are disabled by default, and using | ||
TLS-PSK thus requires explicitly specifying a cipher suite with the | ||
`ciphers` option. Additionally, it may be necessary to disable | ||
`rejectUnauthorized` when not intending to use certificates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
“it may be necessary” sounds a bit vague … can you clarify when that is necessary?
lib/_tls_common.js
Outdated
|
||
if (typeof ret.psk === 'string') { | ||
ret.psk = Buffer.from(ret.psk); | ||
} else if (!Buffer.isBuffer(ret.psk)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use ArrayBuffer.isVew(ret.psk)
instead and adjust the thrown error accordingly? Like in
https://github.com/nodejs/node/pull/14807/files#diff-1c8a7b9222892c3457fa64cbbfab6573R56
lib/_tls_common.js
Outdated
// Only set for the client callback, which must provide an identity. | ||
if (maxIdentLen) { | ||
if (typeof ret.identity === 'string') { | ||
ret.identity = Buffer.from(ret.identity + '\0'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like it would be easier to pass a string to C++ instead? Am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, the C++ method is using the identity in its buffer form, so we could pass a string and then convert it to a buffer in C++. Since Buffer::Data
returns a char*
, that's directly compatible with identity
. I suppose we could convert the string
directly to a char*
as well. Seems like it would be about the same amount of work on either side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One difference that this is always creating new intermediate JS objects in the process, which is part of why we don’t usually do that (I guess)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, if we can avoid creating unnecessary (Buffer) objects and such in between then that would be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I'll get on this refactor in the morning.
src/node_crypto.cc
Outdated
Isolate* isolate = env->isolate(); | ||
|
||
Local<Value> argv[] = { | ||
String::NewFromUtf8(isolate, identity), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use the overload that uses v8::NewStringType
/returns a MaybeLocal
? This one is being deprecated by V8.
src/node_crypto.cc
Outdated
sc->object(), | ||
env->onpskexchange_string(), | ||
arraysize(argv), | ||
argv); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is only called synchronously, right (i.e. there is a JS stack somewhere below it)? In that case, it’s better to just use Function::Call
instead of MakeCallback
src/node_crypto.cc
Outdated
Local<Object> obj = ret.As<Object>(); | ||
|
||
Local<Value> psk_buf = obj->Get(env->psk_string()); | ||
assert(Buffer::HasInstance(psk_buf)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Can you use CHECK
instead of assert
? Also, this is equivalent to psk_buf->IsArrayBufferView()
, just btw.
src/node_crypto.cc
Outdated
Local<Value> psk_buf = obj->Get(env->psk_string()); | ||
assert(Buffer::HasInstance(psk_buf)); | ||
size_t psk_len = Buffer::Length(psk_buf); | ||
assert(psk_len <= max_psk_len); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: CHECK_LE
?
src/node_crypto.cc
Outdated
} | ||
Local<Object> obj = ret.As<Object>(); | ||
|
||
Local<Value> psk_buf = obj->Get(env->psk_string()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This variant of Get
is being deprecated as well, the replacement API basically looks like obj->Get(env->context(), env->psk_string()).ToLocalChecked()
in this case
} | ||
|
||
process.on('exit', () => { | ||
assert.strictEqual(serverConnections, 3); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a test utility that’s called common.mustCall()
and allows you to have a bit more code locality in tests; e.g. you could wrap the callback that increases serverConnections
in common.mustCall()
and wouldn’t need to have an explicit counter variable :)
if (!common.hasCrypto) { | ||
console.log('1..0 # Skipped: missing crypto'); | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just fyi, in most tests this looks like this by now:
node/test/parallel/test-benchmark-crypto.js
Lines 5 to 6 in d348512
if (!common.hasCrypto) | |
common.skip('missing crypto'); |
Thanks for the follow-up, I'll get these changes implemented. |
…g of pskCallback note.
I believe that all issues brought up by @addaleax have been resolved. Some code snippets were updated in both |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM if @nodejs/crypto is happy
src/node_crypto.cc
Outdated
Local<Value> argv[] = { | ||
String::NewFromUtf8(isolate, | ||
identity, | ||
String::kNormalString, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
v8::NewStringType::kNormal
(the overload that uses a Maybe – sorry, should have been clearer about that)
src/node_crypto.cc
Outdated
if (hint != nullptr) { | ||
argv[0] = String::NewFromUtf8(isolate, | ||
hint, | ||
String::kNormalString, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(ditto)
src/node_crypto.cc
Outdated
String::kNormalString, | ||
strlen(hint)); | ||
} | ||
Local<Value> value = sc->object()->Get(env->onpskexchange_string()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the deprecated Get()
overload (no context
argument)
src/node_crypto.cc
Outdated
size_t identity_len = Buffer::Length(identity_buf); | ||
CHECK_LE(identity_len, max_identity_len); | ||
memcpy(identity, Buffer::Data(identity_buf), identity_len); | ||
assert(identity[identity_len - 1] == '\0'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also a reason why using passing strings to C++ might be nice :)
Didn't realize I was using the wrong methods, I must have been looking at an old version of the V8 documentation. My |
Alright, TLS-PSK API now only accepts a hex-encoded string for |
lib/_tls_common.js
Outdated
@@ -132,6 +133,49 @@ exports.createSecureContext = function createSecureContext(options, context) { | |||
} | |||
} | |||
|
|||
if (options.pskCallback && c.context.enablePskCallback) { | |||
c.context.onpskexchange = (identity, maxPskLen, maxIdentLen) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should there be a check here to ensure that pskCallback
is a function?
lib/_tls_common.js
Outdated
if (options.pskCallback && c.context.enablePskCallback) { | ||
c.context.onpskexchange = (identity, maxPskLen, maxIdentLen) => { | ||
let ret = options.pskCallback(identity); | ||
if (typeof ret !== 'object') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if ret === null
? (I know the check before looks for that, but we should be clear here... just to be safe :-) ...)
lib/_tls_common.js
Outdated
|
||
return ret; | ||
}; | ||
c.context.enablePskCallback(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm reading the #ifdef
statements write in the source below, there's a chance the enablePskCallback()
will not be defined if OPENSSL_PSK_SUPPORT
is switched off, correct? I may be missing something because I haven't done a complete walkthrough, but if that's the case, I'd recommend gating these with a flag in node_config.cc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There shouldn't be a chance that enablePskCallback
is enabled, since the definition of that is also inside of an #ifdef
, see here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I see, I was misreading the location of the closing brackets. This call is made inside the if that checks c.context.enablePskCallback
is not falsy. :-)
src/node_crypto.cc
Outdated
|
||
MaybeLocal<Value> maybe_ret; | ||
if (value->IsFunction()) { | ||
Local<Function> func = Local<Function>::Cast(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
value.As<Function>();
?
HandleScope scope(isolate); | ||
|
||
Local<Value> argv[] = { | ||
Null(isolate), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be helpful to add a comment here reminding folks that this is a error-back style callback, which explains why the first argument is passed as a Null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So that's actually the object passed to the pskCallback
function, where the first argument (initially defined as Null
) is the hint provided by OpenSSL/TLS. The hint is an optional field, which is why we're not erroring when it's not provided.
|
||
function startTest() { | ||
function connectClient(options, next) { | ||
const client = tls.connect(common.PORT, 'localhost', options, () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/common.PORT
/0
|
||
client.end(TEST_DATA); | ||
|
||
client.on('data', (data) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps wrap this in a common.mustCallAtLeast()
assert.strictEqual(data.toString(), TEST_DATA); | ||
}); | ||
|
||
client.on('close', next); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
common.mustCall(next)
test/parallel/test-tls-psk-server.js
Outdated
let gotWorld = false; | ||
let opensslExitCode = -1; | ||
|
||
server.listen(common.PORT, () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/common.PORT
/0
note that in general we've been moving away from using common.PORT
in the parallel tests.
…g of pskCallback note.
…eaned up some formatting.
…hange` to use `Function::Call`. Updated PSK callbacks to use new/non-deprecated functions.
…f8`. Updated PSK callbacks to use `MaybeLocal` overload of Function::Call.
…ded some `mustCall` wrappers in TLS-PSK circuit test.
…d TLS-PSK callback to ensure that the returned value is not null.
… MaybeLocal to a Function.
… MaybeLocal to a Function.
…e the new error throwing format.
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. Co-authored-by: Chris Osborn <[email protected]> Co-authored-by: stephank <[email protected]> Co-authored-by: Denys Otrishko <[email protected]>
Alright, I believe I've rebased correctly (again), and cherry-picked @lundibundi's commit. Though, I'm not seeing my changes appear in the "Commits" or "Files changed" tabs. Is there a way to resolve that issue? |
@taylorzane you just had to reopen the PR. =) |
Ah, I see what you did. I didn't actually remove the previous commits, I missed that part of your comment. Let me see what I can do about this this morning. |
@taylorzane ping, will you have time for this? |
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
tls, crypto
This is an updated PR based off of #6701 that supports Node.js v9.0.0.
I understand that the Node.js team is waiting for FIPS support in OpenSSL-1.1, but this will get the ball rolling for TLS-PSK support in the latest Node.js versions.