-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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: client ECDSA detection during ClientHello. #5200
Conversation
This PR adds support to ServerContextImpl::selectTlsContext(), allowing for the selection of a certificate based on whether the client is RSA and/or ECDSA capable from a list. Since the list is still a singleton due to config ingest limitations, we're still effectively single certificate, but this PR exercises the logic that will be used in the multi-certificate selection case. Ideally, there should be no observable behavioral changes. Risk Level: Low (this should continue to fallback to the default certificate, even if the ECDSA code is exercised). Testing: Some new paramterized integration tests. Signed-off-by: Harvey Tuch <htuch@google.com>
@davidben @PiotrSikora can you folks take a look at the detection algorithm and compare with the reference algorithm we discussed? I can add more tests as well, I just want to make sure we're in agreement first on what to test. Thanks. |
include/envoy/ssl/context_config.h
Outdated
/** | ||
* @return const std::string& with the signature algorithms for the context. | ||
*/ | ||
virtual const std::string& sigalgs() const PURE; |
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: signingAlgorithmsForTest()
.
source/common/ssl/context_impl.cc
Outdated
return false; | ||
} | ||
|
||
if (named_curve == 23 /* secp256r1 */) { |
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 check for stronger curves as well, i.e.
if (named_curve == SSL_CURVE_SECP256R1 || named_curve == SSL_CURVE_SECP384R1 ||
named_curve == SSL_CURVE_SECP521R1) {
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.
That's not quite right either. You need to specifically check for the curve that your certificate uses. The code this was based on assumes server ECDSA certificates are always P-256. (P-256 receives by far the bulk of security hardening and performance work. Until earlier this year, our P-384 and P-521 implementations were not even constant-time, and we are unlikely to pay the binary size to optimize either with pre-computed base point tables.)
If Envoy also only cares about P-256 (good idea) this code should stay as-is (but do replace 23 with SSL_CURVE_SECP256R1
). If it wants to support others, it should extract the curve type and look for just that one.
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 going to follow the logic that will arrive with SSL_IDENTITY. I'll find the first ECDSA certificate in the list, extract its curve and use this in the check. Does this make sense to you and @davidben?
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.
Yeah.
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 you extract just the first ECDSA certificate's curve, then you should only consider the first certificate because the check won't be valid for the others.
But I would recommend just doing P-256. Again, the other curves do not see security or performance work.
const uint8_t* supported_versions_data; | ||
size_t supported_versions_len; | ||
if (SSL_early_callback_ctx_extension_get(ssl_client_hello, TLSEXT_TYPE_supported_versions, | ||
&supported_versions_data, &supported_versions_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.
In theory, we should verify that client announced TLS 1.3 in supported_versions
.
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 going to follow the reference implementation here and leave as is, we can discuss offline.
test/config/utility.cc
Outdated
// We'll negotiate up to TLSv1.3, but it really depends on what the client | ||
// sets. | ||
common_tls_context.mutable_tls_params()->set_tls_maximum_protocol_version( | ||
envoy::api::v2::auth::TlsParameters::TLSv1_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.
I'm not in love with this change, since we don't have TLS 1.3 enabled by default yet. Could we get away with setting this only when needed?
@@ -25,7 +25,8 @@ namespace Envoy { | |||
namespace Ssl { | |||
|
|||
ContextImpl::ContextImpl(Stats::Scope& scope, const ContextConfig& config, TimeSource& time_source) |
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 it would make sense to verify that you're adding at most one RSA and one ECDSA certificate, since SSL_IDENTITY
might reject configs like that. cc @davidben
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 didn't have particular plans to reject it though, at least with the PR as-is, I don't think anything but the first ECDSA certificate and the first RSA certificate will ever be used.
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.
Based on what @davidben writes, I think we should be more permissive today. In general, considering that this code is throw away when SSL_IDENTITY arrives, we should aim to minimize the number of additional branches and testing complexity.
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 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.
@htuch I think you are confusing two related decisions.
If you only look at one curve, then obviously only one ECDSA certificate will be used and multiple are pointless. If you look at multiple curves, having both a P-256 and a P-384 certificate is potentially useful. However, P-384 and P-521 are themselves pointless, so that informs the first decision.
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 I'm going to stick to P-256 only for simplicity and validate the certificate at config ingestion time to ensure it meets this requirement.
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.
See #5224
@@ -88,6 +88,11 @@ class ClientContextConfig : public virtual ContextConfig { | |||
* @return The maximum number of session keys to store. | |||
*/ | |||
virtual size_t maxSessionKeys() const PURE; | |||
|
|||
/** | |||
* @return const std::string& with the signature algorithms for the context. |
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 is no standard string representation of a signature algorithm list. This should document what format is being used.
@@ -25,7 +25,8 @@ namespace Envoy { | |||
namespace Ssl { | |||
|
|||
ContextImpl::ContextImpl(Stats::Scope& scope, const ContextConfig& config, TimeSource& time_source) |
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 didn't have particular plans to reject it though, at least with the PR as-is, I don't think anything but the first ECDSA certificate and the first RSA certificate will ever be used.
if (!config.sigalgs().empty()) { | ||
for (auto& ctx : tls_contexts_) { | ||
int rc = SSL_CTX_set1_sigalgs_list(ctx.ssl_ctx_.get(), config.sigalgs().c_str()); | ||
RELEASE_ASSERT(rc == 1, ""); |
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.
(Note this will fail on syntax error. Is that okay for your purposes 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.
Yeah, this is just for tests; I've done the rename suggested by @PiotrSikora, so we are good here.
source/common/ssl/context_impl.cc
Outdated
CBS_init(&client_hello, ssl_client_hello->client_hello, ssl_client_hello->client_hello_len); | ||
|
||
uint16_t client_version; | ||
if (!CBS_get_u16(&client_hello, &client_version)) { |
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 is no need to parse this value out. It's already in ssl_client_hello->version
.
source/common/ssl/context_impl.cc
Outdated
return false; | ||
} | ||
|
||
if (named_curve == 23 /* secp256r1 */) { |
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.
That's not quite right either. You need to specifically check for the curve that your certificate uses. The code this was based on assumes server ECDSA certificates are always P-256. (P-256 receives by far the bulk of security hardening and performance work. Until earlier this year, our P-384 and P-521 implementations were not even constant-time, and we are unlikely to pay the binary size to optimize either with pre-computed base point tables.)
If Envoy also only cares about P-256 (good idea) this code should stay as-is (but do replace 23 with SSL_CURVE_SECP256R1
). If it wants to support others, it should extract the curve type and look for just that one.
source/common/ssl/context_impl.cc
Outdated
@@ -866,5 +988,16 @@ void ServerContextImpl::TlsContext::addClientValidationContext( | |||
} | |||
} | |||
|
|||
bool ServerContextImpl::TlsContext::isCipherEnabled(const SSL_CIPHER* cipher) { | |||
STACK_OF(SSL_CIPHER)* our_ciphers = SSL_CTX_get_ciphers(ssl_ctx_.get()); | |||
for (size_t j = 0; j < sk_SSL_CIPHER_num(our_ciphers); j++) { |
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.
You can just write:
for (const SSL_CIPHER* our_c : SSL_CTX_get_ciphers(ssl_ctx_.get())) {
if (SSL_CIPHER_get_id(our_c) == SSL_CIPHER_get_id(cipher)) {
return true;
}
}
return false;
I was bored one day and made range-for loops work on STACK_OF(T)
. :-)
source/common/ssl/context_impl.cc
Outdated
return false; | ||
} | ||
|
||
while (CBS_len(&signature_algorithms) > 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.
Optional: you could probably abstract this and the curves loop into a cbsContainsU16(&signature_algorithms, SSL_SIGN_ECDSA_SECP256R1_SHA256)
and similar for the curve.
Signed-off-by: Harvey Tuch <htuch@google.com>
Signed-off-by: Harvey Tuch <htuch@google.com>
In support of envoyproxy#5200. There are no performance or security advantages today of non-P256 ECDSA certs. Risk Level: Low (unlikely anyone is using these). Testing: Pending; will add when we have agreement this is where we want to go. Signed-off-by: Harvey Tuch <htuch@google.com>
source/common/ssl/context_impl.cc
Outdated
return false; | ||
} | ||
|
||
const SSL_CIPHER* c = SSL_get_cipher_by_value(cipher_id); |
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.
You could probably push SSL_get_cipher_by_value()
, SSL_CIPHER_get_min_version()
and SSL_CIPHER_get_auth_nid()
into isCipherEnabled()
to make this loop more readable, i.e.:
while (CBS_len(&cipher_suites) > 0) {
uint16_t cipher_id;
if (!CBS_get_u16(&cipher_suites, &cipher_id)) {
return false;
}
// All tls_context_ share the same set of enabled ciphers, so we can just look at the base
// context.
if (tls_contexts_[0].isCipherEnabled(cipher_id)) {
return true;
}
}
Signed-off-by: Harvey Tuch <htuch@google.com>
In support of #5200. There are no performance or security advantages today of non-P256 ECDSA certs. Risk Level: Low (unlikely anyone is using these). Testing: Unit tests added to ssl:context_impl_test for happy/sad paths. Signed-off-by: Harvey Tuch <htuch@google.com>
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.
Oops, sorry for the delay! I was sure I that approved this yesterday along with the P-256 PR.
source/common/ssl/context_impl.cc
Outdated
@@ -812,11 +839,88 @@ int ServerContextImpl::sessionTicketProcess(SSL*, uint8_t* key_name, uint8_t* iv | |||
} | |||
} | |||
|
|||
bool ServerContextImpl::isClientEcdsaCapable(const SSL_CLIENT_HELLO* ssl_client_hello) { | |||
const uint16_t client_version = ssl_client_hello->version; |
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: not that it matters, but I would move it just before the first use, i.e. a line before the if
statement.
} | ||
|
||
// Validate certificate selection across different certificate types and client | ||
// TLS versions. |
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: comment wrapped at 80 chars again?
In support of envoyproxy#5200. There are no performance or security advantages today of non-P256 ECDSA certs. Risk Level: Low (unlikely anyone is using these). Testing: Unit tests added to ssl:context_impl_test for happy/sad paths. Signed-off-by: Harvey Tuch <htuch@google.com> Signed-off-by: Fred Douglas <fredlas@google.com>
This PR adds support to ServerContextImpl::selectTlsContext(), allowing for the selection of a certificate based on whether the client is RSA and/or ECDSA capable from a list. Since the list is still a singleton due to config ingest limitations, we're still effectively single certificate, but this PR exercises the logic that will be used in the multi-certificate selection case. Ideally, there should be no observable behavioral changes. Risk Level: Low (this should continue to fallback to the default certificate, even if the ECDSA code is exercised). Testing: Some new parameterized integration tests. Part of envoyproxy#1319 Signed-off-by: Harvey Tuch <htuch@google.com> Signed-off-by: Fred Douglas <fredlas@google.com>
Hi, |
This PR adds support to ServerContextImpl::selectTlsContext(), allowing
for the selection of a certificate based on whether the client is RSA
and/or ECDSA capable from a list.
Since the list is still a singleton due to config ingest limitations,
we're still effectively single certificate, but this PR exercises the
logic that will be used in the multi-certificate selection case.
Ideally, there should be no observable behavioral changes.
Risk Level: Low (this should continue to fallback to the default
certificate, even if the ECDSA code is exercised).
Testing: Some new parameterized integration tests.
Signed-off-by: Harvey Tuch htuch@google.com