★ wanayoo — archive 1999 https://github.com/nodejs/node/pull/21288Nouvelle recherche | Portail wanayoo
Skip to content
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

crypto: handle OpenSSL error queue in CipherBase #21288

Conversation

Projects
None yet
5 participants
@tniessen
Copy link
Member

tniessen commented Jun 12, 2018

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
crypto: handle OpenSSL error queue in CipherBase
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: #21281
Refs: #21287
@nodejs-github-bot

This comment has been minimized.

Copy link

nodejs-github-bot commented Jun 12, 2018

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

@tniessen

This comment has been minimized.

Copy link
Member Author

tniessen commented Jun 12, 2018

@@ -2749,6 +2753,7 @@ static bool IsValidGCMTagLength(unsigned int tag_len) {
bool CipherBase::InitAuthenticated(const char* cipher_type, int iv_len,
unsigned int auth_tag_len) {
CHECK(IsAuthenticatedMode());
MarkPopErrorOnReturn mark_pop_error_on_return;

This comment has been minimized.

@addaleax

addaleax Jun 14, 2018

Member

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

This comment has been minimized.

@tniessen

tniessen Jun 14, 2018 •

Author Member

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

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

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

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

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?

This comment has been minimized.

@tniessen

tniessen Jul 1, 2018

Author Member

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

This comment has been minimized.

Copy link
Member

addaleax commented Jun 14, 2018

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

This comment has been minimized.

Copy link
Member Author

tniessen commented Jun 14, 2018

@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.


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,

This comment has been minimized.

@bnoordhuis

bnoordhuis Jun 14, 2018

Member

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

This comment has been minimized.

@tniessen

tniessen Jun 14, 2018

Author Member

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

This comment has been minimized.

@bnoordhuis

bnoordhuis Jun 14, 2018

Member

Indeed, errors should be reported.

This comment has been minimized.

@tniessen

tniessen Jun 14, 2018

Author Member

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

This comment has been minimized.

@bnoordhuis

bnoordhuis Jun 14, 2018

Member

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.

@@ -2893,6 +2898,7 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& args) {
bool CipherBase::SetAAD(const char* data, unsigned int len, int plaintext_len) {
if (!ctx_ || !IsAuthenticatedMode())
return false;
ClearErrorOnReturn clear_error_on_return;

This comment has been minimized.

@bnoordhuis

bnoordhuis Jun 14, 2018

Member

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

This comment has been minimized.

@tniessen

tniessen Jun 14, 2018

Author Member

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?

This comment has been minimized.

@bnoordhuis

bnoordhuis Jun 14, 2018 •

Member

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.

tniessen added some commits Jun 14, 2018

Don't clear all errors in return
Instead of clearing all errors, mark and pop only errors that were
added within the current scope.
@tniessen

This comment has been minimized.

Copy link
Member Author

tniessen commented Jul 1, 2018

@bnoordhuis Please take another look.

@ryzokuken
Copy link
Member

ryzokuken left a comment

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?


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,

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

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.

This comment has been minimized.

@tniessen

tniessen Jul 1, 2018

Author Member

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.

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

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.

This comment has been minimized.

@tniessen

tniessen Jul 1, 2018

Author Member

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.

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

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

Same as above.

@@ -2686,7 +2696,11 @@ void CipherBase::InitIv(const char* cipher_type,
EVP_CIPHER_CTX_set_flags(ctx_.get(), EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);

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

This comment has been minimized.

@ryzokuken
reinterpret_cast<const unsigned char*>(key),
reinterpret_cast<const unsigned char*>(iv),
encrypt);
if (1 != EVP_CipherInit_ex(ctx_.get(),

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

This is getting boring, isn't it? 😛

@@ -2749,6 +2753,7 @@ static bool IsValidGCMTagLength(unsigned int tag_len) {
bool CipherBase::InitAuthenticated(const char* cipher_type, int iv_len,
unsigned int auth_tag_len) {
CHECK(IsAuthenticatedMode());
MarkPopErrorOnReturn mark_pop_error_on_return;

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

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

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

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

Same as above.

@@ -2686,7 +2696,11 @@ void CipherBase::InitIv(const char* cipher_type,
EVP_CIPHER_CTX_set_flags(ctx_.get(), EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);

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

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

Same as above.

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

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

God, isn't this repetitive 😅

@@ -2749,6 +2753,7 @@ static bool IsValidGCMTagLength(unsigned int tag_len) {
bool CipherBase::InitAuthenticated(const char* cipher_type, int iv_len,
unsigned int auth_tag_len) {
CHECK(IsAuthenticatedMode());
MarkPopErrorOnReturn mark_pop_error_on_return;

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

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?

EVP_CTRL_GCM_SET_TAG,
auth_tag_len_,
reinterpret_cast<unsigned char*>(auth_tag_));
CHECK(EVP_CIPHER_CTX_ctrl(ctx_.get(),

This comment has been minimized.

@ryzokuken

ryzokuken Jul 1, 2018

Member

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?

This comment has been minimized.

@tniessen

tniessen Jul 1, 2018

Author Member

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.

@ryzokuken
Copy link
Member

ryzokuken left a comment

LGTM

@tniessen

This comment has been minimized.

Copy link
Member Author

tniessen commented Jul 7, 2018

@tniessen

This comment has been minimized.

Copy link
Member Author

tniessen commented Jul 12, 2018

Landed in 85b0f16.

@tniessen tniessen closed this Jul 12, 2018

tniessen added a commit that referenced this pull request Jul 12, 2018

crypto: handle OpenSSL error queue in CipherBase
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: #21288
Fixes: #21281
Refs: #21287
Reviewed-By: Ujjwal Sharma <usharma1998@gmail.com>

targos added a commit that referenced this pull request Jul 14, 2018

crypto: handle OpenSSL error queue in CipherBase
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: #21288
Fixes: #21281
Refs: #21287
Reviewed-By: Ujjwal Sharma <usharma1998@gmail.com>

@targos targos referenced this pull request Jul 17, 2018

Merged

v10.7.0 proposal #21851

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.