[tls] Fix incorrect use of certificate verification callbacks - #35369
[tls] Fix incorrect use of certificate verification callbacks#35369davidben wants to merge 2 commits into
Conversation
As documented in [0], there are two certificate verification callbacks in the OpenSSL/BoringSSL TLS API. The one taken as a parameter to SSL_CTX_set_verify is the "verify callback". It is called multiple times during a single certificate verification is used to suppress errors and otherwise be notified about various events during verification. Such a callback is not appropriate for accepting all certificates (you waste time processing things that will be thrown away), nor for post-verification inspection of the result (it will run multiple times). This is, however, what gRPC does with it. Rather, gRPC should have used SSL_CTX_set_cert_verify_callback, which swaps out the verification process entirely. That is called exactly once per handshake and allows you to skip the verification, or verify and then inspect the results afterwards. Fix gRPC to heed the documentation. In addition, this PR fixes a lifetime bug in gRPC's handling of the root certificate. RootCertExtractCallback stashes the root certificate without retaining it anywhere, but the X509_STORE_CTX will shortly be destroyed. There is no immediate guarantee the X509 object lasts as long as the SSL object. It most likely does because the object is often cached in the X509_STORE, which lives on the SSL_CTX, but this is at best, non-obvious. Instead, gRPC should have made g_ssl_ex_verified_root_cert_index own a refcount to the X509 object by registering a free function and calling X509_up_ref when saving the value. [0] https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_CTX_set_verify
|
Thanks for putting this together I have two questions:
I need to spend some more time with the code on this, but my immediate worry is that we are inherently depending on base verification behaviors in BoringSSL, then these callbacks are designed to be additional rather than replacement. And if we were to now replace that behavior, would we be breaking a significant assumption?
From looking at the API I thought this would be guaranteed, but I defer to you. I made the assumption because we get the This ends up being used in the following chain
So long as the lifetimes are ok here, this specific piece should be ok? ** - From on our other discussion this likely needs to be adjusted to use a stable string API as well because at one point it uses |
I'm not following. The callback you are currently using is the one that will break as BoringSSL evolves, because it exposes a bunch of implementation details about the order in which we do checks, and the granularity at which they're reported. The one this PR switches you to is the well-defined one, called exactly once per verification. Can you elaborate on this? For context, I'm one of the maintainers of BoringSSL. You all are decidedly doing the wrong thing right now.
The SSL object outlives the X509_STORE_CTX, not the other way around. That is the problem. Your callback is passed an X509_STORE_CTX. You use that to get your SSL. That is all fine but also irrelevant to the issue here. For the duration of the callback, anything you get off the X509_STORE_CTX is, of course, alive. However that's only true for the duration of the callback. After verification is done, the X509_STORE_CTX is destroyed. (X509_STORE_CTX != X509_STORE.) You all are saving an X509 that you get from X509_STORE_CTX, onto the SSL. There is no guarantee that the lifetimes match up. Now, as it happens, the root X509 will come out of the X509_STORE, which persistently caches things it looks up in memory, and the X509_STORE does live as long as the SSL. So it's probably fine. However this is subtle enough, and lifetime bugs serious enough, that you all should be bumping the refcount. It also has nothing to do with SSL_get_ex_data_X509_STORE_CTX_idx. |
The RFC 2253 output is at least a well-defined format. So, depending on what you're trying to do, it can potentially be okay. Your CRL bits were using something truly ad-hoc. (Though no text format would work for what you were doing with CRLs.) |
Like I said, I defer to you on this because you certainly know BoringSSL better than me. However, I do want to understand the changes to this codebase we're making. And in So based on my current understanding, this PR would be moving to the full replacement and calling a function that is meant to be used internally? But I don't think we want a full replacement, we still want the built-in procedure that "is quite powerful and in most cases it should be sufficient to modify its behaviour using the verify_callback function." In short, I'm having trouble reconciling you're suggestion with the documentation suggesting that using the
Thanks for the detailed explanation |
|
Ah. Yeah, we and OpenSSL do not always have the same views on which of their APIs are problematic. 😄 While we try to be mostly compatible, we expect projects like gRPC to reference BoringSSL's documentation and not just OpenSSL's. (The SSL-level APIs have been document for a while now. I expect to be done with the X509-level APIs this year.) But really this is just a question of understanding what the two callbacks are doing. The documentation is consistent, but it sounds like you are reading too much into "rarely" and "in most cases". I'll try to elaborate a bit. If you imagine this were a C++ class with virtual methods, there's two hooks here: Now, given those two hooks, hopefully some things are clear:
Something that may not be clear at a glance, but becomes abundantly clear once you've actually worked on a certificate verifier, is that making every arbitrary error in certificate verification suppressible is unbelievely fragile and unpredictable. E.g. if I suppress Now, what are gRPC's use cases here:
For the second, you want: If you need to tweak the config, you might also imagine doing this. (Note there is no way to do this with OnVerifyEvent at all.) That is what this PR is doing. Now for the particular snippets of OpenSSL documentation that you cited...
This is saying you shouldn't confuse the two. Indeed they are different callbacks and you should not mix them up.
Honestly, this warning is badly written. Yes, providing a complete verification procedure is complex. But also OpenSSL provides you the inputs to the built-in validator and you can call the "base class" implementation by simply calling Beyond that, the verify callback is pretty finicky, so I don't think using it a good idea. But, sure, if your customization looks like error suppression, you could use that.
This is also not very well-written. This is saying that, if you use the S/MIME and TLS APIs, they will run certificate validation for you as part of S/MIME or TLS. But if you are in a position where you need to run certificate validation itself, |
gtcooke94
left a comment
There was a problem hiding this comment.
Thanks for the detailed explanations! These changes make a lot of sense now.
It looks like the CI is hitting a few small things - some strict build errors related to unused args, and compiler errors when building with OpenSSL1.0.2 since X509_up_ref was introduced in 1.1.1
|
Ah fun. Will fix that up tomorrow. (I'd forgotten BTW, another fun thing about the verify callback: you might think that the calls with (I think it used to be sort of at the end, but then they moved some checks afterwards because they're pretty expensive so you want them done after the signature check.) |
|
The CI output seems to be ACLed, so I can't see the error messages. I've pushed a speculative fix. If there are still issues, can you post the errors here? |
As documented in [0], there are two certificate verification callbacks in the OpenSSL/BoringSSL TLS API. The one taken as a parameter to SSL_CTX_set_verify is the "verify callback". It is called multiple times during a single certificate verification is used to suppress errors and otherwise be notified about various events during verification.
Such a callback is not appropriate for accepting all certificates (you waste time processing things that will be thrown away), nor for post-verification inspection of the result (it will run multiple times). This is, however, what gRPC does with it.
Rather, gRPC should have used SSL_CTX_set_cert_verify_callback, which swaps out the verification process entirely. That is called exactly once per handshake and allows you to skip the verification, or verify and then inspect the results afterwards. Fix gRPC to heed the documentation.
In addition, this PR fixes a lifetime bug in gRPC's handling of the root certificate. RootCertExtractCallback stashes the root certificate without retaining it anywhere, but the X509_STORE_CTX will shortly be destroyed. There is no immediate guarantee the X509 object lasts as long as the SSL object. It most likely does because the object is often cached in the X509_STORE, which lives on the SSL_CTX, but this is at best, non-obvious. Instead, gRPC should have made
g_ssl_ex_verified_root_cert_index own a refcount to the X509 object by registering a free function and calling X509_up_ref when saving the value.
[0] https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_CTX_set_verify