Skip to content

Commit a95d1b7

Browse files
authored
tls: support async cert validation (envoyproxy#21417)
Commit Message: change TLS transport socket to use SSL_CTX_set_custom_verify() instead of SSL_CTX_set_verify() and change CertValidator interface to support async cert validation. Also change EnvoyQuicCertVerifier to use the new async interfaces. This change is needed for envoyproxy/envoy-mobile#1575. Envoy Mobile allows certificates to be verified by the OS-provided certificate verifier. And these verification can be very slow and so blocking the network thread while the verification happens is not an option. Instead, the verification should be performed asynchronously on a different thread. (This is how the cert verification works in Chrome, which is what we're modeling this implementation on). Risk Level: high, change boring SSL interface used Testing: added new unit tests and integration tests Docs Changes: release note Release Notes: documented tls transport changes. Runtime guard: envoy.reloadable_features.tls_async_cert_validation Signed-off-by: Dan Zhang <[email protected]>
1 parent 86bc68a commit a95d1b7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1808
-300
lines changed

.clang-format

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ ColumnLimit: 100
55
DerivePointerAlignment: false
66
PointerAlignment: Left
77
SortIncludes: false
8+
TypenameMacros: ['STACK_OF']
89
...
910

1011
---

changelogs/current.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ behavior_changes:
99
change: |
1010
Envoy no longer adds ``content-length: 0`` header when proxying UPGRADE requests without ``content-length`` and ``transfer-encoding`` headers.
1111
This behavior change can be reverted by setting the ``envoy.reloadable_features.http_skip_adding_content_length_to_upgrade`` runtime flag to false.
12+
- area: tls
13+
change: |
14+
Change TLS and QUIC transport sockets to support asynchronous cert validation extension. This behavior change can be reverted by setting runtime guard ``envoy.reloadable_features.tls_async_cert_validation`` to false.
1215
1316
minor_behavior_changes:
1417
# *Changes that may cause incompatibilities for some users, but should not for most*

envoy/ssl/handshaker.h

+6
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ class HandshakeCallbacks {
3838
* unset.
3939
*/
4040
virtual Network::TransportSocketCallbacks* transportSocketCallbacks() PURE;
41+
42+
/**
43+
* A callback to be called upon certificate validation completion if the validation is
44+
* asynchronous.
45+
*/
46+
virtual void onAsynchronousCertValidationComplete() PURE;
4147
};
4248

4349
/**

envoy/ssl/ssl_socket_extended_info.h

+54
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,46 @@
11
#pragma once
22

3+
#include <cstdint>
34
#include <memory>
45
#include <string>
56
#include <vector>
67

78
#include "envoy/common/pure.h"
9+
#include "envoy/event/dispatcher.h"
810

911
namespace Envoy {
1012
namespace Ssl {
1113

1214
enum class ClientValidationStatus { NotValidated, NoClientCertificate, Validated, Failed };
1315

16+
enum class ValidateStatus {
17+
NotStarted,
18+
Pending,
19+
Successful,
20+
Failed,
21+
};
22+
23+
/**
24+
* Used to return the result from an asynchronous cert validation.
25+
*/
26+
class ValidateResultCallback {
27+
public:
28+
virtual ~ValidateResultCallback() = default;
29+
30+
virtual Event::Dispatcher& dispatcher() PURE;
31+
32+
/**
33+
* Called when the asynchronous cert validation completes.
34+
* @param succeeded true if the validation succeeds
35+
* @param error_details failure details, only used if the validation fails.
36+
* @param tls_alert the TLS error related to the failure, only used if the validation fails.
37+
*/
38+
virtual void onCertValidationResult(bool succeeded, const std::string& error_details,
39+
uint8_t tls_alert) PURE;
40+
};
41+
42+
using ValidateResultCallbackPtr = std::unique_ptr<ValidateResultCallback>;
43+
1444
class SslExtendedSocketInfo {
1545
public:
1646
virtual ~SslExtendedSocketInfo() = default;
@@ -24,6 +54,30 @@ class SslExtendedSocketInfo {
2454
* @return ClientValidationStatus The peer certificate validation status.
2555
**/
2656
virtual ClientValidationStatus certificateValidationStatus() const PURE;
57+
58+
/**
59+
* Only called when doing asynchronous cert validation.
60+
* @return ValidateResultCallbackPtr a callback used to return the validation result.
61+
*/
62+
virtual ValidateResultCallbackPtr createValidateResultCallback() PURE;
63+
64+
/**
65+
* Called after the cert validation completes either synchronously or asynchronously.
66+
* @param succeeded true if the validation succeeded.
67+
*/
68+
virtual void onCertificateValidationCompleted(bool succeeded) PURE;
69+
70+
/**
71+
* @return ValidateStatus the validation status.
72+
*/
73+
virtual ValidateStatus certificateValidationResult() const PURE;
74+
75+
/**
76+
* Called when doing asynchronous cert validation.
77+
* @return uint8_t represents the TLS alert populated by cert validator in
78+
* case of failure.
79+
*/
80+
virtual uint8_t certificateValidationAlert() const PURE;
2781
};
2882

2983
} // namespace Ssl

source/common/quic/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ envoy_cc_library(
131131
deps = [
132132
":envoy_quic_proof_verifier_base_lib",
133133
":envoy_quic_utils_lib",
134+
":quic_ssl_connection_info_lib",
134135
"//source/extensions/transport_sockets/tls:context_lib",
135136
],
136137
)

source/common/quic/envoy_quic_client_session.cc

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
#include "source/common/quic/envoy_quic_client_session.h"
22

3+
#include <openssl/ssl.h>
4+
5+
#include <memory>
6+
37
#include "source/common/event/dispatcher_impl.h"
48
#include "source/common/quic/envoy_quic_proof_verifier.h"
59
#include "source/common/quic/envoy_quic_utils.h"
@@ -14,9 +18,10 @@ class EnvoyQuicProofVerifyContextImpl : public EnvoyQuicProofVerifyContext {
1418
public:
1519
EnvoyQuicProofVerifyContextImpl(
1620
Event::Dispatcher& dispatcher, const bool is_server,
17-
const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options)
21+
const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options,
22+
QuicSslConnectionInfo& ssl_info)
1823
: dispatcher_(dispatcher), is_server_(is_server),
19-
transport_socket_options_(transport_socket_options) {}
24+
transport_socket_options_(transport_socket_options), ssl_info_(ssl_info) {}
2025

2126
// EnvoyQuicProofVerifyContext
2227
bool isServer() const override { return is_server_; }
@@ -25,10 +30,17 @@ class EnvoyQuicProofVerifyContextImpl : public EnvoyQuicProofVerifyContext {
2530
return transport_socket_options_;
2631
}
2732

33+
Extensions::TransportSockets::Tls::CertValidator::ExtraValidationContext
34+
extraValidationContext() const override {
35+
ASSERT(ssl_info_.ssl());
36+
return {};
37+
}
38+
2839
private:
2940
Event::Dispatcher& dispatcher_;
3041
const bool is_server_;
3142
const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options_;
43+
QuicSslConnectionInfo& ssl_info_;
3244
};
3345

3446
EnvoyQuicClientSession::EnvoyQuicClientSession(
@@ -183,7 +195,7 @@ std::unique_ptr<quic::QuicCryptoClientStreamBase> EnvoyQuicClientSession::Create
183195
return crypto_stream_factory_.createEnvoyQuicCryptoClientStream(
184196
server_id(), this,
185197
std::make_unique<EnvoyQuicProofVerifyContextImpl>(dispatcher_, /*is_server=*/false,
186-
transport_socket_options_),
198+
transport_socket_options_, *quic_ssl_info_),
187199
crypto_config(), this, /*has_application_state = */ version().UsesHttp3());
188200
}
189201

source/common/quic/envoy_quic_proof_verifier.cc

+116-9
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,73 @@
11
#include "source/common/quic/envoy_quic_proof_verifier.h"
22

3+
#include <openssl/ssl.h>
4+
5+
#include <cstdint>
6+
#include <memory>
7+
38
#include "source/common/quic/envoy_quic_utils.h"
9+
#include "source/common/runtime/runtime_features.h"
410
#include "source/extensions/transport_sockets/tls/utility.h"
511

612
#include "quiche/quic/core/crypto/certificate_view.h"
713

814
namespace Envoy {
915
namespace Quic {
1016

17+
using ValidationResults = Envoy::Extensions::TransportSockets::Tls::ValidationResults;
18+
19+
namespace {
20+
21+
// Returns true if hostname matches one of the Subject Alt Names in cert_view. Returns false and
22+
// sets error_details otherwise
23+
bool verifyLeafCertMatchesHostname(quic::CertificateView& cert_view, const std::string& hostname,
24+
std::string* error_details) {
25+
for (const absl::string_view& config_san : cert_view.subject_alt_name_domains()) {
26+
if (Extensions::TransportSockets::Tls::Utility::dnsNameMatch(hostname, config_san)) {
27+
return true;
28+
}
29+
}
30+
*error_details = absl::StrCat("Leaf certificate doesn't match hostname: ", hostname);
31+
return false;
32+
}
33+
34+
class QuicValidateResultCallback : public Ssl::ValidateResultCallback {
35+
public:
36+
QuicValidateResultCallback(Event::Dispatcher& dispatcher,
37+
std::unique_ptr<quic::ProofVerifierCallback>&& quic_callback,
38+
const std::string& hostname, const std::string& leaf_cert)
39+
: dispatcher_(dispatcher), quic_callback_(std::move(quic_callback)), hostname_(hostname),
40+
leaf_cert_(leaf_cert) {}
41+
42+
Event::Dispatcher& dispatcher() override { return dispatcher_; }
43+
44+
void onCertValidationResult(bool succeeded, const std::string& error_details,
45+
uint8_t /*tls_alert*/) override {
46+
if (!succeeded) {
47+
std::unique_ptr<quic::ProofVerifyDetails> details = std::make_unique<CertVerifyResult>(false);
48+
quic_callback_->Run(succeeded, error_details, &details);
49+
return;
50+
}
51+
std::string error;
52+
53+
std::unique_ptr<quic::CertificateView> cert_view =
54+
quic::CertificateView::ParseSingleCertificate(leaf_cert_);
55+
succeeded = verifyLeafCertMatchesHostname(*cert_view, hostname_, &error);
56+
std::unique_ptr<quic::ProofVerifyDetails> details =
57+
std::make_unique<CertVerifyResult>(succeeded);
58+
quic_callback_->Run(succeeded, error, &details);
59+
}
60+
61+
private:
62+
Event::Dispatcher& dispatcher_;
63+
std::unique_ptr<quic::ProofVerifierCallback> quic_callback_;
64+
const std::string hostname_;
65+
// Leaf cert needs to be retained in case of asynchronous validation.
66+
std::string leaf_cert_;
67+
};
68+
69+
} // namespace
70+
1171
quic::QuicAsyncStatus EnvoyQuicProofVerifier::VerifyCertChain(
1272
const std::string& hostname, const uint16_t port, const std::vector<std::string>& certs,
1373
const std::string& ocsp_response, const std::string& cert_sct,
@@ -23,11 +83,61 @@ quic::QuicAsyncStatus EnvoyQuicProofVerifier::VerifyCertChain(
2383
}
2484
ENVOY_BUG(!verify_context->isServer(), "Client certificates are not supported in QUIC yet.");
2585

26-
if (doVerifyCertChain(hostname, port, certs, ocsp_response, cert_sct, context, error_details,
27-
out_alert, std::move(callback))) {
28-
*details = std::make_unique<CertVerifyResult>(true);
29-
return quic::QUIC_SUCCESS;
86+
if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.tls_async_cert_validation")) {
87+
if (doVerifyCertChain(hostname, port, certs, ocsp_response, cert_sct, context, error_details,
88+
out_alert, std::move(callback))) {
89+
*details = std::make_unique<CertVerifyResult>(true);
90+
return quic::QUIC_SUCCESS;
91+
}
92+
*details = std::make_unique<CertVerifyResult>(false);
93+
return quic::QUIC_FAILURE;
3094
}
95+
96+
bssl::UniquePtr<STACK_OF(X509)> cert_chain(sk_X509_new_null());
97+
for (const auto& cert_str : certs) {
98+
bssl::UniquePtr<X509> cert = parseDERCertificate(cert_str, error_details);
99+
if (!cert || !bssl::PushToStack(cert_chain.get(), std::move(cert))) {
100+
return quic::QUIC_FAILURE;
101+
}
102+
}
103+
std::unique_ptr<quic::CertificateView> cert_view =
104+
quic::CertificateView::ParseSingleCertificate(certs[0]);
105+
ASSERT(cert_view != nullptr);
106+
int sign_alg = deduceSignatureAlgorithmFromPublicKey(cert_view->public_key(), error_details);
107+
if (sign_alg == 0) {
108+
return quic::QUIC_FAILURE;
109+
}
110+
111+
auto envoy_callback = std::make_unique<QuicValidateResultCallback>(
112+
verify_context->dispatcher(), std::move(callback), hostname, certs[0]);
113+
ASSERT(dynamic_cast<Extensions::TransportSockets::Tls::ClientContextImpl*>(context_.get()) !=
114+
nullptr);
115+
// We down cast rather than add customVerifyCertChainForQuic to Envoy::Ssl::Context because
116+
// verifyCertChain uses a bunch of SSL-specific structs which we want to keep out of the interface
117+
// definition.
118+
ValidationResults result =
119+
static_cast<Extensions::TransportSockets::Tls::ClientContextImpl*>(context_.get())
120+
->customVerifyCertChainForQuic(
121+
*cert_chain, std::move(envoy_callback), verify_context->isServer(),
122+
verify_context->transportSocketOptions(), verify_context->extraValidationContext());
123+
if (result.status == ValidationResults::ValidationStatus::Pending) {
124+
return quic::QUIC_PENDING;
125+
}
126+
if (result.status == ValidationResults::ValidationStatus::Successful) {
127+
if (verifyLeafCertMatchesHostname(*cert_view, hostname, error_details)) {
128+
*details = std::make_unique<CertVerifyResult>(true);
129+
return quic::QUIC_SUCCESS;
130+
}
131+
} else {
132+
ASSERT(result.status == ValidationResults::ValidationStatus::Failed);
133+
if (result.error_details.has_value() && error_details) {
134+
*error_details = std::move(result.error_details.value());
135+
}
136+
if (result.tls_alert.has_value() && out_alert) {
137+
*out_alert = result.tls_alert.value();
138+
}
139+
}
140+
31141
*details = std::make_unique<CertVerifyResult>(false);
32142
return quic::QUIC_FAILURE;
33143
}
@@ -65,11 +175,8 @@ bool EnvoyQuicProofVerifier::doVerifyCertChain(
65175
if (!success) {
66176
return false;
67177
}
68-
69-
for (const absl::string_view& config_san : cert_view->subject_alt_name_domains()) {
70-
if (Extensions::TransportSockets::Tls::Utility::dnsNameMatch(hostname, config_san)) {
71-
return true;
72-
}
178+
if (verifyLeafCertMatchesHostname(*cert_view, hostname, error_details)) {
179+
return true;
73180
}
74181
*error_details = absl::StrCat("Leaf certificate doesn't match hostname: ", hostname);
75182
return false;

source/common/quic/envoy_quic_proof_verifier.h

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#pragma once
22

3+
#include <memory>
4+
35
#include "source/common/quic/envoy_quic_proof_verifier_base.h"
6+
#include "source/common/quic/quic_ssl_connection_info.h"
47
#include "source/extensions/transport_sockets/tls/context_impl.h"
58

69
namespace Envoy {
@@ -18,19 +21,25 @@ class CertVerifyResult : public quic::ProofVerifyDetails {
1821
bool is_valid_{false};
1922
};
2023

24+
using CertVerifyResultPtr = std::unique_ptr<CertVerifyResult>();
25+
2126
// An interface for the Envoy specific QUIC verify context.
2227
class EnvoyQuicProofVerifyContext : public quic::ProofVerifyContext {
2328
public:
2429
virtual Event::Dispatcher& dispatcher() const PURE;
2530
virtual bool isServer() const PURE;
2631
virtual const Network::TransportSocketOptionsConstSharedPtr& transportSocketOptions() const PURE;
32+
virtual Extensions::TransportSockets::Tls::CertValidator::ExtraValidationContext
33+
extraValidationContext() const PURE;
2734
};
2835

36+
using EnvoyQuicProofVerifyContextPtr = std::unique_ptr<EnvoyQuicProofVerifyContext>;
37+
2938
// A quic::ProofVerifier implementation which verifies cert chain using SSL
3039
// client context config.
3140
class EnvoyQuicProofVerifier : public EnvoyQuicProofVerifierBase {
3241
public:
33-
EnvoyQuicProofVerifier(Envoy::Ssl::ClientContextSharedPtr&& context)
42+
explicit EnvoyQuicProofVerifier(Envoy::Ssl::ClientContextSharedPtr&& context)
3443
: context_(std::move(context)) {
3544
ASSERT(context_.get());
3645
}
@@ -45,6 +54,7 @@ class EnvoyQuicProofVerifier : public EnvoyQuicProofVerifierBase {
4554
std::unique_ptr<quic::ProofVerifierCallback> callback) override;
4655

4756
private:
57+
// TODO(danzh) remove when deprecating envoy.reloadable_features.tls_async_cert_validation.
4858
bool doVerifyCertChain(const std::string& hostname, const uint16_t port,
4959
const std::vector<std::string>& certs, const std::string& ocsp_response,
5060
const std::string& cert_sct, const quic::ProofVerifyContext* context,

source/common/runtime/runtime_features.cc

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ RUNTIME_GUARD(envoy_reloadable_features_skip_delay_close);
6262
RUNTIME_GUARD(envoy_reloadable_features_strict_check_on_ipv4_compat);
6363
RUNTIME_GUARD(envoy_reloadable_features_support_locality_update_on_eds_cluster_endpoints);
6464
RUNTIME_GUARD(envoy_reloadable_features_test_feature_true);
65+
RUNTIME_GUARD(envoy_reloadable_features_tls_async_cert_validation);
6566
RUNTIME_GUARD(envoy_reloadable_features_top_level_ecds_stats);
6667
RUNTIME_GUARD(envoy_reloadable_features_update_grpc_response_error_tag);
6768
RUNTIME_GUARD(envoy_reloadable_features_use_rfc_connect);

0 commit comments

Comments
 (0)