Skip to content

crypto: handle OpenSSL error queue in CipherBase - #21288

Closed
tniessen wants to merge 3 commits into
nodejs:masterfrom
tniessen:crypto-fix-openssl-error-handling-in-cipherbase
Closed

crypto: handle OpenSSL error queue in CipherBase#21288
tniessen wants to merge 3 commits into
nodejs:masterfrom
tniessen:crypto-fix-openssl-error-handling-in-cipherbase

Conversation

@tniessen

Copy link
Copy Markdown
Member

This handles all errors produced by OpenSSL within the CipherBase class. API functions clear the error queue on return, utility functions such as InitAuthenticated() ensure that they do not add any new errors to the queue. Previously ignored return values are now being CHECK'd.

::Final does not clear the error queue as there is a custom error handler in place.

Note that #21287 is required for this to work, otherwise the CHECK in line 2597 will fail.

Fixes: #21281

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • commit message follows commit guidelines

This handles all errors produced by OpenSSL within the CipherBase
class. API functions clear the error queue on return, utility
functions such as InitAuthenticated() ensure that they do not add
any new errors to the queue. Previously ignored return values are
now being CHECK'd.
Fixes: nodejs#21281
Refs: nodejs#21287
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@tniessen sadly an error occured when I tried to trigger a build :(

@nodejs-github-botnodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. crypto Issues and PRs related to the crypto subsystem. labels Jun 12, 2018
@tniessen

Copy link
Copy Markdown
MemberAuthor

cc @nodejs/crypto

Comment threadsrc/node_crypto.cc
bool CipherBase::InitAuthenticated(const char* cipher_type, int iv_len,
unsigned int auth_tag_len) {
CHECK(IsAuthenticatedMode());
MarkPopErrorOnReturn mark_pop_error_on_return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own understanding: MarkPopErrorOnReturn basically means that OpenSSL errors inside this scope are swallowed/ignored?

@tniessentniessenJun 14, 2018

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly, all errors that occur in this scope are dropped once the destructor is called (unless they have been handled specifically).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And now that we're handling the errors manually, Node will be throwing an error instead of just crashing, I suppose?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And because we're manually handling the errors now, it's better because we'll throw an error that you could catch and what not instead of just crashing, right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't throw more errors than before (except for ::Init), it will just make sure that if it throws, it also clears the internal error queue of OpenSSL.

@addaleax

Copy link
Copy Markdown
Member

Travis CI failure looks related:


not ok 346 parallel/test-crypto-binary-default
---
duration_ms: 0.418
severity: crashed
exitcode: -6
stack: |-
out/Release/node[60848]: ../src/node_crypto.cc:2598:void node::crypto::CipherBase::Init(const char *, const char *, int, unsigned int): Assertion `EVP_CipherInit_ex(ctx_.get(), cipher, nullptr, nullptr, nullptr, encrypt)' failed.
1: node::Abort() [out/Release/node]
2: 0x815a7d [out/Release/node]
3: node::crypto::CipherBase::Init(char const*, char const*, int, unsigned int) [out/Release/node]
4: node::crypto::CipherBase::Init(v8::FunctionCallbackInfo<v8::Value> const&) [out/Release/node]
5: v8::internal::FunctionCallbackArguments::Call(v8::internal::CallHandlerInfo*) [out/Release/node]
6: 0xa39f4f [out/Release/node]
7: 0xa39616 [out/Release/node]
8: 0x2be3fc7041bd
...

@tniessen

Copy link
Copy Markdown
MemberAuthor

@addaleax Yes, see above:

Note that #21287 is required for this to work, otherwise the CHECK in line 2597 will fail.

As far as I can tell, #15037 should have set the flag before calling EVP_CipherInit_Ex, otherwise, the call will fail in some modes. We never noticed because we didn't perform proper error-checking and OpenSSL left the context in a partially initialized state which was enough for the rest of the API to work. This PR adds a CHECK to ensure that the initialization actually succeeds from OpenSSL's perspective.

Comment threadsrc/node_crypto.cc Outdated
ctx_.reset(EVP_CIPHER_CTX_new());
const bool encrypt = (kind_ == kCipher);
EVP_CipherInit_ex(ctx_.get(), cipher, nullptr, nullptr, nullptr, encrypt);
CHECK(EVP_CipherInit_ex(ctx_.get(), cipher, nullptr,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EVP_CipherInit_ex() can fail for legitimate reasons (e.g. an engine that fails to load) so a CHECK is not appropriate.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the old behavior (= no error checking) isn't appropriate either?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, errors should be reported.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would make this semver-major I guess, so I'll probably just leave the CHECKs out for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mwah, I don't think better error checking constitutes semver-major. The kind of bugs it'd flush out are programmer bugs and environmental issues.

Comment threadsrc/node_crypto.cc Outdated
bool CipherBase::SetAAD(const char* data, unsigned int len, int plaintext_len) {
if (!ctx_ || !IsAuthenticatedMode())
return false;
ClearErrorOnReturn clear_error_on_return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should ideally be MarkPopErrorOnReturn. ClearErrorOnReturn is a blunt hammer and might swallow genuine errors.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Point taken, I can change it. In practice, the error stack should be empty when the JS layer calls into the C++ layer though, right?

@bnoordhuisbnoordhuisJun 14, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, except for the following:

  1. It can mess up C++ -> C++ calls when the caller also manipulates the error stack.
  2. I know of at least one add-on that leaves openssl errors on the stack for some time.

Instead of clearing all errors, mark and pop only errors that were
added within the current scope.
@tniessen

Copy link
Copy Markdown
MemberAuthor

@bnoordhuis Please take another look.

@ryzokukenryzokuken left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but node_crypto.cc seems to have become rather long, don't you think? Do you believe we should try break it into smaller parts (and even namespaces) at this point?

Comment threadsrc/node_crypto.cc
ctx_.reset(EVP_CIPHER_CTX_new());
const bool encrypt = (kind_ == kCipher);
EVP_CipherInit_ex(ctx_.get(), cipher, nullptr, nullptr, nullptr, encrypt);
if (1 != EVP_CipherInit_ex(ctx_.get(), cipher, nullptr,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit, but shouldn't this be done the other way around? Eg: EVP_CipherInit_ex(...) != 1? Would love to hear why you chose this over the former.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't know we had a preference. I usually do the opposite, but I think having != 1 at the end of a multi-line expression looks weird.

Fun fact, this style was commonly used to prevent accidental assignment of variables / pointers in boolean expressions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because people would accidentally type = instead of == all the times? Sounds more likely in the era of butterfly keyboards, heh. If it makes sense for you to keep this, feel free to do so. I agree wholeheartedly that != 1 looks weird when in a multiline statement, just wanted to make sure we're not going against the style guide here.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, even though I personally still prefer var == constant, the only reason here is that it looks weird. If no one has a strong opinion against this, I'd prefer to keep it this way.

Comment threadsrc/node_crypto.cc
reinterpret_cast<unsigned char*>(key),
reinterpret_cast<unsigned char*>(iv),
encrypt);
if (1 != EVP_CipherInit_ex(ctx_.get(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Comment threadsrc/node_crypto.cc

const bool encrypt = (kind_ == kCipher);
EVP_CipherInit_ex(ctx_.get(), cipher, nullptr, nullptr, nullptr, encrypt);
if (1 != EVP_CipherInit_ex(ctx_.get(), cipher, nullptr,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Comment threadsrc/node_crypto.cc
reinterpret_cast<const unsigned char*>(key),
reinterpret_cast<const unsigned char*>(iv),
encrypt);
if (1 != EVP_CipherInit_ex(ctx_.get(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting boring, isn't it? 😛

Comment threadsrc/node_crypto.cc
bool CipherBase::InitAuthenticated(const char* cipher_type, int iv_len,
unsigned int auth_tag_len) {
CHECK(IsAuthenticatedMode());
MarkPopErrorOnReturn mark_pop_error_on_return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And now that we're handling the errors manually, Node will be throwing an error instead of just crashing, I suppose?

Comment threadsrc/node_crypto.cc
reinterpret_cast<unsigned char*>(key),
reinterpret_cast<unsigned char*>(iv),
encrypt);
if (1 != EVP_CipherInit_ex(ctx_.get(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Comment threadsrc/node_crypto.cc

const bool encrypt = (kind_ == kCipher);
EVP_CipherInit_ex(ctx_.get(), cipher, nullptr, nullptr, nullptr, encrypt);
if (1 != EVP_CipherInit_ex(ctx_.get(), cipher, nullptr,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Comment threadsrc/node_crypto.cc
reinterpret_cast<const unsigned char*>(key),
reinterpret_cast<const unsigned char*>(iv),
encrypt);
if (1 != EVP_CipherInit_ex(ctx_.get(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

God, isn't this repetitive 😅

Comment threadsrc/node_crypto.cc
bool CipherBase::InitAuthenticated(const char* cipher_type, int iv_len,
unsigned int auth_tag_len) {
CHECK(IsAuthenticatedMode());
MarkPopErrorOnReturn mark_pop_error_on_return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And because we're manually handling the errors now, it's better because we'll throw an error that you could catch and what not instead of just crashing, right?

Comment threadsrc/node_crypto.cc
EVP_CTRL_GCM_SET_TAG,
auth_tag_len_,
reinterpret_cast<unsigned char*>(auth_tag_));
CHECK(EVP_CIPHER_CTX_ctrl(ctx_.get(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit intrigued.

https://www.openssl.org/docs/man1.0.2/crypto/EVP_EncryptInit.html doesn't show EVP_CIPHER_CTX_ctrl returning anything. If it's actually returning void (I hope not), the would this work?

If it doesn't return void, shouldn't we use an if statement here as well and throw something nice instead of crashing the process?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only reasons I can think of right now are a bug (e.g. because we permitted the call in an invalid state) or a memory allocation failure, and those are generally not handled within our APIs.

@ryzokukenryzokuken left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@tniessen

Copy link
Copy Markdown
MemberAuthor

CI: https://ci.nodejs.org/job/node-test-pull-request/15753/

Pinging @nodejs/crypto one last time.

@tniessen

Copy link
Copy Markdown
MemberAuthor

Landed in 85b0f16.

tniessen added a commit that referenced this pull request Jul 12, 2018
This handles all errors produced by OpenSSL within the CipherBase
class. API functions ensure that they do not add any new errors to the
error queue. Also adds a couple of CHECKs and throws under certain
conditions.
PR-URL: #21288Fixes: #21281
Refs: #21287
Reviewed-By: Ujjwal Sharma <usharma1998@gmail.com>
targos pushed a commit that referenced this pull request Jul 14, 2018
This handles all errors produced by OpenSSL within the CipherBase
class. API functions ensure that they do not add any new errors to the
error queue. Also adds a couple of CHECKs and throws under certain
conditions.
PR-URL: #21288Fixes: #21281
Refs: #21287
Reviewed-By: Ujjwal Sharma <usharma1998@gmail.com>
@targostargos mentioned this pull request Jul 17, 2018
hugo19941994 added a commit to Telefonica/node-jwt-utils that referenced this pull request May 13, 2020
Squashed commit of the following:
commit a61cace46f4f745283177a6ca4551e6237aa4d8a
Author: Hugo Ferrando Seage <hugoseage@gmail.com>
Date: Wed May 13 15:33:19 2020 +0200
Downgrade eslint to version 6.8.0
Version 7 dropped support for node 8
commit f08aa1e125738fe00183306f925d4e2ecf9e5704
Author: Hugo Ferrando Seage <hugoseage@gmail.com>
Date: Wed May 13 14:52:18 2020 +0200
FIX unit tests
- Made environment.js work with the newer mocha version
- Remove all Buffer constructors
- Change test order to fix broken tests in node < 10.7
- nodejs/node#21288
- Changed minimum node version to 8, due to the updated dependencies
commit ce1f40d4e5ae79f4bcf6a3cca0f914ef51effd3b
Author: Hugo Ferrando Seage <hugoseage@gmail.com>
Date: Wed May 13 14:52:04 2020 +0200
Relint jwt-utils.js with newer eslint rules
commit 59940f5552dbfb8353e71250b1a0d3038913a17e
Author: Hugo Ferrando Seage <hugoseage@gmail.com>
Date: Wed May 13 14:50:51 2020 +0200
Replace new Buffer with Buffer.from and Buffer.alloc
The Buffer constructor was deprecated: https://nodejs.org/en/docs/guides/buffer-constructor-deprecation/
commit b46245788a6235b0490f9902e9218945cb6d585c
Author: Hugo Ferrando Seage <hugoseage@gmail.com>
Date: Wed May 13 14:48:56 2020 +0200
Remove travis and coveralls badges
Also removed the .travis file and npm script. Neither service is
executing their pipelines for this repo anymore
commit 23f537ad6fdc4b8924add6feb16628512529a47a
Author: Hugo Ferrando Seage <hugoseage@gmail.com>
Date: Wed May 13 14:48:42 2020 +0200
Integrate jscsrc rules into eslintrc
commit c0e23b53a27d82d14430d05b63426dcae868cfd9
Author: Hugo Ferrando Seage <hugoseage@gmail.com>
Date: Wed May 13 14:44:52 2020 +0200
CHORE update dependencies
- coveralls removed as the pipeline is not executed anymore
- jscs removed as it was deprecated
- added eslint-google dependency to replace jscs with eslint
- jwa NOT updated as it would break compatibility
- therror NOT updated
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++Issues and PRs that require attention from people who are familiar with C++.cryptoIssues and PRs related to the crypto subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calling crypto.createCipheriv with a key of invalid length can cause errors in other consumers of the OpenSSL error queue

5 participants

@tniessen@nodejs-github-bot@addaleax@bnoordhuis@ryzokuken