From 928bd258be54b6773cc897fc694fa7be2c4e4f3c Mon Sep 17 00:00:00 2001 From: Michael Hargreaves Date: Fri, 31 Jan 2020 15:52:18 +1000 Subject: [PATCH] Add support to pass through and route untrusted certificates Signed-off-by: Michael Hargreaves --- api/envoy/api/v2/auth/cert.proto | 17 +++- api/envoy/api/v2/route/route_components.proto | 5 ++ .../config/route/v3/route_components.proto | 5 ++ .../transport_sockets/tls/v3/cert.proto | 17 +++- docs/root/intro/version_history.rst | 2 + .../envoy/api/v2/auth/cert.proto | 17 +++- .../envoy/api/v2/route/route_components.proto | 5 ++ .../config/route/v3/route_components.proto | 5 ++ .../transport_sockets/tls/v3/cert.proto | 17 +++- include/envoy/router/router.h | 12 +++ include/envoy/ssl/BUILD | 8 ++ .../certificate_validation_context_config.h | 8 ++ include/envoy/ssl/connection.h | 5 ++ include/envoy/ssl/ssl_socket_extended_info.h | 30 +++++++ source/common/router/config_impl.cc | 6 ++ .../router/tls_context_match_criteria_impl.cc | 4 + .../router/tls_context_match_criteria_impl.h | 2 + ...tificate_validation_context_config_impl.cc | 3 +- ...rtificate_validation_context_config_impl.h | 7 ++ source/extensions/transport_sockets/tls/BUILD | 2 + .../transport_sockets/tls/context_impl.cc | 90 +++++++++++++++---- .../transport_sockets/tls/context_impl.h | 13 ++- .../transport_sockets/tls/ssl_socket.cc | 22 ++++- .../transport_sockets/tls/ssl_socket.h | 15 +++- test/common/router/config_impl_test.cc | 68 ++++++++++++++ .../transport_sockets/tls/ssl_socket_test.cc | 86 ++++++++++++++++++ test/mocks/router/mocks.h | 1 + test/mocks/ssl/mocks.h | 1 + 28 files changed, 449 insertions(+), 24 deletions(-) create mode 100644 include/envoy/ssl/ssl_socket_extended_info.h diff --git a/api/envoy/api/v2/auth/cert.proto b/api/envoy/api/v2/auth/cert.proto index e2302b2621a3..8321787240be 100644 --- a/api/envoy/api/v2/auth/cert.proto +++ b/api/envoy/api/v2/auth/cert.proto @@ -180,8 +180,19 @@ message TlsSessionTicketKeys { [(validate.rules).repeated = {min_items: 1}, (udpa.annotations.sensitive) = true]; } -// [#next-free-field: 10] +// [#next-free-field: 11] message CertificateValidationContext { + // Peer certificate verification mode. + enum TrustChainVerification { + // Perform default certificate verification (e.g., against CA / verification lists) + VERIFY_TRUST_CHAIN = 0; + + // Connections where the certificate fails verification will be permitted. + // For HTTP connections, the result of certificate verification can be used in route matching. ( + // see :ref:`validated ` ). + ACCEPT_UNTRUSTED = 1; + } + // TLS certificate data containing certificate authority certificates to use in verifying // a presented peer certificate (e.g. server certificate for clusters or client certificate // for listeners). If not specified and a peer certificate is presented it will not be @@ -300,6 +311,10 @@ message CertificateValidationContext { // If specified, Envoy will not reject expired certificates. bool allow_expired_certificate = 8; + + // Certificate trust chain verification mode. + TrustChainVerification trust_chain_verification = 10 + [(validate.rules).enum = {defined_only: true}]; } // TLS context shared by both client and server TLS contexts. diff --git a/api/envoy/api/v2/route/route_components.proto b/api/envoy/api/v2/route/route_components.proto index d6c18b503cd6..14b4dffc3af1 100644 --- a/api/envoy/api/v2/route/route_components.proto +++ b/api/envoy/api/v2/route/route_components.proto @@ -352,7 +352,12 @@ message RouteMatch { message TlsContextMatchOptions { // If specified, the route will match against whether or not a certificate is presented. + // If not specified, certificate presentation status (true or false) will not be considered when route matching. google.protobuf.BoolValue presented = 1; + + // If specified, the route will match against whether or not a certificate is validated. + // If not specified, certificate validation status (true or false) will not be considered when route matching. + google.protobuf.BoolValue validated = 2; } reserved 5; diff --git a/api/envoy/config/route/v3/route_components.proto b/api/envoy/config/route/v3/route_components.proto index f28ab12d1b5d..2ac0c34af4a8 100644 --- a/api/envoy/config/route/v3/route_components.proto +++ b/api/envoy/config/route/v3/route_components.proto @@ -355,7 +355,12 @@ message RouteMatch { "envoy.api.v2.route.RouteMatch.TlsContextMatchOptions"; // If specified, the route will match against whether or not a certificate is presented. + // If not specified, certificate presentation status (true or false) will not be considered when route matching. google.protobuf.BoolValue presented = 1; + + // If specified, the route will match against whether or not a certificate is validated. + // If not specified, certificate validation status (true or false) will not be considered when route matching. + google.protobuf.BoolValue validated = 2; } reserved 5, 3; diff --git a/api/envoy/extensions/transport_sockets/tls/v3/cert.proto b/api/envoy/extensions/transport_sockets/tls/v3/cert.proto index e9fa044fa65c..162cdb1d0ced 100644 --- a/api/envoy/extensions/transport_sockets/tls/v3/cert.proto +++ b/api/envoy/extensions/transport_sockets/tls/v3/cert.proto @@ -190,11 +190,22 @@ message TlsSessionTicketKeys { [(validate.rules).repeated = {min_items: 1}, (udpa.annotations.sensitive) = true]; } -// [#next-free-field: 10] +// [#next-free-field: 11] message CertificateValidationContext { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.auth.CertificateValidationContext"; + // Peer certificate verification mode. + enum TrustChainVerification { + // Perform default certificate verification (e.g., against CA / verification lists) + VERIFY_TRUST_CHAIN = 0; + + // Connections where the certificate fails verification will be permitted. + // For HTTP connections, the result of certificate verification can be used in route matching. ( + // see :ref:`validated ` ). + ACCEPT_UNTRUSTED = 1; + } + reserved 4; reserved "verify_subject_alt_name"; @@ -307,6 +318,10 @@ message CertificateValidationContext { // If specified, Envoy will not reject expired certificates. bool allow_expired_certificate = 8; + + // Certificate trust chain verification mode. + TrustChainVerification trust_chain_verification = 10 + [(validate.rules).enum = {defined_only: true}]; } // TLS context shared by both client and server TLS contexts. diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 3d776ded3e5e..8313d0ca7e79 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -6,6 +6,8 @@ Version history * config: use type URL to select an extension whenever the config type URL (or its previous versions) uniquely identify a typed extension, see :ref:`extension configuration `. * http: fixing a bug in HTTP/1.0 responses where Connection: keep-alive was not appended for connections which were kept alive. * retry: added a retry predicate that :ref:`rejects hosts based on metadata. ` +* router: added the ability to match a route based on whether a downstream TLS connection certificate has been + :ref:`validated `. * upstream: combined HTTP/1 and HTTP/2 connection pool code. This means that circuit breaker limits for both requests and connections apply to both pool types. Also, HTTP/2 now has the option to limit concurrent requests on a connection, and allow multiple draining diff --git a/generated_api_shadow/envoy/api/v2/auth/cert.proto b/generated_api_shadow/envoy/api/v2/auth/cert.proto index e2302b2621a3..8321787240be 100644 --- a/generated_api_shadow/envoy/api/v2/auth/cert.proto +++ b/generated_api_shadow/envoy/api/v2/auth/cert.proto @@ -180,8 +180,19 @@ message TlsSessionTicketKeys { [(validate.rules).repeated = {min_items: 1}, (udpa.annotations.sensitive) = true]; } -// [#next-free-field: 10] +// [#next-free-field: 11] message CertificateValidationContext { + // Peer certificate verification mode. + enum TrustChainVerification { + // Perform default certificate verification (e.g., against CA / verification lists) + VERIFY_TRUST_CHAIN = 0; + + // Connections where the certificate fails verification will be permitted. + // For HTTP connections, the result of certificate verification can be used in route matching. ( + // see :ref:`validated ` ). + ACCEPT_UNTRUSTED = 1; + } + // TLS certificate data containing certificate authority certificates to use in verifying // a presented peer certificate (e.g. server certificate for clusters or client certificate // for listeners). If not specified and a peer certificate is presented it will not be @@ -300,6 +311,10 @@ message CertificateValidationContext { // If specified, Envoy will not reject expired certificates. bool allow_expired_certificate = 8; + + // Certificate trust chain verification mode. + TrustChainVerification trust_chain_verification = 10 + [(validate.rules).enum = {defined_only: true}]; } // TLS context shared by both client and server TLS contexts. diff --git a/generated_api_shadow/envoy/api/v2/route/route_components.proto b/generated_api_shadow/envoy/api/v2/route/route_components.proto index d6c18b503cd6..14b4dffc3af1 100644 --- a/generated_api_shadow/envoy/api/v2/route/route_components.proto +++ b/generated_api_shadow/envoy/api/v2/route/route_components.proto @@ -352,7 +352,12 @@ message RouteMatch { message TlsContextMatchOptions { // If specified, the route will match against whether or not a certificate is presented. + // If not specified, certificate presentation status (true or false) will not be considered when route matching. google.protobuf.BoolValue presented = 1; + + // If specified, the route will match against whether or not a certificate is validated. + // If not specified, certificate validation status (true or false) will not be considered when route matching. + google.protobuf.BoolValue validated = 2; } reserved 5; diff --git a/generated_api_shadow/envoy/config/route/v3/route_components.proto b/generated_api_shadow/envoy/config/route/v3/route_components.proto index d3cebd0a8404..45c2727f3056 100644 --- a/generated_api_shadow/envoy/config/route/v3/route_components.proto +++ b/generated_api_shadow/envoy/config/route/v3/route_components.proto @@ -373,7 +373,12 @@ message RouteMatch { "envoy.api.v2.route.RouteMatch.TlsContextMatchOptions"; // If specified, the route will match against whether or not a certificate is presented. + // If not specified, certificate presentation status (true or false) will not be considered when route matching. google.protobuf.BoolValue presented = 1; + + // If specified, the route will match against whether or not a certificate is validated. + // If not specified, certificate validation status (true or false) will not be considered when route matching. + google.protobuf.BoolValue validated = 2; } reserved 5; diff --git a/generated_api_shadow/envoy/extensions/transport_sockets/tls/v3/cert.proto b/generated_api_shadow/envoy/extensions/transport_sockets/tls/v3/cert.proto index 9cbc61fec9fd..e5c53dfacb23 100644 --- a/generated_api_shadow/envoy/extensions/transport_sockets/tls/v3/cert.proto +++ b/generated_api_shadow/envoy/extensions/transport_sockets/tls/v3/cert.proto @@ -189,11 +189,22 @@ message TlsSessionTicketKeys { [(validate.rules).repeated = {min_items: 1}, (udpa.annotations.sensitive) = true]; } -// [#next-free-field: 10] +// [#next-free-field: 11] message CertificateValidationContext { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.auth.CertificateValidationContext"; + // Peer certificate verification mode. + enum TrustChainVerification { + // Perform default certificate verification (e.g., against CA / verification lists) + VERIFY_TRUST_CHAIN = 0; + + // Connections where the certificate fails verification will be permitted. + // For HTTP connections, the result of certificate verification can be used in route matching. ( + // see :ref:`validated ` ). + ACCEPT_UNTRUSTED = 1; + } + // TLS certificate data containing certificate authority certificates to use in verifying // a presented peer certificate (e.g. server certificate for clusters or client certificate // for listeners). If not specified and a peer certificate is presented it will not be @@ -312,6 +323,10 @@ message CertificateValidationContext { // If specified, Envoy will not reject expired certificates. bool allow_expired_certificate = 8; + + // Certificate trust chain verification mode. + TrustChainVerification trust_chain_verification = 10 + [(validate.rules).enum = {defined_only: true}]; } // TLS context shared by both client and server TLS contexts. diff --git a/include/envoy/router/router.h b/include/envoy/router/router.h index 3f7ba07a0f93..d7a2a4db1835 100644 --- a/include/envoy/router/router.h +++ b/include/envoy/router/router.h @@ -530,11 +530,23 @@ class MetadataMatchCriteria { filterMatchCriteria(const std::set& names) const PURE; }; +/** + * Criterion that a route entry uses for matching TLS connection context. + */ class TlsContextMatchCriteria { public: virtual ~TlsContextMatchCriteria() = default; + /** + * @return bool indicating whether the client presented credentials. + */ virtual const absl::optional& presented() const PURE; + + /** + * @return bool indicating whether the client credentials successfully validated against the TLS + * context validation context. + */ + virtual const absl::optional& validated() const PURE; }; using TlsContextMatchCriteriaConstPtr = std::unique_ptr; diff --git a/include/envoy/ssl/BUILD b/include/envoy/ssl/BUILD index 1e4a70d3a67d..fb14af1a211c 100644 --- a/include/envoy/ssl/BUILD +++ b/include/envoy/ssl/BUILD @@ -57,6 +57,14 @@ envoy_cc_library( hdrs = ["certificate_validation_context_config.h"], deps = [ "//source/common/common:matchers_lib", + "@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg_cc_proto", "@envoy_api//envoy/type/matcher/v3:pkg_cc_proto", ], ) + +envoy_cc_library( + name = "ssl_socket_extended_info_interface", + hdrs = ["ssl_socket_extended_info.h"], + deps = [ + ], +) diff --git a/include/envoy/ssl/certificate_validation_context_config.h b/include/envoy/ssl/certificate_validation_context_config.h index 53ff908e49c4..5011cb1226f0 100644 --- a/include/envoy/ssl/certificate_validation_context_config.h +++ b/include/envoy/ssl/certificate_validation_context_config.h @@ -5,6 +5,7 @@ #include #include "envoy/common/pure.h" +#include "envoy/extensions/transport_sockets/tls/v3/cert.pb.h" #include "envoy/type/matcher/v3/string.pb.h" namespace Envoy { @@ -61,6 +62,13 @@ class CertificateValidationContextConfig { * @return whether to ignore expired certificates (both too new and too old). */ virtual bool allowExpiredCertificate() const PURE; + + /** + * @return client certificate validation configuration. + */ + virtual envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext:: + TrustChainVerification + trustChainVerification() const PURE; }; using CertificateValidationContextConfigPtr = std::unique_ptr; diff --git a/include/envoy/ssl/connection.h b/include/envoy/ssl/connection.h index 89b12d1e5ae1..bb2a13484930 100644 --- a/include/envoy/ssl/connection.h +++ b/include/envoy/ssl/connection.h @@ -23,6 +23,11 @@ class ConnectionInfo { **/ virtual bool peerCertificatePresented() const PURE; + /** + * @return bool whether the peer certificate was validated. + **/ + virtual bool peerCertificateValidated() const PURE; + /** * @return std::string the URIs in the SAN field of the local certificate. Returns {} if there is * no local certificate, or no SAN field, or no URI. diff --git a/include/envoy/ssl/ssl_socket_extended_info.h b/include/envoy/ssl/ssl_socket_extended_info.h new file mode 100644 index 000000000000..b219b2ed0f49 --- /dev/null +++ b/include/envoy/ssl/ssl_socket_extended_info.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include "envoy/common/pure.h" + +namespace Envoy { +namespace Ssl { + +enum class ClientValidationStatus { NotValidated, NoClientCertificate, Validated, Failed }; + +class SslExtendedSocketInfo { +public: + virtual ~SslExtendedSocketInfo() = default; + + /** + * Set the peer certificate validation status. + **/ + virtual void setCertificateValidationStatus(ClientValidationStatus validated) PURE; + + /** + * @return ClientValidationStatus The peer certificate validation status. + **/ + virtual ClientValidationStatus certificateValidationStatus() const PURE; +}; + +} // namespace Ssl +} // namespace Envoy diff --git a/source/common/router/config_impl.cc b/source/common/router/config_impl.cc index 2a592bab59d2..4f9a73c0e4a1 100644 --- a/source/common/router/config_impl.cc +++ b/source/common/router/config_impl.cc @@ -399,6 +399,12 @@ bool RouteEntryImplBase::evaluateTlsContextMatch(const StreamInfo::StreamInfo& s matches &= criteria.presented().value() == peer_presented; } + if (criteria.validated().has_value()) { + const bool peer_validated = stream_info.downstreamSslConnection() && + stream_info.downstreamSslConnection()->peerCertificateValidated(); + matches &= criteria.validated().value() == peer_validated; + } + return matches; } diff --git a/source/common/router/tls_context_match_criteria_impl.cc b/source/common/router/tls_context_match_criteria_impl.cc index 69b427fe234d..ee1436972027 100644 --- a/source/common/router/tls_context_match_criteria_impl.cc +++ b/source/common/router/tls_context_match_criteria_impl.cc @@ -10,6 +10,10 @@ TlsContextMatchCriteriaImpl::TlsContextMatchCriteriaImpl( if (options.has_presented()) { presented_ = options.presented().value(); } + + if (options.has_validated()) { + validated_ = options.validated().value(); + } } } // namespace Router diff --git a/source/common/router/tls_context_match_criteria_impl.h b/source/common/router/tls_context_match_criteria_impl.h index 327c38b0ddad..32fe5d898821 100644 --- a/source/common/router/tls_context_match_criteria_impl.h +++ b/source/common/router/tls_context_match_criteria_impl.h @@ -12,9 +12,11 @@ class TlsContextMatchCriteriaImpl : public TlsContextMatchCriteria { const envoy::config::route::v3::RouteMatch::TlsContextMatchOptions& options); const absl::optional& presented() const override { return presented_; } + const absl::optional& validated() const override { return validated_; } private: absl::optional presented_; + absl::optional validated_; }; } // namespace Router diff --git a/source/common/ssl/certificate_validation_context_config_impl.cc b/source/common/ssl/certificate_validation_context_config_impl.cc index c7f4c2978b4b..2f4a1ac8bc84 100644 --- a/source/common/ssl/certificate_validation_context_config_impl.cc +++ b/source/common/ssl/certificate_validation_context_config_impl.cc @@ -31,7 +31,8 @@ CertificateValidationContextConfigImpl::CertificateValidationContextConfigImpl( config.verify_certificate_hash().end()), verify_certificate_spki_list_(config.verify_certificate_spki().begin(), config.verify_certificate_spki().end()), - allow_expired_certificate_(config.allow_expired_certificate()) { + allow_expired_certificate_(config.allow_expired_certificate()), + trust_chain_verification_(config.trust_chain_verification()) { if (ca_cert_.empty()) { if (!certificate_revocation_list_.empty()) { throw EnvoyException(fmt::format("Failed to load CRL from {} without trusted CA", diff --git a/source/common/ssl/certificate_validation_context_config_impl.h b/source/common/ssl/certificate_validation_context_config_impl.h index 532a22c37847..f054039ee1ba 100644 --- a/source/common/ssl/certificate_validation_context_config_impl.h +++ b/source/common/ssl/certificate_validation_context_config_impl.h @@ -38,6 +38,11 @@ class CertificateValidationContextConfigImpl : public CertificateValidationConte return verify_certificate_spki_list_; } bool allowExpiredCertificate() const override { return allow_expired_certificate_; } + envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext:: + TrustChainVerification + trustChainVerification() const override { + return trust_chain_verification_; + } private: const std::string ca_cert_; @@ -49,6 +54,8 @@ class CertificateValidationContextConfigImpl : public CertificateValidationConte const std::vector verify_certificate_hash_list_; const std::vector verify_certificate_spki_list_; const bool allow_expired_certificate_; + const envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext:: + TrustChainVerification trust_chain_verification_; }; } // namespace Ssl diff --git a/source/extensions/transport_sockets/tls/BUILD b/source/extensions/transport_sockets/tls/BUILD index 11f8983861de..c02362fbfd4a 100644 --- a/source/extensions/transport_sockets/tls/BUILD +++ b/source/extensions/transport_sockets/tls/BUILD @@ -41,6 +41,7 @@ envoy_cc_library( ":utility_lib", "//include/envoy/network:connection_interface", "//include/envoy/network:transport_socket_interface", + "//include/envoy/ssl:ssl_socket_extended_info_interface", "//include/envoy/ssl/private_key:private_key_callbacks_interface", "//include/envoy/ssl/private_key:private_key_interface", "//include/envoy/stats:stats_macros", @@ -96,6 +97,7 @@ envoy_cc_library( "//include/envoy/ssl:context_config_interface", "//include/envoy/ssl:context_interface", "//include/envoy/ssl:context_manager_interface", + "//include/envoy/ssl:ssl_socket_extended_info_interface", "//include/envoy/ssl/private_key:private_key_interface", "//include/envoy/stats:stats_interface", "//include/envoy/stats:stats_macros", diff --git a/source/extensions/transport_sockets/tls/context_impl.cc b/source/extensions/transport_sockets/tls/context_impl.cc index 0d4216fd89fe..460028c54a6b 100644 --- a/source/extensions/transport_sockets/tls/context_impl.cc +++ b/source/extensions/transport_sockets/tls/context_impl.cc @@ -8,6 +8,7 @@ #include "envoy/admin/v3/certs.pb.h" #include "envoy/common/exception.h" #include "envoy/common/platform.h" +#include "envoy/ssl/ssl_socket_extended_info.h" #include "envoy/stats/scope.h" #include "envoy/type/matcher/v3/string.pb.h" @@ -49,6 +50,14 @@ bool cbsContainsU16(CBS& cbs, uint16_t n) { } // namespace +int ContextImpl::sslExtendedSocketInfoIndex() { + CONSTRUCT_ON_FIRST_USE(int, []() -> int { + int ssl_context_index = SSL_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr); + RELEASE_ASSERT(ssl_context_index >= 0, ""); + return ssl_context_index; + }()); +} + ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& config, TimeSource& time_source) : scope_(scope), stats_(generateStats(scope)), time_source_(time_source), @@ -98,6 +107,20 @@ ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& c } int verify_mode = SSL_VERIFY_NONE; + int verify_mode_validation_context = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + + if (config.certificateValidationContext() != nullptr) { + envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext:: + TrustChainVerification verification = + config.certificateValidationContext()->trustChainVerification(); + if (verification == envoy::extensions::transport_sockets::tls::v3:: + CertificateValidationContext::ACCEPT_UNTRUSTED) { + verify_mode = SSL_VERIFY_PEER; // Ensure client-certs will be requested even if we have + // nothing to verify against + verify_mode_validation_context = SSL_VERIFY_PEER; + } + } + if (config.certificateValidationContext() != nullptr && !config.certificateValidationContext()->caCert().empty()) { ca_file_path_ = config.certificateValidationContext()->caCertPath(); @@ -183,7 +206,7 @@ ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& c if (cert_validation_config != nullptr) { if (!cert_validation_config->verifySubjectAltNameList().empty()) { verify_subject_alt_name_list_ = cert_validation_config->verifySubjectAltNameList(); - verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + verify_mode = verify_mode_validation_context; } if (!cert_validation_config->subjectAltNameMatchers().empty()) { @@ -191,7 +214,7 @@ ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& c cert_validation_config->subjectAltNameMatchers()) { subject_alt_name_matchers_.push_back(Matchers::StringMatcherImpl(matcher)); } - verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + verify_mode = verify_mode_validation_context; } if (!cert_validation_config->verifyCertificateHashList().empty()) { @@ -207,7 +230,7 @@ ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& c } verify_certificate_hash_list_.push_back(decoded); } - verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + verify_mode = verify_mode_validation_context; } if (!cert_validation_config->verifyCertificateSpkiList().empty()) { @@ -218,7 +241,7 @@ ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& c } verify_certificate_spki_list_.emplace_back(decoded.begin(), decoded.end()); } - verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; + verify_mode = verify_mode_validation_context; } } @@ -386,6 +409,13 @@ ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& c SSL_CTX_set_options(ctx.ssl_ctx_.get(), SSL_OP_CIPHER_SERVER_PREFERENCE); } + if (config.certificateValidationContext() != nullptr) { + allow_untrusted_certificate_ = + config.certificateValidationContext()->trustChainVerification() == + envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext:: + ACCEPT_UNTRUSTED; + } + parsed_alpn_protocols_ = parseAlpnProtocols(config.alpnProtocols()); // To enumerate the required builtin ciphers, curves, algorithms, and @@ -478,41 +508,69 @@ int ContextImpl::ignoreCertificateExpirationCallback(int ok, X509_STORE_CTX* ctx int ContextImpl::verifyCallback(X509_STORE_CTX* store_ctx, void* arg) { ContextImpl* impl = reinterpret_cast(arg); + SSL* ssl = reinterpret_cast( + X509_STORE_CTX_get_ex_data(store_ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); + Envoy::Ssl::SslExtendedSocketInfo* sslExtendedInfo = + reinterpret_cast( + SSL_get_ex_data(ssl, ContextImpl::sslExtendedSocketInfoIndex())); if (impl->verify_trusted_ca_) { int ret = X509_verify_cert(store_ctx); + if (sslExtendedInfo) { + sslExtendedInfo->setCertificateValidationStatus( + ret == 1 ? Envoy::Ssl::ClientValidationStatus::Validated + : Envoy::Ssl::ClientValidationStatus::Failed); + } + if (ret <= 0) { impl->stats_.fail_verify_error_.inc(); - return ret; + return impl->allow_untrusted_certificate_ ? 1 : ret; } } - SSL* ssl = reinterpret_cast( - X509_STORE_CTX_get_ex_data(store_ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); bssl::UniquePtr cert(SSL_get_peer_certificate(ssl)); const Network::TransportSocketOptions* transport_socket_options = static_cast(SSL_get_app_data(ssl)); - return impl->verifyCertificate( + + Envoy::Ssl::ClientValidationStatus validated = impl->verifyCertificate( cert.get(), transport_socket_options && !transport_socket_options->verifySubjectAltNameListOverride().empty() ? transport_socket_options->verifySubjectAltNameListOverride() : impl->verify_subject_alt_name_list_, impl->subject_alt_name_matchers_); + + if (sslExtendedInfo) { + if (sslExtendedInfo->certificateValidationStatus() == + Envoy::Ssl::ClientValidationStatus::NotValidated) { + sslExtendedInfo->setCertificateValidationStatus(validated); + } else if (validated != Envoy::Ssl::ClientValidationStatus::NotValidated) { + sslExtendedInfo->setCertificateValidationStatus(validated); + } + } + + return impl->allow_untrusted_certificate_ + ? 1 + : (validated != Envoy::Ssl::ClientValidationStatus::Failed); } -int ContextImpl::verifyCertificate( +Envoy::Ssl::ClientValidationStatus ContextImpl::verifyCertificate( X509* cert, const std::vector& verify_san_list, const std::vector& subject_alt_name_matchers) { - if (!verify_san_list.empty() && !verifySubjectAltName(cert, verify_san_list)) { - stats_.fail_verify_san_.inc(); - return 0; + Envoy::Ssl::ClientValidationStatus validated = Envoy::Ssl::ClientValidationStatus::NotValidated; + + if (!verify_san_list.empty()) { + if (!verifySubjectAltName(cert, verify_san_list)) { + stats_.fail_verify_san_.inc(); + return Envoy::Ssl::ClientValidationStatus::Failed; + } + validated = Envoy::Ssl::ClientValidationStatus::Validated; } if (!subject_alt_name_matchers.empty() && !matchSubjectAltName(cert, subject_alt_name_matchers)) { stats_.fail_verify_san_.inc(); - return 0; + return Envoy::Ssl::ClientValidationStatus::Failed; } if (!verify_certificate_hash_list_.empty() || !verify_certificate_spki_list_.empty()) { @@ -525,11 +583,13 @@ int ContextImpl::verifyCertificate( if (!valid_certificate_hash && !valid_certificate_spki) { stats_.fail_verify_cert_hash_.inc(); - return 0; + return Envoy::Ssl::ClientValidationStatus::Failed; } + + validated = Envoy::Ssl::ClientValidationStatus::Validated; } - return 1; + return validated; } void ContextImpl::incCounter(const Stats::StatName name, absl::string_view value, diff --git a/source/extensions/transport_sockets/tls/context_impl.h b/source/extensions/transport_sockets/tls/context_impl.h index 920405a05679..f60ddec758dc 100644 --- a/source/extensions/transport_sockets/tls/context_impl.h +++ b/source/extensions/transport_sockets/tls/context_impl.h @@ -10,6 +10,7 @@ #include "envoy/ssl/context.h" #include "envoy/ssl/context_config.h" #include "envoy/ssl/private_key/private_key.h" +#include "envoy/ssl/ssl_socket_extended_info.h" #include "envoy/stats/scope.h" #include "envoy/stats/stats_macros.h" @@ -87,6 +88,12 @@ class ContextImpl : public virtual Envoy::Ssl::Context { SslStats& stats() { return stats_; } + /** + * The global SSL-library index used for storing a pointer to the SslExtendedSocketInfo + * class in the SSL instance, for retrieval in callbacks. + */ + static int sslExtendedSocketInfoIndex(); + // Ssl::Context size_t daysUntilFirstCertExpires() const override; Envoy::Ssl::CertificateDetailsPtr getCaCertInformation() const override; @@ -110,8 +117,9 @@ class ContextImpl : public virtual Envoy::Ssl::Context { // A SSL_CTX_set_cert_verify_callback for custom cert validation. static int verifyCallback(X509_STORE_CTX* store_ctx, void* arg); - int verifyCertificate(X509* cert, const std::vector& verify_san_list, - const std::vector& subject_alt_name_matchers); + Envoy::Ssl::ClientValidationStatus + verifyCertificate(X509* cert, const std::vector& verify_san_list, + const std::vector& subject_alt_name_matchers); /** * Verifies certificate hash for pinning. The hash is a hex-encoded SHA-256 of the DER-encoded @@ -175,6 +183,7 @@ class ContextImpl : public virtual Envoy::Ssl::Context { std::vector subject_alt_name_matchers_; std::vector> verify_certificate_hash_list_; std::vector> verify_certificate_spki_list_; + bool allow_untrusted_certificate_{false}; Stats::Scope& scope_; SslStats stats_; std::vector parsed_alpn_protocols_; diff --git a/source/extensions/transport_sockets/tls/ssl_socket.cc b/source/extensions/transport_sockets/tls/ssl_socket.cc index 00c341a5d734..316523d88e20 100644 --- a/source/extensions/transport_sockets/tls/ssl_socket.cc +++ b/source/extensions/transport_sockets/tls/ssl_socket.cc @@ -48,7 +48,8 @@ SslSocket::SslSocket(Envoy::Ssl::ContextSharedPtr ctx, InitialState state, ctx_(std::dynamic_pointer_cast(ctx)), state_(SocketState::PreHandshake) { bssl::UniquePtr ssl = ctx_->newSsl(transport_socket_options_.get()); ssl_ = ssl.get(); - info_ = std::make_shared(std::move(ssl)); + info_ = std::make_shared(std::move(ssl), ctx_); + if (state == InitialState::Client) { SSL_set_connect_state(ssl_); } else { @@ -301,11 +302,30 @@ void SslSocket::shutdownSsl() { } } +void SslExtendedSocketInfoImpl::setCertificateValidationStatus( + Envoy::Ssl::ClientValidationStatus validated) { + certificate_validation_status_ = validated; +} + +Envoy::Ssl::ClientValidationStatus SslExtendedSocketInfoImpl::certificateValidationStatus() const { + return certificate_validation_status_; +} + +SslSocketInfo::SslSocketInfo(bssl::UniquePtr ssl, ContextImplSharedPtr ctx) + : ssl_(std::move(ssl)) { + SSL_set_ex_data(ssl_.get(), ctx->sslExtendedSocketInfoIndex(), &(this->extended_socket_info_)); +} + bool SslSocketInfo::peerCertificatePresented() const { bssl::UniquePtr cert(SSL_get_peer_certificate(ssl_.get())); return cert != nullptr; } +bool SslSocketInfo::peerCertificateValidated() const { + return extended_socket_info_.certificateValidationStatus() == + Envoy::Ssl::ClientValidationStatus::Validated; +} + absl::Span SslSocketInfo::uriSanLocalCertificate() const { if (!cached_uri_san_local_certificate_.empty()) { return cached_uri_san_local_certificate_; diff --git a/source/extensions/transport_sockets/tls/ssl_socket.h b/source/extensions/transport_sockets/tls/ssl_socket.h index 2ce9f6b08dc3..87ad666aacee 100644 --- a/source/extensions/transport_sockets/tls/ssl_socket.h +++ b/source/extensions/transport_sockets/tls/ssl_socket.h @@ -7,6 +7,7 @@ #include "envoy/network/transport_socket.h" #include "envoy/secret/secret_callbacks.h" #include "envoy/ssl/private_key/private_key_callbacks.h" +#include "envoy/ssl/ssl_socket_extended_info.h" #include "envoy/stats/scope.h" #include "envoy/stats/stats_macros.h" @@ -39,12 +40,23 @@ struct SslSocketFactoryStats { enum class InitialState { Client, Server }; enum class SocketState { PreHandshake, HandshakeInProgress, HandshakeComplete, ShutdownSent }; +class SslExtendedSocketInfoImpl : public Envoy::Ssl::SslExtendedSocketInfo { +public: + void setCertificateValidationStatus(Envoy::Ssl::ClientValidationStatus validated) override; + Envoy::Ssl::ClientValidationStatus certificateValidationStatus() const override; + +private: + Envoy::Ssl::ClientValidationStatus certificate_validation_status_{ + Envoy::Ssl::ClientValidationStatus::NotValidated}; +}; + class SslSocketInfo : public Envoy::Ssl::ConnectionInfo { public: - SslSocketInfo(bssl::UniquePtr ssl) : ssl_(std::move(ssl)) {} + SslSocketInfo(bssl::UniquePtr ssl, ContextImplSharedPtr ctx); // Ssl::ConnectionInfo bool peerCertificatePresented() const override; + bool peerCertificateValidated() const override; absl::Span uriSanLocalCertificate() const override; const std::string& sha256PeerCertificateDigest() const override; const std::string& serialNumberPeerCertificate() const override; @@ -81,6 +93,7 @@ class SslSocketInfo : public Envoy::Ssl::ConnectionInfo { mutable std::vector cached_dns_san_local_certificate_; mutable std::string cached_session_id_; mutable std::string cached_tls_version_; + mutable SslExtendedSocketInfoImpl extended_socket_info_; }; class SslSocket : public Network::TransportSocket, diff --git a/test/common/router/config_impl_test.cc b/test/common/router/config_impl_test.cc index 888346a7886f..8bfff452dad5 100644 --- a/test/common/router/config_impl_test.cc +++ b/test/common/router/config_impl_test.cc @@ -5956,6 +5956,18 @@ name: foo presented: false route: cluster: server_peer-cert-not-presented + - match: + prefix: "/peer-validated-cert-test" + tls_context: + validated: true + route: + cluster: server_peer-cert-validated + - match: + prefix: "/peer-validated-cert-test" + tls_context: + validated: false + route: + cluster: server_peer-cert-not-validated - match: prefix: "/peer-cert-no-tls-context-match" route: @@ -5972,6 +5984,7 @@ name: foo NiceMock stream_info; auto connection_info = std::make_shared(); EXPECT_CALL(*connection_info, peerCertificatePresented()).WillRepeatedly(Return(true)); + EXPECT_CALL(*connection_info, peerCertificateValidated()).WillRepeatedly(Return(true)); EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); Http::TestHeaderMapImpl headers = genHeaders("www.lyft.com", "/peer-cert-test", "GET"); @@ -5983,6 +5996,7 @@ name: foo NiceMock stream_info; auto connection_info = std::make_shared(); EXPECT_CALL(*connection_info, peerCertificatePresented()).WillRepeatedly(Return(false)); + EXPECT_CALL(*connection_info, peerCertificateValidated()).WillRepeatedly(Return(true)); EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); Http::TestHeaderMapImpl headers = genHeaders("www.lyft.com", "/peer-cert-test", "GET"); @@ -5994,6 +6008,59 @@ name: foo NiceMock stream_info; auto connection_info = std::make_shared(); EXPECT_CALL(*connection_info, peerCertificatePresented()).WillRepeatedly(Return(false)); + EXPECT_CALL(*connection_info, peerCertificateValidated()).WillRepeatedly(Return(true)); + EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); + + Http::TestHeaderMapImpl headers = + genHeaders("www.lyft.com", "/peer-cert-no-tls-context-match", "GET"); + EXPECT_EQ("server_peer-cert-no-tls-context-match", + config.route(headers, stream_info, 0)->routeEntry()->clusterName()); + } + + { + NiceMock stream_info; + auto connection_info = std::make_shared(); + EXPECT_CALL(*connection_info, peerCertificatePresented()).WillRepeatedly(Return(true)); + EXPECT_CALL(*connection_info, peerCertificateValidated()).WillRepeatedly(Return(true)); + EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); + + Http::TestHeaderMapImpl headers = + genHeaders("www.lyft.com", "/peer-cert-no-tls-context-match", "GET"); + EXPECT_EQ("server_peer-cert-no-tls-context-match", + config.route(headers, stream_info, 0)->routeEntry()->clusterName()); + } + + { + NiceMock stream_info; + auto connection_info = std::make_shared(); + EXPECT_CALL(*connection_info, peerCertificatePresented()).WillRepeatedly(Return(true)); + EXPECT_CALL(*connection_info, peerCertificateValidated()).WillRepeatedly(Return(true)); + EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); + + Http::TestHeaderMapImpl headers = + genHeaders("www.lyft.com", "/peer-validated-cert-test", "GET"); + EXPECT_EQ("server_peer-cert-validated", + config.route(headers, stream_info, 0)->routeEntry()->clusterName()); + } + + { + NiceMock stream_info; + auto connection_info = std::make_shared(); + EXPECT_CALL(*connection_info, peerCertificatePresented()).WillRepeatedly(Return(true)); + EXPECT_CALL(*connection_info, peerCertificateValidated()).WillRepeatedly(Return(false)); + EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); + + Http::TestHeaderMapImpl headers = + genHeaders("www.lyft.com", "/peer-validated-cert-test", "GET"); + EXPECT_EQ("server_peer-cert-not-validated", + config.route(headers, stream_info, 0)->routeEntry()->clusterName()); + } + + { + NiceMock stream_info; + auto connection_info = std::make_shared(); + EXPECT_CALL(*connection_info, peerCertificatePresented()).WillRepeatedly(Return(true)); + EXPECT_CALL(*connection_info, peerCertificateValidated()).WillRepeatedly(Return(false)); EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); Http::TestHeaderMapImpl headers = @@ -6006,6 +6073,7 @@ name: foo NiceMock stream_info; auto connection_info = std::make_shared(); EXPECT_CALL(*connection_info, peerCertificatePresented()).WillRepeatedly(Return(true)); + EXPECT_CALL(*connection_info, peerCertificateValidated()).WillRepeatedly(Return(true)); EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); Http::TestHeaderMapImpl headers = diff --git a/test/extensions/transport_sockets/tls/ssl_socket_test.cc b/test/extensions/transport_sockets/tls/ssl_socket_test.cc index 6918791f1fcc..107e02d4ee4d 100644 --- a/test/extensions/transport_sockets/tls/ssl_socket_test.cc +++ b/test/extensions/transport_sockets/tls/ssl_socket_test.cc @@ -1141,6 +1141,92 @@ TEST_P(SslSocketTest, GetPeerCert) { .setExpectedPeerCert(expected_peer_cert)); } +TEST_P(SslSocketTest, GetPeerCertAcceptUntrusted) { + const std::string client_ctx_yaml = R"EOF( + common_tls_context: + tls_certificates: + certificate_chain: + filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/no_san_cert.pem" + private_key: + filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/no_san_key.pem" +)EOF"; + + const std::string server_ctx_yaml = R"EOF( + common_tls_context: + tls_certificates: + certificate_chain: + filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/san_uri_cert.pem" + private_key: + filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/san_uri_key.pem" + validation_context: + trusted_ca: + filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/fake_ca_cert.pem" + trust_chain_verification: ACCEPT_UNTRUSTED + require_client_certificate: true +)EOF"; + + TestUtilOptions test_options(client_ctx_yaml, server_ctx_yaml, true, GetParam()); + std::string expected_peer_cert = + TestEnvironment::readFileToStringForTest(TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/no_san_cert.pem")); + testUtil(test_options.setExpectedSerialNumber(TEST_NO_SAN_CERT_SERIAL) + .setExpectedPeerIssuer( + "CN=Test CA,OU=Lyft Engineering,O=Lyft,L=San Francisco,ST=California,C=US") + .setExpectedPeerSubject( + "CN=Test Server,OU=Lyft Engineering,O=Lyft,L=San Francisco,ST=California,C=US") + .setExpectedLocalSubject( + "CN=Test Server,OU=Lyft Engineering,O=Lyft,L=San Francisco,ST=California,C=US") + .setExpectedPeerCert(expected_peer_cert)); +} + +TEST_P(SslSocketTest, NoCertUntrustedNotPermitted) { + const std::string client_ctx_yaml = R"EOF( + common_tls_context: + )EOF"; + + const std::string server_ctx_yaml = R"EOF( + common_tls_context: + tls_certificates: + certificate_chain: + filename: "{{ test_tmpdir }}/unittestcert.pem" + private_key: + filename: "{{ test_tmpdir }}/unittestkey.pem" + validation_context: + trusted_ca: + filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/fake_ca_cert.pem" + trust_chain_verification: VERIFY_TRUST_CHAIN + verify_certificate_hash: "0000000000000000000000000000000000000000000000000000000000000000" +)EOF"; + + TestUtilOptions test_options(client_ctx_yaml, server_ctx_yaml, false, GetParam()); + testUtil(test_options.setExpectedServerStats("ssl.fail_verify_no_cert")); +} + +TEST_P(SslSocketTest, NoCertUntrustedPermitted) { + const std::string client_ctx_yaml = R"EOF( + common_tls_context: + )EOF"; + + const std::string server_ctx_yaml = R"EOF( + common_tls_context: + tls_certificates: + certificate_chain: + filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/san_uri_cert.pem" + private_key: + filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/san_uri_key.pem" + validation_context: + trusted_ca: + filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/fake_ca_cert.pem" + trust_chain_verification: ACCEPT_UNTRUSTED + verify_certificate_hash: "0000000000000000000000000000000000000000000000000000000000000000" +)EOF"; + + TestUtilOptions test_options(client_ctx_yaml, server_ctx_yaml, true, GetParam()); + testUtil(test_options.setExpectedServerStats("ssl.no_certificate") + .setExpectNoCert() + .setExpectNoCertChain()); +} + TEST_P(SslSocketTest, GetPeerCertChain) { const std::string client_ctx_yaml = R"EOF( common_tls_context: diff --git a/test/mocks/router/mocks.h b/test/mocks/router/mocks.h index de49258b0010..db5a33795a16 100644 --- a/test/mocks/router/mocks.h +++ b/test/mocks/router/mocks.h @@ -286,6 +286,7 @@ class MockTlsContextMatchCriteria : public TlsContextMatchCriteria { // Router::MockTlsContextMatchCriteria MOCK_METHOD(const absl::optional&, presented, (), (const)); + MOCK_METHOD(const absl::optional&, validated, (), (const)); }; class MockPathMatchCriterion : public PathMatchCriterion { diff --git a/test/mocks/ssl/mocks.h b/test/mocks/ssl/mocks.h index 07f478776a8c..00a6e5697a13 100644 --- a/test/mocks/ssl/mocks.h +++ b/test/mocks/ssl/mocks.h @@ -39,6 +39,7 @@ class MockConnectionInfo : public ConnectionInfo { ~MockConnectionInfo() override; MOCK_METHOD(bool, peerCertificatePresented, (), (const)); + MOCK_METHOD(bool, peerCertificateValidated, (), (const)); MOCK_METHOD(absl::Span, uriSanLocalCertificate, (), (const)); MOCK_METHOD(const std::string&, sha256PeerCertificateDigest, (), (const)); MOCK_METHOD(const std::string&, serialNumberPeerCertificate, (), (const));