From 43b110ab6ec17c80463a50bf6d3ae6077f9fb226 Mon Sep 17 00:00:00 2001 From: danzh Date: Fri, 7 Aug 2020 13:16:29 -0400 Subject: [PATCH] quiche: implement certificate verification (#12063) Implement quic::ProofVerifier which consists of cert verification and signature verification. Cert verification: Share cert verification code with Extensions::TransportSockets::Tls::ClientContextImpl. And initialize ProofVerifier using Envoy::Ssl::ClientContextConfig protobuf. Signature verification: Use quic::CertificateViewer to verify signature. Part of #9434 #2557 Signed-off-by: Dan Zhang --- source/extensions/quic_listeners/quiche/BUILD | 36 ++- .../quiche/envoy_quic_fake_proof_verifier.h | 61 ----- .../quiche/envoy_quic_proof_source.cc | 15 +- .../quiche/envoy_quic_proof_source.h | 19 +- .../quiche/envoy_quic_proof_source_base.cc | 81 ++++++ ...ource.h => envoy_quic_proof_source_base.h} | 44 +-- .../quiche/envoy_quic_proof_verifier.cc | 48 ++++ .../quiche/envoy_quic_proof_verifier.h | 30 +++ .../quiche/envoy_quic_proof_verifier_base.cc | 70 +++++ .../quiche/envoy_quic_proof_verifier_base.h | 47 ++++ .../quic_listeners/quiche/envoy_quic_utils.cc | 61 +++++ .../quic_listeners/quiche/envoy_quic_utils.h | 11 + .../tls/context_config_impl.h | 5 +- .../transport_sockets/tls/context_impl.cc | 79 ++++-- .../transport_sockets/tls/context_impl.h | 9 +- test/extensions/quic_listeners/quiche/BUILD | 27 +- .../quiche/crypto_test_utils_for_envoy.cc | 4 +- .../quiche/envoy_quic_proof_source_test.cc | 221 ++++++++++++--- .../quiche/envoy_quic_proof_verifier_test.cc | 252 ++++++++++++++++++ .../integration/quic_http_integration_test.cc | 75 +++++- .../quic_listeners/quiche/test_proof_source.h | 20 +- .../quiche/test_proof_verifier.h | 30 +++ test/mocks/ssl/mocks.h | 17 ++ tools/spelling/spelling_dictionary.txt | 1 + 24 files changed, 1072 insertions(+), 191 deletions(-) delete mode 100644 source/extensions/quic_listeners/quiche/envoy_quic_fake_proof_verifier.h create mode 100644 source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.cc rename source/extensions/quic_listeners/quiche/{envoy_quic_fake_proof_source.h => envoy_quic_proof_source_base.h} (68%) create mode 100644 source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier.cc create mode 100644 source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier.h create mode 100644 source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.cc create mode 100644 source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.h create mode 100644 test/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_test.cc create mode 100644 test/extensions/quic_listeners/quiche/test_proof_verifier.h diff --git a/source/extensions/quic_listeners/quiche/BUILD b/source/extensions/quic_listeners/quiche/BUILD index 1099eb26deb8..fd2cce9b9b5f 100644 --- a/source/extensions/quic_listeners/quiche/BUILD +++ b/source/extensions/quic_listeners/quiche/BUILD @@ -62,12 +62,17 @@ envoy_cc_library( ) envoy_cc_library( - name = "envoy_quic_fake_proof_source_lib", - hdrs = ["envoy_quic_fake_proof_source.h"], + name = "envoy_quic_proof_source_base_lib", + srcs = ["envoy_quic_proof_source_base.cc"], + hdrs = ["envoy_quic_proof_source_base.h"], external_deps = ["quiche_quic_platform"], tags = ["nofips"], deps = [ + ":envoy_quic_utils_lib", + "@com_googlesource_quiche//:quic_core_crypto_certificate_view_lib", + "@com_googlesource_quiche//:quic_core_crypto_crypto_handshake_lib", "@com_googlesource_quiche//:quic_core_crypto_proof_source_interface_lib", + "@com_googlesource_quiche//:quic_core_data_lib", "@com_googlesource_quiche//:quic_core_versions_lib", ], ) @@ -79,7 +84,7 @@ envoy_cc_library( external_deps = ["ssl"], tags = ["nofips"], deps = [ - ":envoy_quic_fake_proof_source_lib", + ":envoy_quic_proof_source_base_lib", ":envoy_quic_utils_lib", ":quic_io_handle_wrapper_lib", ":quic_transport_socket_factory_lib", @@ -91,16 +96,32 @@ envoy_cc_library( ) envoy_cc_library( - name = "envoy_quic_proof_verifier_lib", - hdrs = ["envoy_quic_fake_proof_verifier.h"], + name = "envoy_quic_proof_verifier_base_lib", + srcs = ["envoy_quic_proof_verifier_base.cc"], + hdrs = ["envoy_quic_proof_verifier_base.h"], external_deps = ["quiche_quic_platform"], tags = ["nofips"], deps = [ + ":envoy_quic_utils_lib", + "@com_googlesource_quiche//:quic_core_crypto_certificate_view_lib", "@com_googlesource_quiche//:quic_core_crypto_crypto_handshake_lib", "@com_googlesource_quiche//:quic_core_versions_lib", ], ) +envoy_cc_library( + name = "envoy_quic_proof_verifier_lib", + srcs = ["envoy_quic_proof_verifier.cc"], + hdrs = ["envoy_quic_proof_verifier.h"], + external_deps = ["quiche_quic_platform"], + tags = ["nofips"], + deps = [ + ":envoy_quic_proof_verifier_base_lib", + ":envoy_quic_utils_lib", + "//source/extensions/transport_sockets/tls:context_lib", + ], +) + envoy_cc_library( name = "spdy_server_push_utils_for_envoy_lib", srcs = ["spdy_server_push_utils_for_envoy.cc"], @@ -323,7 +344,10 @@ envoy_cc_library( name = "envoy_quic_utils_lib", srcs = ["envoy_quic_utils.cc"], hdrs = ["envoy_quic_utils.h"], - external_deps = ["quiche_quic_platform"], + external_deps = [ + "quiche_quic_platform", + "ssl", + ], tags = ["nofips"], deps = [ "//include/envoy/http:codec_interface", diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_fake_proof_verifier.h b/source/extensions/quic_listeners/quiche/envoy_quic_fake_proof_verifier.h deleted file mode 100644 index af107983317b..000000000000 --- a/source/extensions/quic_listeners/quiche/envoy_quic_fake_proof_verifier.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include "absl/strings/str_cat.h" - -#pragma GCC diagnostic push - -// QUICHE allows unused parameters. -#pragma GCC diagnostic ignored "-Wunused-parameter" - -#include "quiche/quic/core/crypto/proof_verifier.h" -#include "quiche/quic/core/quic_versions.h" - -#pragma GCC diagnostic pop - -namespace Envoy { -namespace Quic { - -// A fake implementation of quic::ProofVerifier which approves the certs and -// signature produced by EnvoyQuicFakeProofSource. -class EnvoyQuicFakeProofVerifier : public quic::ProofVerifier { -public: - ~EnvoyQuicFakeProofVerifier() override = default; - - // quic::ProofVerifier - // Return success if the certs chain is valid and signature is "Fake signature for { - // [server_config] }". Otherwise failure. - quic::QuicAsyncStatus - VerifyProof(const std::string& hostname, const uint16_t port, - const std::string& /*server_config*/, quic::QuicTransportVersion /*quic_version*/, - absl::string_view /*chlo_hash*/, const std::vector& certs, - const std::string& cert_sct, const std::string& /*signature*/, - const quic::ProofVerifyContext* context, std::string* error_details, - std::unique_ptr* details, - std::unique_ptr callback) override { - if (VerifyCertChain(hostname, port, certs, "", cert_sct, context, error_details, details, - std::move(callback)) == quic::QUIC_SUCCESS) { - return quic::QUIC_SUCCESS; - } - return quic::QUIC_FAILURE; - } - - // Return success upon one arbitrary cert content. Otherwise failure. - quic::QuicAsyncStatus - VerifyCertChain(const std::string& /*hostname*/, const uint16_t /*port*/, - const std::vector& certs, const std::string& /*ocsp_response*/, - const std::string& cert_sct, const quic::ProofVerifyContext* /*context*/, - std::string* /*error_details*/, - std::unique_ptr* /*details*/, - std::unique_ptr /*callback*/) override { - // Cert SCT support is not enabled for fake ProofSource. - if (cert_sct.empty() && certs.size() == 1) { - return quic::QUIC_SUCCESS; - } - return quic::QUIC_FAILURE; - } - - std::unique_ptr CreateDefaultContext() override { return nullptr; } -}; - -} // namespace Quic -} // namespace Envoy diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_proof_source.cc b/source/extensions/quic_listeners/quiche/envoy_quic_proof_source.cc index 66fe7017436d..96fe056e818e 100644 --- a/source/extensions/quic_listeners/quiche/envoy_quic_proof_source.cc +++ b/source/extensions/quic_listeners/quiche/envoy_quic_proof_source.cc @@ -28,19 +28,13 @@ EnvoyQuicProofSource::GetCertChain(const quic::QuicSocketAddress& server_address } auto& cert_config = cert_config_ref.value().get(); const std::string& chain_str = cert_config.certificateChain(); - std::string pem_str = std::string(const_cast(chain_str.data()), chain_str.size()); std::stringstream pem_stream(chain_str); std::vector chain = quic::CertificateView::LoadPemFromStream(&pem_stream); - if (chain.empty()) { - ENVOY_LOG(warn, "Failed to load certificate chain from %s", cert_config.certificateChainPath()); - return quic::QuicReferenceCountedPointer( - new quic::ProofSource::Chain({})); - } return quic::QuicReferenceCountedPointer( new quic::ProofSource::Chain(chain)); } -void EnvoyQuicProofSource::ComputeTlsSignature( +void EnvoyQuicProofSource::signPayload( const quic::QuicSocketAddress& server_address, const quic::QuicSocketAddress& client_address, const std::string& hostname, uint16_t signature_algorithm, quiche::QuicheStringPiece in, std::unique_ptr callback) { @@ -59,7 +53,11 @@ void EnvoyQuicProofSource::ComputeTlsSignature( std::stringstream pem_str(pkey); std::unique_ptr pem_key = quic::CertificatePrivateKey::LoadPemFromStream(&pem_str); - + if (pem_key == nullptr) { + ENVOY_LOG(warn, "Failed to load private key."); + callback->Run(false, "", nullptr); + return; + } // Sign. std::string sig = pem_key->Sign(in, signature_algorithm); @@ -85,7 +83,6 @@ EnvoyQuicProofSource::getTlsCertConfigAndFilterChain(const quic::QuicSocketAddre const Network::FilterChain* filter_chain = filter_chain_manager_.findFilterChain(connection_socket); if (filter_chain == nullptr) { - ENVOY_LOG(warn, "No matching filter chain found for handshake."); listener_stats_.no_filter_chain_match_.inc(); return {absl::nullopt, absl::nullopt}; } diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_proof_source.h b/source/extensions/quic_listeners/quiche/envoy_quic_proof_source.h index 4dab673687d8..6e1c74c9234c 100644 --- a/source/extensions/quic_listeners/quiche/envoy_quic_proof_source.h +++ b/source/extensions/quic_listeners/quiche/envoy_quic_proof_source.h @@ -2,14 +2,14 @@ #include "server/connection_handler_impl.h" -#include "extensions/quic_listeners/quiche/envoy_quic_fake_proof_source.h" +#include "extensions/quic_listeners/quiche/envoy_quic_proof_source_base.h" #include "extensions/quic_listeners/quiche/quic_transport_socket_factory.h" namespace Envoy { namespace Quic { -class EnvoyQuicProofSource : public EnvoyQuicFakeProofSource, - protected Logger::Loggable { +// A ProofSource implementation which supplies a proof instance with certs from filter chain. +class EnvoyQuicProofSource : public EnvoyQuicProofSourceBase { public: EnvoyQuicProofSource(Network::Socket& listen_socket, Network::FilterChainManager& filter_chain_manager, @@ -19,14 +19,17 @@ class EnvoyQuicProofSource : public EnvoyQuicFakeProofSource, ~EnvoyQuicProofSource() override = default; + // quic::ProofSource quic::QuicReferenceCountedPointer GetCertChain(const quic::QuicSocketAddress& server_address, const quic::QuicSocketAddress& client_address, const std::string& hostname) override; - void ComputeTlsSignature(const quic::QuicSocketAddress& server_address, - const quic::QuicSocketAddress& client_address, - const std::string& hostname, uint16_t signature_algorithm, - quiche::QuicheStringPiece in, - std::unique_ptr callback) override; + +protected: + // quic::ProofSource + void signPayload(const quic::QuicSocketAddress& server_address, + const quic::QuicSocketAddress& client_address, const std::string& hostname, + uint16_t signature_algorithm, quiche::QuicheStringPiece in, + std::unique_ptr callback) override; private: struct CertConfigWithFilterChain { diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.cc b/source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.cc new file mode 100644 index 000000000000..220dc4cb1ccf --- /dev/null +++ b/source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.cc @@ -0,0 +1,81 @@ +#include "extensions/quic_listeners/quiche/envoy_quic_proof_source_base.h" + +#pragma GCC diagnostic push + +// QUICHE allows unused parameters. +#pragma GCC diagnostic ignored "-Wunused-parameter" +#include "quiche/quic/core/quic_data_writer.h" + +#pragma GCC diagnostic pop + +#include "extensions/quic_listeners/quiche/envoy_quic_utils.h" + +namespace Envoy { +namespace Quic { + +void EnvoyQuicProofSourceBase::GetProof(const quic::QuicSocketAddress& server_address, + const quic::QuicSocketAddress& client_address, + const std::string& hostname, + const std::string& server_config, + quic::QuicTransportVersion /*transport_version*/, + quiche::QuicheStringPiece chlo_hash, + std::unique_ptr callback) { + quic::QuicReferenceCountedPointer chain = + GetCertChain(server_address, client_address, hostname); + + if (chain == nullptr || chain->certs.empty()) { + quic::QuicCryptoProof proof; + callback->Run(/*ok=*/false, nullptr, proof, nullptr); + return; + } + size_t payload_size = sizeof(quic::kProofSignatureLabel) + sizeof(uint32_t) + chlo_hash.size() + + server_config.size(); + auto payload = std::make_unique(payload_size); + quic::QuicDataWriter payload_writer(payload_size, payload.get(), + quiche::Endianness::HOST_BYTE_ORDER); + bool success = + payload_writer.WriteBytes(quic::kProofSignatureLabel, sizeof(quic::kProofSignatureLabel)) && + payload_writer.WriteUInt32(chlo_hash.size()) && payload_writer.WriteStringPiece(chlo_hash) && + payload_writer.WriteStringPiece(server_config); + if (!success) { + quic::QuicCryptoProof proof; + callback->Run(/*ok=*/false, nullptr, proof, nullptr); + return; + } + + std::string error_details; + bssl::UniquePtr cert = parseDERCertificate(chain->certs[0], &error_details); + if (cert == nullptr) { + ENVOY_LOG(warn, absl::StrCat("Invalid leaf cert: ", error_details)); + quic::QuicCryptoProof proof; + callback->Run(/*ok=*/false, nullptr, proof, nullptr); + return; + } + + bssl::UniquePtr pub_key(X509_get_pubkey(cert.get())); + int sign_alg = deduceSignatureAlgorithmFromPublicKey(pub_key.get(), &error_details); + if (sign_alg == 0) { + ENVOY_LOG(warn, absl::StrCat("Failed to deduce signature algorithm from public key: ", + error_details)); + quic::QuicCryptoProof proof; + callback->Run(/*ok=*/false, nullptr, proof, nullptr); + return; + } + + auto signature_callback = std::make_unique(std::move(callback), chain); + + signPayload(server_address, client_address, hostname, sign_alg, + quiche::QuicheStringPiece(payload.get(), payload_size), + std::move(signature_callback)); +} + +void EnvoyQuicProofSourceBase::ComputeTlsSignature( + const quic::QuicSocketAddress& server_address, const quic::QuicSocketAddress& client_address, + const std::string& hostname, uint16_t signature_algorithm, quiche::QuicheStringPiece in, + std::unique_ptr callback) { + signPayload(server_address, client_address, hostname, signature_algorithm, in, + std::move(callback)); +} + +} // namespace Quic +} // namespace Envoy diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_fake_proof_source.h b/source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.h similarity index 68% rename from source/extensions/quic_listeners/quiche/envoy_quic_fake_proof_source.h rename to source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.h index f4a2a9466f42..149cc50c7d63 100644 --- a/source/extensions/quic_listeners/quiche/envoy_quic_fake_proof_source.h +++ b/source/extensions/quic_listeners/quiche/envoy_quic_proof_source_base.h @@ -12,14 +12,16 @@ #pragma GCC diagnostic ignored "-Wunused-parameter" #include "quiche/quic/core/crypto/proof_source.h" #include "quiche/quic/core/quic_versions.h" - +#include "quiche/quic/core/crypto/crypto_protocol.h" +#include "quiche/quic/platform/api/quic_reference_counted.h" +#include "quiche/quic/platform/api/quic_socket_address.h" +#include "quiche/common/platform/api/quiche_string_piece.h" #pragma GCC diagnostic pop #include "openssl/ssl.h" #include "envoy/network/filter.h" -#include "quiche/quic/platform/api/quic_reference_counted.h" -#include "quiche/quic/platform/api/quic_socket_address.h" -#include "quiche/common/platform/api/quiche_string_piece.h" +#include "server/backtrace.h" +#include "common/common/logger.h" namespace Envoy { namespace Quic { @@ -38,11 +40,12 @@ class EnvoyQuicProofSourceDetails : public quic::ProofSource::Details { const Network::FilterChain& filter_chain_; }; -// A fake implementation of quic::ProofSource which uses RSA cipher suite to sign in GetProof(). -// TODO(danzh) Rename it to EnvoyQuicProofSource once it's fully implemented. -class EnvoyQuicFakeProofSource : public quic::ProofSource { +// A partial implementation of quic::ProofSource which chooses a cipher suite according to the leaf +// cert to sign in GetProof(). +class EnvoyQuicProofSourceBase : public quic::ProofSource, + protected Logger::Loggable { public: - ~EnvoyQuicFakeProofSource() override = default; + ~EnvoyQuicProofSourceBase() override = default; // quic::ProofSource // Returns a certs chain and its fake SCT "Fake timestamp" and TLS signature wrapped @@ -50,19 +53,24 @@ class EnvoyQuicFakeProofSource : public quic::ProofSource { void GetProof(const quic::QuicSocketAddress& server_address, const quic::QuicSocketAddress& client_address, const std::string& hostname, const std::string& server_config, quic::QuicTransportVersion /*transport_version*/, - quiche::QuicheStringPiece /*chlo_hash*/, - std::unique_ptr callback) override { - quic::QuicReferenceCountedPointer chain = - GetCertChain(server_address, client_address, hostname); - quic::QuicCryptoProof proof; - // TODO(danzh) Get the signature algorithm from leaf cert. - auto signature_callback = std::make_unique(std::move(callback), chain); - ComputeTlsSignature(server_address, client_address, hostname, SSL_SIGN_RSA_PSS_RSAE_SHA256, - server_config, std::move(signature_callback)); - } + quiche::QuicheStringPiece chlo_hash, + std::unique_ptr callback) override; TicketCrypter* GetTicketCrypter() override { return nullptr; } + void ComputeTlsSignature(const quic::QuicSocketAddress& server_address, + const quic::QuicSocketAddress& client_address, + const std::string& hostname, uint16_t signature_algorithm, + quiche::QuicheStringPiece in, + std::unique_ptr callback) override; + +protected: + virtual void signPayload(const quic::QuicSocketAddress& server_address, + const quic::QuicSocketAddress& client_address, + const std::string& hostname, uint16_t signature_algorithm, + quiche::QuicheStringPiece in, + std::unique_ptr callback) PURE; + private: // Used by GetProof() to get signature. class SignatureCallback : public quic::ProofSource::SignatureCallback { diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier.cc b/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier.cc new file mode 100644 index 000000000000..b7040d1279d7 --- /dev/null +++ b/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier.cc @@ -0,0 +1,48 @@ +#include "extensions/quic_listeners/quiche/envoy_quic_proof_verifier.h" + +#include "extensions/quic_listeners/quiche/envoy_quic_utils.h" + +#include "quiche/quic/core/crypto/certificate_view.h" + +namespace Envoy { +namespace Quic { + +quic::QuicAsyncStatus EnvoyQuicProofVerifier::VerifyCertChain( + const std::string& hostname, const uint16_t /*port*/, const std::vector& certs, + const std::string& /*ocsp_response*/, const std::string& /*cert_sct*/, + const quic::ProofVerifyContext* /*context*/, std::string* error_details, + std::unique_ptr* /*details*/, + std::unique_ptr /*callback*/) { + ASSERT(!certs.empty()); + bssl::UniquePtr intermediates(sk_X509_new_null()); + bssl::UniquePtr leaf; + for (size_t i = 0; i < certs.size(); i++) { + bssl::UniquePtr cert = parseDERCertificate(certs[i], error_details); + if (!cert) { + return quic::QUIC_FAILURE; + } + if (i == 0) { + leaf = std::move(cert); + } else { + sk_X509_push(intermediates.get(), cert.release()); + } + } + bool success = context_impl_.verifyCertChain(*leaf, *intermediates, *error_details); + if (!success) { + return quic::QUIC_FAILURE; + } + + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(certs[0]); + ASSERT(cert_view != nullptr); + for (const absl::string_view config_san : cert_view->subject_alt_name_domains()) { + if (Extensions::TransportSockets::Tls::ContextImpl::dnsNameMatch(hostname, config_san)) { + return quic::QUIC_SUCCESS; + } + } + *error_details = absl::StrCat("Leaf certificate doesn't match hostname: ", hostname); + return quic::QUIC_FAILURE; +} + +} // namespace Quic +} // namespace Envoy diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier.h b/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier.h new file mode 100644 index 000000000000..a29eb999119f --- /dev/null +++ b/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier.h @@ -0,0 +1,30 @@ +#pragma once + +#include "extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.h" +#include "extensions/transport_sockets/tls/context_impl.h" + +namespace Envoy { +namespace Quic { + +// A quic::ProofVerifier implementation which verifies cert chain using SSL +// client context config. +class EnvoyQuicProofVerifier : public EnvoyQuicProofVerifierBase { +public: + EnvoyQuicProofVerifier(Stats::Scope& scope, const Envoy::Ssl::ClientContextConfig& config, + TimeSource& time_source) + : context_impl_(scope, config, time_source) {} + + // EnvoyQuicProofVerifierBase + quic::QuicAsyncStatus + VerifyCertChain(const std::string& hostname, const uint16_t port, + const std::vector& certs, const std::string& ocsp_response, + const std::string& cert_sct, const quic::ProofVerifyContext* context, + std::string* error_details, std::unique_ptr* details, + std::unique_ptr callback) override; + +private: + Extensions::TransportSockets::Tls::ClientContextImpl context_impl_; +}; + +} // namespace Quic +} // namespace Envoy diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.cc b/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.cc new file mode 100644 index 000000000000..229b3ab36628 --- /dev/null +++ b/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.cc @@ -0,0 +1,70 @@ +#include "extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.h" + +#include "extensions/quic_listeners/quiche/envoy_quic_utils.h" + +#include "openssl/ssl.h" +#include "quiche/quic/core/crypto/certificate_view.h" +#include "quiche/quic/core/crypto/crypto_protocol.h" +#include "quiche/quic/core/quic_data_writer.h" + +namespace Envoy { +namespace Quic { + +quic::QuicAsyncStatus EnvoyQuicProofVerifierBase::VerifyProof( + const std::string& hostname, const uint16_t port, const std::string& server_config, + quic::QuicTransportVersion /*quic_version*/, absl::string_view chlo_hash, + const std::vector& certs, const std::string& cert_sct, + const std::string& signature, const quic::ProofVerifyContext* context, + std::string* error_details, std::unique_ptr* details, + std::unique_ptr callback) { + if (certs.empty()) { + *error_details = "Received empty cert chain."; + return quic::QUIC_FAILURE; + } + if (!verifySignature(server_config, chlo_hash, certs[0], signature, error_details)) { + return quic::QUIC_FAILURE; + } + + return VerifyCertChain(hostname, port, certs, "", cert_sct, context, error_details, details, + std::move(callback)); +} + +bool EnvoyQuicProofVerifierBase::verifySignature(const std::string& server_config, + absl::string_view chlo_hash, + const std::string& cert, + const std::string& signature, + std::string* error_details) { + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(cert); + if (cert_view == nullptr) { + *error_details = "Invalid leaf cert."; + return false; + } + int sign_alg = deduceSignatureAlgorithmFromPublicKey(cert_view->public_key(), error_details); + if (sign_alg == 0) { + return false; + } + + size_t payload_size = sizeof(quic::kProofSignatureLabel) + sizeof(uint32_t) + chlo_hash.size() + + server_config.size(); + auto payload = std::make_unique(payload_size); + quic::QuicDataWriter payload_writer(payload_size, payload.get(), + quiche::Endianness::HOST_BYTE_ORDER); + bool success = + payload_writer.WriteBytes(quic::kProofSignatureLabel, sizeof(quic::kProofSignatureLabel)) && + payload_writer.WriteUInt32(chlo_hash.size()) && payload_writer.WriteStringPiece(chlo_hash) && + payload_writer.WriteStringPiece(server_config); + if (!success) { + *error_details = "QuicPacketWriter error."; + return false; + } + bool valid = cert_view->VerifySignature(quiche::QuicheStringPiece(payload.get(), payload_size), + signature, sign_alg); + if (!valid) { + *error_details = "Signature is not valid."; + } + return valid; +} + +} // namespace Quic +} // namespace Envoy diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.h b/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.h new file mode 100644 index 000000000000..02dac5facd42 --- /dev/null +++ b/source/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.h @@ -0,0 +1,47 @@ +#pragma once + +#include "absl/strings/str_cat.h" + +#pragma GCC diagnostic push + +// QUICHE allows unused parameters. +#pragma GCC diagnostic ignored "-Wunused-parameter" + +#include "quiche/quic/core/crypto/proof_verifier.h" +#include "quiche/quic/core/quic_versions.h" + +#pragma GCC diagnostic pop + +#include "common/common/logger.h" + +namespace Envoy { +namespace Quic { + +// A partial implementation of quic::ProofVerifier which does signature +// verification. +class EnvoyQuicProofVerifierBase : public quic::ProofVerifier, + protected Logger::Loggable { +public: + ~EnvoyQuicProofVerifierBase() override = default; + + // quic::ProofVerifier + // Return success if the certs chain is valid and signature of { + // server_config + chlo_hash} is valid. Otherwise failure. + quic::QuicAsyncStatus + VerifyProof(const std::string& hostname, const uint16_t port, const std::string& server_config, + quic::QuicTransportVersion /*quic_version*/, absl::string_view chlo_hash, + const std::vector& certs, const std::string& cert_sct, + const std::string& signature, const quic::ProofVerifyContext* context, + std::string* error_details, std::unique_ptr* details, + std::unique_ptr callback) override; + + std::unique_ptr CreateDefaultContext() override { return nullptr; } + +protected: + virtual bool verifySignature(const std::string& server_config, absl::string_view chlo_hash, + const std::string& cert, const std::string& signature, + std::string* error_details); +}; + +} // namespace Quic +} // namespace Envoy diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_utils.cc b/source/extensions/quic_listeners/quiche/envoy_quic_utils.cc index aefb6a860e5e..b5c710a81269 100644 --- a/source/extensions/quic_listeners/quiche/envoy_quic_utils.cc +++ b/source/extensions/quic_listeners/quiche/envoy_quic_utils.cc @@ -124,5 +124,66 @@ createConnectionSocket(Network::Address::InstanceConstSharedPtr& peer_addr, return connection_socket; } +bssl::UniquePtr parseDERCertificate(const std::string& der_bytes, + std::string* error_details) { + const uint8_t* data; + const uint8_t* orig_data; + orig_data = data = reinterpret_cast(der_bytes.data()); + bssl::UniquePtr cert(d2i_X509(nullptr, &data, der_bytes.size())); + if (!cert.get()) { + *error_details = "d2i_X509: fail to parse DER"; + return nullptr; + } + if (data < orig_data || static_cast(data - orig_data) != der_bytes.size()) { + *error_details = "There is trailing garbage in DER."; + return nullptr; + } + return cert; +} + +int deduceSignatureAlgorithmFromPublicKey(const EVP_PKEY* public_key, std::string* error_details) { + int sign_alg = 0; + const int pkey_id = EVP_PKEY_id(public_key); + switch (pkey_id) { + case EVP_PKEY_EC: { + // We only support P-256 ECDSA today. + const EC_KEY* ecdsa_public_key = EVP_PKEY_get0_EC_KEY(public_key); + // Since we checked the key type above, this should be valid. + ASSERT(ecdsa_public_key != nullptr); + const EC_GROUP* ecdsa_group = EC_KEY_get0_group(ecdsa_public_key); + if (ecdsa_group == nullptr || EC_GROUP_get_curve_name(ecdsa_group) != NID_X9_62_prime256v1) { + *error_details = "Invalid leaf cert, only P-256 ECDSA certificates are supported"; + break; + } + // QUICHE uses SHA-256 as hash function in cert signature. + sign_alg = SSL_SIGN_ECDSA_SECP256R1_SHA256; + } break; + case EVP_PKEY_RSA: { + // We require RSA certificates with 2048-bit or larger keys. + const RSA* rsa_public_key = EVP_PKEY_get0_RSA(public_key); + // Since we checked the key type above, this should be valid. + ASSERT(rsa_public_key != nullptr); + const unsigned rsa_key_length = RSA_size(rsa_public_key); +#ifdef BORINGSSL_FIPS + if (rsa_key_length != 2048 / 8 && rsa_key_length != 3072 / 8) { + *error_details = "Invalid leaf cert, only RSA certificates with 2048-bit or 3072-bit keys " + "are supported in FIPS mode"; + break; + } +#else + if (rsa_key_length < 2048 / 8) { + *error_details = + "Invalid leaf cert, only RSA certificates with 2048-bit or larger keys are supported"; + break; + } +#endif + sign_alg = SSL_SIGN_RSA_PSS_RSAE_SHA256; + } break; + default: + *error_details = "Invalid leaf cert, only RSA and ECDSA certificates are supported"; + } + return sign_alg; +} + } // namespace Quic } // namespace Envoy diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_utils.h b/source/extensions/quic_listeners/quiche/envoy_quic_utils.h index f5714ef15b83..34dce87d836b 100644 --- a/source/extensions/quic_listeners/quiche/envoy_quic_utils.h +++ b/source/extensions/quic_listeners/quiche/envoy_quic_utils.h @@ -24,6 +24,8 @@ #include "quiche/quic/platform/api/quic_ip_address.h" #include "quiche/quic/platform/api/quic_socket_address.h" +#include "openssl/ssl.h" + namespace Envoy { namespace Quic { @@ -80,5 +82,14 @@ createConnectionSocket(Network::Address::InstanceConstSharedPtr& peer_addr, Network::Address::InstanceConstSharedPtr& local_addr, const Network::ConnectionSocket::OptionsSharedPtr& options); +// Convert a cert in string form to X509 object. +// Return nullptr if the bytes passed cannot be passed. +bssl::UniquePtr parseDERCertificate(const std::string& der_bytes, std::string* error_details); + +// Deduce the suitable signature algorithm according to the public key. +// Return the sign algorithm id works with the public key; If the public key is +// not supported, return 0 with error_details populated correspondingly. +int deduceSignatureAlgorithmFromPublicKey(const EVP_PKEY* public_key, std::string* error_details); + } // namespace Quic } // namespace Envoy diff --git a/source/extensions/transport_sockets/tls/context_config_impl.h b/source/extensions/transport_sockets/tls/context_config_impl.h index 9cfaff0482fb..ad2d927d8231 100644 --- a/source/extensions/transport_sockets/tls/context_config_impl.h +++ b/source/extensions/transport_sockets/tls/context_config_impl.h @@ -98,6 +98,9 @@ class ContextConfigImpl : public virtual Ssl::ContextConfig { class ClientContextConfigImpl : public ContextConfigImpl, public Envoy::Ssl::ClientContextConfig { public: + static const std::string DEFAULT_CIPHER_SUITES; + static const std::string DEFAULT_CURVES; + ClientContextConfigImpl( const envoy::extensions::transport_sockets::tls::v3::UpstreamTlsContext& config, absl::string_view sigalgs, @@ -116,8 +119,6 @@ class ClientContextConfigImpl : public ContextConfigImpl, public Envoy::Ssl::Cli private: static const unsigned DEFAULT_MIN_VERSION; static const unsigned DEFAULT_MAX_VERSION; - static const std::string DEFAULT_CIPHER_SUITES; - static const std::string DEFAULT_CURVES; const std::string server_name_indication_; const bool allow_renegotiation_; diff --git a/source/extensions/transport_sockets/tls/context_impl.cc b/source/extensions/transport_sockets/tls/context_impl.cc index 369bdd460f98..502739958e50 100644 --- a/source/extensions/transport_sockets/tls/context_impl.cc +++ b/source/extensions/transport_sockets/tls/context_impl.cc @@ -527,49 +527,50 @@ 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 = + auto cert = bssl::UniquePtr(SSL_get_peer_certificate(ssl)); + return impl->doVerifyCertChain( + store_ctx, reinterpret_cast( - SSL_get_ex_data(ssl, ContextImpl::sslExtendedSocketInfoIndex())); + SSL_get_ex_data(ssl, ContextImpl::sslExtendedSocketInfoIndex())), + *cert, static_cast(SSL_get_app_data(ssl))); +} - if (impl->verify_trusted_ca_) { +int ContextImpl::doVerifyCertChain( + X509_STORE_CTX* store_ctx, Ssl::SslExtendedSocketInfo* ssl_extended_info, X509& leaf_cert, + const Network::TransportSocketOptions* transport_socket_options) { + if (verify_trusted_ca_) { int ret = X509_verify_cert(store_ctx); - if (sslExtendedInfo) { - sslExtendedInfo->setCertificateValidationStatus( + if (ssl_extended_info) { + ssl_extended_info->setCertificateValidationStatus( ret == 1 ? Envoy::Ssl::ClientValidationStatus::Validated : Envoy::Ssl::ClientValidationStatus::Failed); } if (ret <= 0) { - impl->stats_.fail_verify_error_.inc(); - return impl->allow_untrusted_certificate_ ? 1 : ret; + stats_.fail_verify_error_.inc(); + return allow_untrusted_certificate_ ? 1 : ret; } } - bssl::UniquePtr cert(SSL_get_peer_certificate(ssl)); - - const Network::TransportSocketOptions* transport_socket_options = - static_cast(SSL_get_app_data(ssl)); - - Envoy::Ssl::ClientValidationStatus validated = impl->verifyCertificate( - cert.get(), + Envoy::Ssl::ClientValidationStatus validated = verifyCertificate( + &leaf_cert, transport_socket_options && !transport_socket_options->verifySubjectAltNameListOverride().empty() ? transport_socket_options->verifySubjectAltNameListOverride() - : impl->verify_subject_alt_name_list_, - impl->subject_alt_name_matchers_); + : verify_subject_alt_name_list_, + subject_alt_name_matchers_); - if (sslExtendedInfo) { - if (sslExtendedInfo->certificateValidationStatus() == + if (ssl_extended_info) { + if (ssl_extended_info->certificateValidationStatus() == Envoy::Ssl::ClientValidationStatus::NotValidated) { - sslExtendedInfo->setCertificateValidationStatus(validated); + ssl_extended_info->setCertificateValidationStatus(validated); } else if (validated != Envoy::Ssl::ClientValidationStatus::NotValidated) { - sslExtendedInfo->setCertificateValidationStatus(validated); + ssl_extended_info->setCertificateValidationStatus(validated); } } - return impl->allow_untrusted_certificate_ - ? 1 - : (validated != Envoy::Ssl::ClientValidationStatus::Failed); + return allow_untrusted_certificate_ ? 1 + : (validated != Envoy::Ssl::ClientValidationStatus::Failed); } Envoy::Ssl::ClientValidationStatus ContextImpl::verifyCertificate( @@ -675,7 +676,7 @@ bool ContextImpl::matchSubjectAltName( if (general_name->type == GEN_DNS && config_san_matcher.matcher().match_pattern_case() == envoy::type::matcher::v3::StringMatcher::MatchPatternCase::kExact - ? dnsNameMatch(config_san_matcher.matcher().exact(), san.c_str()) + ? dnsNameMatch(config_san_matcher.matcher().exact(), absl::string_view(san)) : config_san_matcher.match(san)) { return true; } @@ -703,20 +704,20 @@ bool ContextImpl::verifySubjectAltName(X509* cert, return false; } -bool ContextImpl::dnsNameMatch(const std::string& dns_name, const char* pattern) { +bool ContextImpl::dnsNameMatch(const absl::string_view dns_name, const absl::string_view pattern) { if (dns_name == pattern) { return true; } - size_t pattern_len = strlen(pattern); + size_t pattern_len = pattern.length(); if (pattern_len > 1 && pattern[0] == '*' && pattern[1] == '.') { if (dns_name.length() > pattern_len - 1) { const size_t off = dns_name.length() - pattern_len + 1; if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.fix_wildcard_matching")) { return dns_name.substr(0, off).find('.') == std::string::npos && - dns_name.compare(off, pattern_len - 1, pattern + 1) == 0; + dns_name.substr(off, pattern_len - 1) == pattern.substr(1, pattern_len - 1); } else { - return dns_name.compare(off, pattern_len - 1, pattern + 1) == 0; + return dns_name.substr(off, pattern_len - 1) == pattern.substr(1, pattern_len - 1); } } } @@ -1394,6 +1395,28 @@ bool ServerContextImpl::TlsContext::isCipherEnabled(uint16_t cipher_id, uint16_t return false; } +bool ContextImpl::verifyCertChain(X509& leaf_cert, STACK_OF(X509) & intermediates, + std::string& error_details) { + bssl::UniquePtr ctx(X509_STORE_CTX_new()); + // It doesn't matter which SSL context is used, because they share the same + // cert validation config. + X509_STORE* store = SSL_CTX_get_cert_store(tls_contexts_[0].ssl_ctx_.get()); + if (!X509_STORE_CTX_init(ctx.get(), store, &leaf_cert, &intermediates)) { + error_details = "Failed to verify certificate chain: X509_STORE_CTX_init"; + return false; + } + + int res = doVerifyCertChain(ctx.get(), nullptr, leaf_cert, nullptr); + if (res <= 0) { + const int n = X509_STORE_CTX_get_error(ctx.get()); + const int depth = X509_STORE_CTX_get_error_depth(ctx.get()); + error_details = absl::StrCat("X509_verify_cert: certificate verification error at depth ", + depth, ": ", X509_verify_cert_error_string(n)); + return false; + } + return true; +} + } // namespace Tls } // namespace TransportSockets } // namespace Extensions diff --git a/source/extensions/transport_sockets/tls/context_impl.h b/source/extensions/transport_sockets/tls/context_impl.h index 407dd45f86f8..5ea35a48228e 100644 --- a/source/extensions/transport_sockets/tls/context_impl.h +++ b/source/extensions/transport_sockets/tls/context_impl.h @@ -84,7 +84,7 @@ class ContextImpl : public virtual Envoy::Ssl::Context { * @param pattern the pattern to match against (*.example.com) * @return true if the san matches pattern */ - static bool dnsNameMatch(const std::string& dns_name, const char* pattern); + static bool dnsNameMatch(const absl::string_view dns_name, const absl::string_view pattern); SslStats& stats() { return stats_; } @@ -101,6 +101,8 @@ class ContextImpl : public virtual Envoy::Ssl::Context { std::vector getPrivateKeyMethodProviders(); + bool verifyCertChain(X509& leaf_cert, STACK_OF(X509) & intermediates, std::string& error_details); + protected: ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& config, TimeSource& time_source); @@ -117,6 +119,11 @@ 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); + // Called by verifyCallback to do the actual cert chain verification. + int doVerifyCertChain(X509_STORE_CTX* store_ctx, Ssl::SslExtendedSocketInfo* ssl_extended_info, + X509& leaf_cert, + const Network::TransportSocketOptions* transport_socket_options); + Envoy::Ssl::ClientValidationStatus verifyCertificate(X509* cert, const std::vector& verify_san_list, const std::vector& subject_alt_name_matchers); diff --git a/test/extensions/quic_listeners/quiche/BUILD b/test/extensions/quic_listeners/quiche/BUILD index b3c4eb70698d..29ae0a89eb28 100644 --- a/test/extensions/quic_listeners/quiche/BUILD +++ b/test/extensions/quic_listeners/quiche/BUILD @@ -49,6 +49,7 @@ envoy_cc_test( deps = [ "//source/extensions/quic_listeners/quiche:envoy_quic_proof_source_lib", "//source/extensions/quic_listeners/quiche:envoy_quic_proof_verifier_lib", + "//source/extensions/transport_sockets/tls:context_config_lib", "//test/mocks/network:network_mocks", "//test/mocks/ssl:ssl_mocks", "@com_googlesource_quiche//:quic_core_versions_lib", @@ -56,6 +57,19 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "envoy_quic_proof_verifier_test", + srcs = ["envoy_quic_proof_verifier_test.cc"], + external_deps = ["quiche_quic_platform"], + tags = ["nofips"], + deps = [ + "//source/extensions/quic_listeners/quiche:envoy_quic_proof_verifier_lib", + "//source/extensions/transport_sockets/tls:context_config_lib", + "//test/mocks/ssl:ssl_mocks", + "@com_googlesource_quiche//:quic_test_tools_test_certificates_lib", + ], +) + envoy_cc_test( name = "envoy_quic_server_stream_test", srcs = ["envoy_quic_server_stream_test.cc"], @@ -221,19 +235,28 @@ envoy_cc_test_library( hdrs = ["test_proof_source.h"], tags = ["nofips"], deps = [ - "//source/extensions/quic_listeners/quiche:envoy_quic_fake_proof_source_lib", + "//source/extensions/quic_listeners/quiche:envoy_quic_proof_source_base_lib", "//test/mocks/network:network_mocks", "@com_googlesource_quiche//:quic_test_tools_test_certificates_lib", ], ) +envoy_cc_test_library( + name = "test_proof_verifier_lib", + hdrs = ["test_proof_verifier.h"], + tags = ["nofips"], + deps = [ + "//source/extensions/quic_listeners/quiche:envoy_quic_proof_verifier_base_lib", + ], +) + envoy_cc_test_library( name = "quic_test_utils_for_envoy_lib", srcs = ["crypto_test_utils_for_envoy.cc"], tags = ["nofips"], deps = [ ":test_proof_source_lib", - "//source/extensions/quic_listeners/quiche:envoy_quic_proof_verifier_lib", + ":test_proof_verifier_lib", "@com_googlesource_quiche//:quic_test_tools_test_utils_interface_lib", ], ) diff --git a/test/extensions/quic_listeners/quiche/crypto_test_utils_for_envoy.cc b/test/extensions/quic_listeners/quiche/crypto_test_utils_for_envoy.cc index c5b7a11d70e3..cafdce0c6227 100644 --- a/test/extensions/quic_listeners/quiche/crypto_test_utils_for_envoy.cc +++ b/test/extensions/quic_listeners/quiche/crypto_test_utils_for_envoy.cc @@ -19,7 +19,7 @@ #endif #include -#include "extensions/quic_listeners/quiche/envoy_quic_fake_proof_verifier.h" +#include "test/extensions/quic_listeners/quiche/test_proof_verifier.h" #include "test/extensions/quic_listeners/quiche/test_proof_source.h" namespace quic { @@ -32,7 +32,7 @@ std::unique_ptr ProofSourceForTesting() { // NOLINTNEXTLINE(readability-identifier-naming) std::unique_ptr ProofVerifierForTesting() { - return std::make_unique(); + return std::make_unique(); } // NOLINTNEXTLINE(readability-identifier-naming) diff --git a/test/extensions/quic_listeners/quiche/envoy_quic_proof_source_test.cc b/test/extensions/quic_listeners/quiche/envoy_quic_proof_source_test.cc index e61e34eac270..d896dbb86b7c 100644 --- a/test/extensions/quic_listeners/quiche/envoy_quic_proof_source_test.cc +++ b/test/extensions/quic_listeners/quiche/envoy_quic_proof_source_test.cc @@ -2,9 +2,10 @@ #include #include -#include "extensions/quic_listeners/quiche/envoy_quic_fake_proof_verifier.h" #include "extensions/quic_listeners/quiche/envoy_quic_proof_source.h" +#include "extensions/quic_listeners/quiche/envoy_quic_proof_verifier.h" #include "extensions/quic_listeners/quiche/envoy_quic_utils.h" +#include "extensions/transport_sockets/tls/context_config_impl.h" #include "test/mocks/network/mocks.h" #include "test/mocks/ssl/mocks.h" @@ -23,29 +24,105 @@ namespace Quic { class TestGetProofCallback : public quic::ProofSource::Callback { public: - TestGetProofCallback(bool& called, std::string leaf_cert_scts, const absl::string_view cert, + TestGetProofCallback(bool& called, bool should_succeed, const std::string& server_config, + quic::QuicTransportVersion& version, quiche::QuicheStringPiece chlo_hash, Network::FilterChain& filter_chain) - : called_(called), expected_leaf_certs_scts_(std::move(leaf_cert_scts)), - expected_leaf_cert_(cert), expected_filter_chain_(filter_chain) {} + : called_(called), should_succeed_(should_succeed), server_config_(server_config), + version_(version), chlo_hash_(chlo_hash), expected_filter_chain_(filter_chain) { + ON_CALL(client_context_config_, cipherSuites) + .WillByDefault(ReturnRef( + Extensions::TransportSockets::Tls::ClientContextConfigImpl::DEFAULT_CIPHER_SUITES)); + ON_CALL(client_context_config_, ecdhCurves) + .WillByDefault( + ReturnRef(Extensions::TransportSockets::Tls::ClientContextConfigImpl::DEFAULT_CURVES)); + const std::string alpn("h2,http/1.1"); + ON_CALL(client_context_config_, alpnProtocols()).WillByDefault(ReturnRef(alpn)); + const std::string empty_string; + ON_CALL(client_context_config_, serverNameIndication()).WillByDefault(ReturnRef(empty_string)); + ON_CALL(client_context_config_, signingAlgorithmsForTest()) + .WillByDefault(ReturnRef(empty_string)); + ON_CALL(client_context_config_, certificateValidationContext()) + .WillByDefault(Return(&cert_validation_ctx_config_)); + + // Getting the last cert in the chain as the root CA cert. + std::string cert_chain(quic::test::kTestCertificateChainPem); + const std::string& root_ca_cert = + cert_chain.substr(cert_chain.rfind("-----BEGIN CERTIFICATE-----")); + const std::string path_string("some_path"); + ON_CALL(cert_validation_ctx_config_, caCert()).WillByDefault(ReturnRef(root_ca_cert)); + ON_CALL(cert_validation_ctx_config_, caCertPath()).WillByDefault(ReturnRef(path_string)); + ON_CALL(cert_validation_ctx_config_, trustChainVerification) + .WillByDefault(Return(envoy::extensions::transport_sockets::tls::v3:: + CertificateValidationContext::VERIFY_TRUST_CHAIN)); + ON_CALL(cert_validation_ctx_config_, allowExpiredCertificate()).WillByDefault(Return(true)); + const std::string crl_list; + ON_CALL(cert_validation_ctx_config_, certificateRevocationList()) + .WillByDefault(ReturnRef(crl_list)); + ON_CALL(cert_validation_ctx_config_, certificateRevocationListPath()) + .WillByDefault(ReturnRef(path_string)); + const std::vector empty_string_list; + ON_CALL(cert_validation_ctx_config_, verifySubjectAltNameList()) + .WillByDefault(ReturnRef(empty_string_list)); + const std::vector san_matchers; + ON_CALL(cert_validation_ctx_config_, subjectAltNameMatchers()) + .WillByDefault(ReturnRef(san_matchers)); + ON_CALL(cert_validation_ctx_config_, verifyCertificateHashList()) + .WillByDefault(ReturnRef(empty_string_list)); + ON_CALL(cert_validation_ctx_config_, verifyCertificateSpkiList()) + .WillByDefault(ReturnRef(empty_string_list)); + verifier_ = + std::make_unique(store_, client_context_config_, time_system_); + } // quic::ProofSource::Callback void Run(bool ok, const quic::QuicReferenceCountedPointer& chain, const quic::QuicCryptoProof& proof, std::unique_ptr details) override { + called_ = true; + if (!should_succeed_) { + EXPECT_FALSE(ok); + return; + }; EXPECT_TRUE(ok); - EXPECT_EQ(expected_leaf_certs_scts_, proof.leaf_cert_scts); EXPECT_EQ(2, chain->certs.size()); - EXPECT_EQ(expected_leaf_cert_, chain->certs[0]); + std::string error; + EXPECT_EQ(quic::QUIC_SUCCESS, + verifier_->VerifyProof("www.example.org", 54321, server_config_, version_, chlo_hash_, + chain->certs, proof.leaf_cert_scts, proof.signature, nullptr, + &error, nullptr, nullptr)) + << error; EXPECT_EQ(&expected_filter_chain_, &static_cast(details.get())->filterChain()); - called_ = true; } private: bool& called_; - std::string expected_leaf_certs_scts_; - absl::string_view expected_leaf_cert_; + bool should_succeed_; + const std::string& server_config_; + const quic::QuicTransportVersion& version_; + quiche::QuicheStringPiece chlo_hash_; Network::FilterChain& expected_filter_chain_; + NiceMock store_; + Event::GlobalTimeSystem time_system_; + NiceMock client_context_config_; + NiceMock cert_validation_ctx_config_; + std::unique_ptr verifier_; +}; + +class TestSignatureCallback : public quic::ProofSource::SignatureCallback { +public: + TestSignatureCallback(bool expect_success) : expect_success_(expect_success) {} + ~TestSignatureCallback() override { EXPECT_TRUE(run_called_); } + + // quic::ProofSource::SignatureCallback + void Run(bool ok, std::string, std::unique_ptr) override { + EXPECT_EQ(expect_success_, ok); + run_called_ = true; + } + +private: + bool expect_success_; + bool run_called_{false}; }; class EnvoyQuicProofSourceTest : public ::testing::Test { @@ -53,17 +130,55 @@ class EnvoyQuicProofSourceTest : public ::testing::Test { EnvoyQuicProofSourceTest() : server_address_(quic::QuicIpAddress::Loopback4(), 12345), client_address_(quic::QuicIpAddress::Loopback4(), 54321), + transport_socket_factory_(std::make_unique()), listener_stats_({ALL_LISTENER_STATS(POOL_COUNTER(listener_config_.listenerScope()), POOL_GAUGE(listener_config_.listenerScope()), POOL_HISTOGRAM(listener_config_.listenerScope()))}), proof_source_(listen_socket_, filter_chain_manager_, listener_stats_) {} + void expectCertChainAndPrivateKey(const std::string& cert, bool expect_private_key) { + EXPECT_CALL(listen_socket_, ioHandle()).Times(expect_private_key ? 2u : 1u); + EXPECT_CALL(filter_chain_manager_, findFilterChain(_)) + .WillRepeatedly(Invoke([&](const Network::ConnectionSocket& connection_socket) { + EXPECT_EQ(*quicAddressToEnvoyAddressInstance(server_address_), + *connection_socket.localAddress()); + EXPECT_EQ(*quicAddressToEnvoyAddressInstance(client_address_), + *connection_socket.remoteAddress()); + EXPECT_EQ(Extensions::TransportSockets::TransportProtocolNames::get().Quic, + connection_socket.detectedTransportProtocol()); + EXPECT_EQ("h2", connection_socket.requestedApplicationProtocols()[0]); + return &filter_chain_; + })); + EXPECT_CALL(filter_chain_, transportSocketFactory()) + .WillRepeatedly(ReturnRef(transport_socket_factory_)); + + std::vector> tls_cert_configs{ + std::reference_wrapper(tls_cert_config_)}; + EXPECT_CALL(dynamic_cast( + transport_socket_factory_.serverContextConfig()), + tlsCertificates()) + .WillRepeatedly(Return(tls_cert_configs)); + EXPECT_CALL(tls_cert_config_, certificateChain()).WillOnce(ReturnRef(cert)); + if (expect_private_key) { + EXPECT_CALL(tls_cert_config_, privateKey()).WillOnce(ReturnRef(pkey_)); + } + } + + void testGetProof(bool expect_success) { + bool called = false; + auto callback = std::make_unique(called, expect_success, server_config_, + version_, chlo_hash_, filter_chain_); + proof_source_.GetProof(server_address_, client_address_, hostname_, server_config_, version_, + chlo_hash_, std::move(callback)); + EXPECT_TRUE(called); + } + protected: std::string hostname_{"www.fake.com"}; quic::QuicSocketAddress server_address_; quic::QuicSocketAddress client_address_; quic::QuicTransportVersion version_{quic::QUIC_VERSION_UNSUPPORTED}; - quiche::QuicheStringPiece chlo_hash_{""}; + quiche::QuicheStringPiece chlo_hash_{"aaaaa"}; std::string server_config_{"Server Config"}; std::string expected_certs_{quic::test::kTestCertificateChainPem}; std::string pkey_{quic::test::kTestCertificatePrivateKeyPem}; @@ -71,27 +186,66 @@ class EnvoyQuicProofSourceTest : public ::testing::Test { Network::MockFilterChainManager filter_chain_manager_; Network::MockListenSocket listen_socket_; testing::NiceMock listener_config_; + QuicServerTransportSocketFactory transport_socket_factory_; + Ssl::MockTlsCertificateConfig tls_cert_config_; Server::ListenerStats listener_stats_; EnvoyQuicProofSource proof_source_; - EnvoyQuicFakeProofVerifier proof_verifier_; }; TEST_F(EnvoyQuicProofSourceTest, TestGetProof) { + expectCertChainAndPrivateKey(expected_certs_, true); + testGetProof(true); +} + +TEST_F(EnvoyQuicProofSourceTest, GetProofFailNoFilterChain) { bool called = false; - auto callback = std::make_unique( - called, "Fake timestamp", quic::test::kTestCertificate, filter_chain_); - EXPECT_CALL(listen_socket_, ioHandle()).Times(2); + auto callback = std::make_unique(called, false, server_config_, version_, + chlo_hash_, filter_chain_); + EXPECT_CALL(listen_socket_, ioHandle()); + EXPECT_CALL(filter_chain_manager_, findFilterChain(_)) + .WillRepeatedly(Invoke([&](const Network::ConnectionSocket&) { return nullptr; })); + proof_source_.GetProof(server_address_, client_address_, hostname_, server_config_, version_, + chlo_hash_, std::move(callback)); + EXPECT_TRUE(called); +} + +TEST_F(EnvoyQuicProofSourceTest, GetProofFailInvalidCert) { + std::string invalid_cert{R"(-----BEGIN CERTIFICATE----- + invalid certificate + -----END CERTIFICATE-----)"}; + expectCertChainAndPrivateKey(invalid_cert, false); + testGetProof(false); +} + +TEST_F(EnvoyQuicProofSourceTest, GetProofFailInvalidPublicKeyInCert) { + // This is a valid cert with RSA public key. But we don't support RSA key with + // length < 1024. + std::string cert_with_rsa_1024{R"(-----BEGIN CERTIFICATE----- +MIIC2jCCAkOgAwIBAgIUDBHEwlCvLGh3w0O8VwIW+CjYXY8wDQYJKoZIhvcNAQEL +BQAwfzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMRIwEAYDVQQHDAlDYW1icmlk +Z2UxDzANBgNVBAoMBkdvb2dsZTEOMAwGA1UECwwFZW52b3kxDTALBgNVBAMMBHRl +c3QxHzAdBgkqhkiG9w0BCQEWEGRhbnpoQGdvb2dsZS5jb20wHhcNMjAwODA0MTg1 +OTQ4WhcNMjEwODA0MTg1OTQ4WjB/MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUEx +EjAQBgNVBAcMCUNhbWJyaWRnZTEPMA0GA1UECgwGR29vZ2xlMQ4wDAYDVQQLDAVl +bnZveTENMAsGA1UEAwwEdGVzdDEfMB0GCSqGSIb3DQEJARYQZGFuemhAZ29vZ2xl +LmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAykCZNjxws+sNfnp18nsp ++7LN81J/RSwAHLkGnwEtd3OxSUuiCYHgYlyuEAwJdf99+SaFrgcA4LvYJ/Mhm/fZ +msnpfsAvoQ49+ax0fm1x56ii4KgNiu9iFsWwwVmkHkgjlRcRsmhr4WeIf14Yvpqs +JNsbNVSCZ4GLQ2V6BqIHlhcCAwEAAaNTMFEwHQYDVR0OBBYEFDO1KPYcdRmeKDvL +H2Yzj8el2Xe1MB8GA1UdIwQYMBaAFDO1KPYcdRmeKDvLH2Yzj8el2Xe1MA8GA1Ud +EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADgYEAnwWVmwSK9TDml7oHGBavzOC1 +f/lOd5zz2e7Tu2pUtx1sX1tlKph1D0ANpJwxRV78R2hjmynLSl7h4Ual9NMubqkD +x96rVeUbRJ/qU4//nNM/XQa9vIAIcTZ0jFhmb0c3R4rmoqqC3vkSDwtaE5yuS5T4 +GUy+n0vQNB0cXGzgcGI= +-----END CERTIFICATE-----)"}; + expectCertChainAndPrivateKey(cert_with_rsa_1024, false); + testGetProof(false); +} + +TEST_F(EnvoyQuicProofSourceTest, InvalidPrivateKey) { + EXPECT_CALL(listen_socket_, ioHandle()); EXPECT_CALL(filter_chain_manager_, findFilterChain(_)) - .WillRepeatedly(Invoke([&](const Network::ConnectionSocket& connection_socket) { - EXPECT_EQ(*quicAddressToEnvoyAddressInstance(server_address_), - *connection_socket.localAddress()); - EXPECT_EQ(*quicAddressToEnvoyAddressInstance(client_address_), - *connection_socket.remoteAddress()); - EXPECT_EQ(Extensions::TransportSockets::TransportProtocolNames::get().Quic, - connection_socket.detectedTransportProtocol()); - EXPECT_EQ("h2", connection_socket.requestedApplicationProtocols()[0]); - return &filter_chain_; - })); + .WillOnce(Invoke([&](const Network::ConnectionSocket&) { return &filter_chain_; })); auto server_context_config = std::make_unique(); auto server_context_config_ptr = server_context_config.get(); QuicServerTransportSocketFactory transport_socket_factory(std::move(server_context_config)); @@ -103,20 +257,11 @@ TEST_F(EnvoyQuicProofSourceTest, TestGetProof) { std::reference_wrapper(tls_cert_config)}; EXPECT_CALL(*server_context_config_ptr, tlsCertificates()) .WillRepeatedly(Return(tls_cert_configs)); - EXPECT_CALL(tls_cert_config, certificateChain()).WillOnce(ReturnRef(expected_certs_)); - EXPECT_CALL(tls_cert_config, privateKey()).WillOnce(ReturnRef(pkey_)); - proof_source_.GetProof(server_address_, client_address_, hostname_, server_config_, version_, - chlo_hash_, std::move(callback)); - EXPECT_TRUE(called); - - EXPECT_EQ(quic::QUIC_SUCCESS, - proof_verifier_.VerifyProof(hostname_, /*port=*/0, server_config_, version_, chlo_hash_, - {"Fake cert"}, "", "fake signature", nullptr, nullptr, - nullptr, nullptr)); - EXPECT_EQ(quic::QUIC_FAILURE, - proof_verifier_.VerifyProof(hostname_, /*port=*/0, server_config_, version_, chlo_hash_, - {"Fake cert", "Unexpected cert"}, "Fake timestamp", - "fake signature", nullptr, nullptr, nullptr, nullptr)); + std::string invalid_pkey("abcdefg"); + EXPECT_CALL(tls_cert_config, privateKey()).WillOnce(ReturnRef(invalid_pkey)); + proof_source_.ComputeTlsSignature(server_address_, client_address_, hostname_, + SSL_SIGN_RSA_PSS_RSAE_SHA256, "payload", + std::make_unique(false)); } } // namespace Quic diff --git a/test/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_test.cc b/test/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_test.cc new file mode 100644 index 000000000000..4a1dfe144dd3 --- /dev/null +++ b/test/extensions/quic_listeners/quiche/envoy_quic_proof_verifier_test.cc @@ -0,0 +1,252 @@ +#include +#include + +#include "extensions/quic_listeners/quiche/envoy_quic_proof_verifier.h" +#include "extensions/transport_sockets/tls/context_config_impl.h" + +#include "test/mocks/ssl/mocks.h" +#include "test/mocks/stats/mocks.h" +#include "test/test_common/test_time.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "quiche/quic/core/crypto/certificate_view.h" +#include "quiche/quic/test_tools/test_certificates.h" + +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Quic { + +class EnvoyQuicProofVerifierTest : public testing::Test { +public: + EnvoyQuicProofVerifierTest() + : root_ca_cert_(cert_chain_.substr(cert_chain_.rfind("-----BEGIN CERTIFICATE-----"))), + leaf_cert_([=]() { + std::stringstream pem_stream(cert_chain_); + std::vector chain = quic::CertificateView::LoadPemFromStream(&pem_stream); + return chain[0]; + }()) { + ON_CALL(client_context_config_, cipherSuites) + .WillByDefault(ReturnRef( + Extensions::TransportSockets::Tls::ClientContextConfigImpl::DEFAULT_CIPHER_SUITES)); + ON_CALL(client_context_config_, ecdhCurves) + .WillByDefault( + ReturnRef(Extensions::TransportSockets::Tls::ClientContextConfigImpl::DEFAULT_CURVES)); + ON_CALL(client_context_config_, alpnProtocols()).WillByDefault(ReturnRef(alpn_)); + ON_CALL(client_context_config_, serverNameIndication()).WillByDefault(ReturnRef(empty_string_)); + ON_CALL(client_context_config_, signingAlgorithmsForTest()).WillByDefault(ReturnRef(sig_algs_)); + ON_CALL(client_context_config_, certificateValidationContext()) + .WillByDefault(Return(&cert_validation_ctx_config_)); + } + + // Since this cert chain contains an expired cert, we can flip allow_expired_cert to test the code + // paths for BoringSSL cert verification success and failure. + void configCertVerificationDetails(bool allow_expired_cert) { + // Getting the last cert in the chain as the root CA cert. + EXPECT_CALL(cert_validation_ctx_config_, caCert()).WillRepeatedly(ReturnRef(root_ca_cert_)); + EXPECT_CALL(cert_validation_ctx_config_, caCertPath()).WillRepeatedly(ReturnRef(path_string_)); + EXPECT_CALL(cert_validation_ctx_config_, trustChainVerification) + .WillRepeatedly(Return(envoy::extensions::transport_sockets::tls::v3:: + CertificateValidationContext::VERIFY_TRUST_CHAIN)); + EXPECT_CALL(cert_validation_ctx_config_, allowExpiredCertificate()) + .WillRepeatedly(Return(allow_expired_cert)); + EXPECT_CALL(cert_validation_ctx_config_, certificateRevocationList()) + .WillRepeatedly(ReturnRef(empty_string_)); + EXPECT_CALL(cert_validation_ctx_config_, certificateRevocationListPath()) + .WillRepeatedly(ReturnRef(path_string_)); + EXPECT_CALL(cert_validation_ctx_config_, verifySubjectAltNameList()) + .WillRepeatedly(ReturnRef(empty_string_list_)); + EXPECT_CALL(cert_validation_ctx_config_, subjectAltNameMatchers()) + .WillRepeatedly(ReturnRef(san_matchers_)); + EXPECT_CALL(cert_validation_ctx_config_, verifyCertificateHashList()) + .WillRepeatedly(ReturnRef(empty_string_list_)); + EXPECT_CALL(cert_validation_ctx_config_, verifyCertificateSpkiList()) + .WillRepeatedly(ReturnRef(empty_string_list_)); + verifier_ = + std::make_unique(store_, client_context_config_, time_system_); + } + +protected: + const std::string path_string_{"some_path"}; + const std::string alpn_{"h2,http/1.1"}; + const std::string sig_algs_{"rsa_pss_rsae_sha256"}; + const std::vector san_matchers_; + const std::string empty_string_; + const std::vector empty_string_list_; + const std::string cert_chain_{quic::test::kTestCertificateChainPem}; + const std::string root_ca_cert_; + const std::string leaf_cert_; + NiceMock store_; + Event::GlobalTimeSystem time_system_; + NiceMock client_context_config_; + Ssl::MockCertificateValidationContextConfig cert_validation_ctx_config_; + std::unique_ptr verifier_; +}; + +TEST_F(EnvoyQuicProofVerifierTest, VerifyCertChainSuccess) { + configCertVerificationDetails(true); + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(leaf_cert_); + const std::string ocsp_response; + const std::string cert_sct; + std::string error_details; + EXPECT_EQ(quic::QUIC_SUCCESS, + verifier_->VerifyCertChain(std::string(cert_view->subject_alt_name_domains()[0]), 54321, + {leaf_cert_}, ocsp_response, cert_sct, nullptr, + &error_details, nullptr, nullptr)) + << error_details; +} + +TEST_F(EnvoyQuicProofVerifierTest, VerifyCertChainFailureFromSsl) { + configCertVerificationDetails(false); + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(leaf_cert_); + const std::string ocsp_response; + const std::string cert_sct; + std::string error_details; + EXPECT_EQ(quic::QUIC_FAILURE, + verifier_->VerifyCertChain(std::string(cert_view->subject_alt_name_domains()[0]), 54321, + {leaf_cert_}, ocsp_response, cert_sct, nullptr, + &error_details, nullptr, nullptr)) + << error_details; + EXPECT_EQ("X509_verify_cert: certificate verification error at depth 1: certificate has expired", + error_details); +} + +TEST_F(EnvoyQuicProofVerifierTest, VerifyCertChainFailureInvalidLeafCert) { + configCertVerificationDetails(true); + const std::string ocsp_response; + const std::string cert_sct; + std::string error_details; + const std::vector certs{"invalid leaf cert"}; + EXPECT_EQ(quic::QUIC_FAILURE, + verifier_->VerifyCertChain("www.google.com", 54321, certs, ocsp_response, cert_sct, + nullptr, &error_details, nullptr, nullptr)); + EXPECT_EQ("d2i_X509: fail to parse DER", error_details); +} + +TEST_F(EnvoyQuicProofVerifierTest, VerifyCertChainFailureLeafCertWithGarbage) { + configCertVerificationDetails(true); + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(leaf_cert_); + const std::string ocsp_response; + const std::string cert_sct; + std::string cert_with_trailing_garbage = absl::StrCat(leaf_cert_, "AAAAAA"); + std::string error_details; + EXPECT_EQ(quic::QUIC_FAILURE, + verifier_->VerifyCertChain(std::string(cert_view->subject_alt_name_domains()[0]), 54321, + {cert_with_trailing_garbage}, ocsp_response, cert_sct, + nullptr, &error_details, nullptr, nullptr)) + << error_details; + EXPECT_EQ("There is trailing garbage in DER.", error_details); +} + +TEST_F(EnvoyQuicProofVerifierTest, VerifyCertChainFailureInvalidHost) { + configCertVerificationDetails(true); + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(leaf_cert_); + const std::string ocsp_response; + const std::string cert_sct; + std::string error_details; + EXPECT_EQ(quic::QUIC_FAILURE, + verifier_->VerifyCertChain("unknown.org", 54321, {leaf_cert_}, ocsp_response, cert_sct, + nullptr, &error_details, nullptr, nullptr)) + << error_details; + EXPECT_EQ("Leaf certificate doesn't match hostname: unknown.org", error_details); +} + +TEST_F(EnvoyQuicProofVerifierTest, VerifyProofFailureEmptyCertChain) { + configCertVerificationDetails(true); + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(leaf_cert_); + quic::QuicTransportVersion version{quic::QUIC_VERSION_UNSUPPORTED}; + quiche::QuicheStringPiece chlo_hash{"aaaaa"}; + std::string server_config{"Server Config"}; + const std::string ocsp_response; + const std::string cert_sct; + std::string error_details; + const std::vector certs; + EXPECT_EQ(quic::QUIC_FAILURE, + verifier_->VerifyProof(std::string(cert_view->subject_alt_name_domains()[0]), 54321, + server_config, version, chlo_hash, certs, cert_sct, "signature", + nullptr, &error_details, nullptr, nullptr)); + EXPECT_EQ("Received empty cert chain.", error_details); +} + +TEST_F(EnvoyQuicProofVerifierTest, VerifyProofFailureInvalidLeafCert) { + configCertVerificationDetails(true); + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(leaf_cert_); + quic::QuicTransportVersion version{quic::QUIC_VERSION_UNSUPPORTED}; + quiche::QuicheStringPiece chlo_hash{"aaaaa"}; + std::string server_config{"Server Config"}; + const std::string ocsp_response; + const std::string cert_sct; + std::string error_details; + const std::vector certs{"invalid leaf cert"}; + EXPECT_EQ(quic::QUIC_FAILURE, + verifier_->VerifyProof(std::string(cert_view->subject_alt_name_domains()[0]), 54321, + server_config, version, chlo_hash, certs, cert_sct, "signature", + nullptr, &error_details, nullptr, nullptr)); + EXPECT_EQ("Invalid leaf cert.", error_details); +} + +TEST_F(EnvoyQuicProofVerifierTest, VerifyProofFailureUnsupportedECKey) { + configCertVerificationDetails(true); + quic::QuicTransportVersion version{quic::QUIC_VERSION_UNSUPPORTED}; + quiche::QuicheStringPiece chlo_hash{"aaaaa"}; + std::string server_config{"Server Config"}; + const std::string ocsp_response; + const std::string cert_sct; + std::string error_details; + // This is a EC cert with secp384r1 curve which is not supported by Envoy. + const std::string certs{R"(-----BEGIN CERTIFICATE----- +MIICkDCCAhagAwIBAgIUTZbykU9eQL3GdrNlodxrOJDecIQwCgYIKoZIzj0EAwIw +fzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1BMRIwEAYDVQQHDAlDYW1icmlkZ2Ux +DzANBgNVBAoMBkdvb2dsZTEOMAwGA1UECwwFZW52b3kxDTALBgNVBAMMBHRlc3Qx +HzAdBgkqhkiG9w0BCQEWEGRhbnpoQGdvb2dsZS5jb20wHhcNMjAwODA1MjAyMDI0 +WhcNMjIwODA1MjAyMDI0WjB/MQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExEjAQ +BgNVBAcMCUNhbWJyaWRnZTEPMA0GA1UECgwGR29vZ2xlMQ4wDAYDVQQLDAVlbnZv +eTENMAsGA1UEAwwEdGVzdDEfMB0GCSqGSIb3DQEJARYQZGFuemhAZ29vZ2xlLmNv +bTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGRaEAtVq+xHXfsF4R/j+mqVN2E29ZYL +oFlvnelKeeT2B51bSfUv+X+Ci1BSa2OxPCVS6o0vpcF6YOlz4CS7QcXZIoRfhsv7 +O2Hz/IdxAPhX/gdK/70T1x+V/6nvIHiiw6NTMFEwHQYDVR0OBBYEFF75rDce6xNJ +GfpKbUg4emG2KWRMMB8GA1UdIwQYMBaAFF75rDce6xNJGfpKbUg4emG2KWRMMA8G +A1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDaAAwZQIxAIyZghTK3cmyrRWkxfQ7 +xEc11gujcT8nbytYbM6jodKwcbtR6SOmLx2ychXrCMm2ZAIwXqmrTYBtrbqb3mBx +VdGXMAjeXhnOnPvmDi5hUz/uvI+Pg6cNmUoCRwSCnK/DazhA +-----END CERTIFICATE-----)"}; + std::stringstream pem_stream(certs); + std::vector chain = quic::CertificateView::LoadPemFromStream(&pem_stream); + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(chain[0]); + ASSERT(cert_view); + EXPECT_EQ(quic::QUIC_FAILURE, + verifier_->VerifyProof("www.google.com", 54321, server_config, version, chlo_hash, + chain, cert_sct, "signature", nullptr, &error_details, nullptr, + nullptr)); + EXPECT_EQ("Invalid leaf cert, only P-256 ECDSA certificates are supported", error_details); +} + +TEST_F(EnvoyQuicProofVerifierTest, VerifyProofFailureInvalidSignature) { + configCertVerificationDetails(true); + std::unique_ptr cert_view = + quic::CertificateView::ParseSingleCertificate(leaf_cert_); + quic::QuicTransportVersion version{quic::QUIC_VERSION_UNSUPPORTED}; + quiche::QuicheStringPiece chlo_hash{"aaaaa"}; + std::string server_config{"Server Config"}; + const std::string ocsp_response; + const std::string cert_sct; + std::string error_details; + EXPECT_EQ(quic::QUIC_FAILURE, + verifier_->VerifyProof(std::string(cert_view->subject_alt_name_domains()[0]), 54321, + server_config, version, chlo_hash, {leaf_cert_}, cert_sct, + "signature", nullptr, &error_details, nullptr, nullptr)); + EXPECT_EQ("Signature is not valid.", error_details); +} + +} // namespace Quic +} // namespace Envoy diff --git a/test/extensions/quic_listeners/quiche/integration/quic_http_integration_test.cc b/test/extensions/quic_listeners/quiche/integration/quic_http_integration_test.cc index 05fb1e61a7aa..85688dbd0835 100644 --- a/test/extensions/quic_listeners/quiche/integration/quic_http_integration_test.cc +++ b/test/extensions/quic_listeners/quiche/integration/quic_http_integration_test.cc @@ -1,3 +1,5 @@ +#include + #include #include "envoy/config/bootstrap/v3/bootstrap.pb.h" @@ -7,6 +9,7 @@ #include "test/config/utility.h" #include "test/integration/http_integration.h" +#include "test/integration/ssl_utility.h" #include "test/test_common/utility.h" #pragma GCC diagnostic push @@ -23,12 +26,14 @@ #include "extensions/quic_listeners/quiche/envoy_quic_client_session.h" #include "extensions/quic_listeners/quiche/envoy_quic_client_connection.h" -#include "extensions/quic_listeners/quiche/envoy_quic_fake_proof_verifier.h" +#include "extensions/quic_listeners/quiche/envoy_quic_proof_verifier.h" #include "extensions/quic_listeners/quiche/envoy_quic_connection_helper.h" #include "extensions/quic_listeners/quiche/envoy_quic_alarm_factory.h" #include "extensions/quic_listeners/quiche/envoy_quic_packet_writer.h" #include "extensions/quic_listeners/quiche/envoy_quic_utils.h" +#include "extensions/quic_listeners/quiche/quic_transport_socket_factory.h" #include "test/extensions/quic_listeners/quiche/test_utils.h" +#include "extensions/transport_sockets/tls/context_config_impl.h" namespace Envoy { namespace Quic { @@ -44,6 +49,43 @@ class CodecClientCallbacksForTest : public Http::CodecClientCallbacks { Http::StreamResetReason last_stream_reset_reason_{Http::StreamResetReason::LocalReset}; }; +std::unique_ptr +createQuicClientTransportSocketFactory(const Ssl::ClientSslTransportOptions& options, Api::Api& api, + const std::string& san_to_match) { + std::string yaml_plain = R"EOF( + common_tls_context: + validation_context: + trusted_ca: + filename: "{{ test_rundir }}/test/config/integration/certs/cacert.pem" +)EOF"; + envoy::extensions::transport_sockets::tls::v3::UpstreamTlsContext tls_context; + TestUtility::loadFromYaml(TestEnvironment::substitute(yaml_plain), tls_context); + auto* common_context = tls_context.mutable_common_tls_context(); + + if (options.alpn_) { + common_context->add_alpn_protocols("h3"); + } + if (options.san_) { + common_context->mutable_validation_context()->add_match_subject_alt_names()->set_exact( + san_to_match); + } + for (const std::string& cipher_suite : options.cipher_suites_) { + common_context->mutable_tls_params()->add_cipher_suites(cipher_suite); + } + if (!options.sni_.empty()) { + tls_context.set_sni(options.sni_); + } + + common_context->mutable_tls_params()->set_tls_minimum_protocol_version(options.tls_version_); + common_context->mutable_tls_params()->set_tls_maximum_protocol_version(options.tls_version_); + + NiceMock mock_factory_ctx; + ON_CALL(mock_factory_ctx, api()).WillByDefault(testing::ReturnRef(api)); + auto cfg = std::make_unique( + tls_context, options.sigalgs_, mock_factory_ctx); + return std::make_unique(std::move(cfg)); +} + class QuicHttpIntegrationTest : public HttpIntegrationTest, public QuicMultiVersionTest { public: QuicHttpIntegrationTest() @@ -59,8 +101,7 @@ class QuicHttpIntegrationTest : public HttpIntegrationTest, public QuicMultiVers SetQuicReloadableFlag(quic_disable_version_draft_25, !use_http3); return quic::CurrentSupportedVersions(); }()), - crypto_config_(std::make_unique()), conn_helper_(*dispatcher_), - alarm_factory_(*dispatcher_, *conn_helper_.GetClock()), + conn_helper_(*dispatcher_), alarm_factory_(*dispatcher_, *conn_helper_.GetClock()), injected_resource_filename_(TestEnvironment::temporaryPath("injected_resource")), file_updater_(injected_resource_filename_) {} @@ -81,7 +122,7 @@ class QuicHttpIntegrationTest : public HttpIntegrationTest, public QuicMultiVers quic::ParsedQuicVersionVector{supported_versions_[0]}, local_addr, *dispatcher_, nullptr); quic_connection_ = connection.get(); auto session = std::make_unique( - quic_config_, supported_versions_, std::move(connection), server_id_, &crypto_config_, + quic_config_, supported_versions_, std::move(connection), server_id_, crypto_config_.get(), &push_promise_index_, *dispatcher_, 0); session->Initialize(); return session; @@ -170,16 +211,24 @@ class QuicHttpIntegrationTest : public HttpIntegrationTest, public QuicMultiVers updateResource(0); HttpIntegrationTest::initialize(); registerTestServerPorts({"http"}); + crypto_config_ = + std::make_unique(std::make_unique( + stats_store_, + createQuicClientTransportSocketFactory( + Ssl::ClientSslTransportOptions().setAlpn(true).setSan(true), *api_, san_to_match_) + ->clientContextConfig(), + timeSystem())); } void updateResource(double pressure) { file_updater_.update(absl::StrCat(pressure)); } protected: quic::QuicConfig quic_config_; - quic::QuicServerId server_id_{"example.com", 443, false}; + quic::QuicServerId server_id_{"lyft.com", 443, false}; + std::string san_to_match_{"spiffe://lyft.com/backend-team"}; quic::QuicClientPushPromiseIndex push_promise_index_; quic::ParsedQuicVersionVector supported_versions_; - quic::QuicCryptoClientConfig crypto_config_; + std::unique_ptr crypto_config_; EnvoyQuicConnectionHelper conn_helper_; EnvoyQuicAlarmFactory alarm_factory_; CodecClientCallbacksForTest client_codec_callback_; @@ -461,5 +510,19 @@ TEST_P(QuicHttpIntegrationTest, AdminDrainDrainsListeners) { testAdminDrain(Http::CodecClient::Type::HTTP1); } +TEST_P(QuicHttpIntegrationTest, CertVerificationFailure) { + san_to_match_ = "www.random_domain.com"; + initialize(); + codec_client_ = makeRawHttpConnection(makeClientConnection((lookupPort("http"))), absl::nullopt); + EXPECT_FALSE(codec_client_->connected()); + std::string failure_reason = + GetParam().second == QuicVersionType::GquicQuicCrypto + ? "QUIC_PROOF_INVALID with details: Proof invalid: X509_verify_cert: certificate " + "verification error at depth 0: ok" + : "QUIC_HANDSHAKE_FAILED with details: TLS handshake failure (ENCRYPTION_HANDSHAKE) 46: " + "certificate unknown"; + EXPECT_EQ(failure_reason, codec_client_->connection()->transportFailureReason()); +} + } // namespace Quic } // namespace Envoy diff --git a/test/extensions/quic_listeners/quiche/test_proof_source.h b/test/extensions/quic_listeners/quiche/test_proof_source.h index ad8bae60a540..8b1baf920d69 100644 --- a/test/extensions/quic_listeners/quiche/test_proof_source.h +++ b/test/extensions/quic_listeners/quiche/test_proof_source.h @@ -15,14 +15,14 @@ #include #include "test/mocks/network/mocks.h" -#include "extensions/quic_listeners/quiche/envoy_quic_fake_proof_source.h" +#include "extensions/quic_listeners/quiche/envoy_quic_proof_source_base.h" namespace Envoy { namespace Quic { // A test ProofSource which always provide a hard-coded test certificate in // QUICHE and a fake signature. -class TestProofSource : public Quic::EnvoyQuicFakeProofSource { +class TestProofSource : public EnvoyQuicProofSourceBase { public: quic::QuicReferenceCountedPointer GetCertChain(const quic::QuicSocketAddress& /*server_address*/, @@ -31,18 +31,18 @@ class TestProofSource : public Quic::EnvoyQuicFakeProofSource { return cert_chain_; } - void - ComputeTlsSignature(const quic::QuicSocketAddress& /*server_address*/, - const quic::QuicSocketAddress& /*client_address*/, - const std::string& /*hostname*/, uint16_t /*signature_algorithm*/, - quiche::QuicheStringPiece in, - std::unique_ptr callback) override { + const Network::MockFilterChain& filterChain() const { return filter_chain_; } + +protected: + void signPayload(const quic::QuicSocketAddress& /*server_address*/, + const quic::QuicSocketAddress& /*client_address*/, + const std::string& /*hostname*/, uint16_t /*signature_algorithm*/, + quiche::QuicheStringPiece in, + std::unique_ptr callback) override { callback->Run(true, absl::StrCat("Fake signature for { ", in, " }"), std::make_unique(filter_chain_)); } - const Network::MockFilterChain& filterChain() const { return filter_chain_; } - private: quic::QuicReferenceCountedPointer cert_chain_{ new quic::ProofSource::Chain( diff --git a/test/extensions/quic_listeners/quiche/test_proof_verifier.h b/test/extensions/quic_listeners/quiche/test_proof_verifier.h new file mode 100644 index 000000000000..77dada22d1cd --- /dev/null +++ b/test/extensions/quic_listeners/quiche/test_proof_verifier.h @@ -0,0 +1,30 @@ +#include "extensions/quic_listeners/quiche/envoy_quic_proof_verifier_base.h" + +namespace Envoy { +namespace Quic { + +// A test quic::ProofVerifier which always approves the certs and signature. +class TestProofVerifier : public EnvoyQuicProofVerifierBase { +public: + // quic::ProofVerifier + quic::QuicAsyncStatus + VerifyCertChain(const std::string& /*hostname*/, const uint16_t /*port*/, + const std::vector& /*certs*/, const std::string& /*ocsp_response*/, + const std::string& /*cert_sct*/, const quic::ProofVerifyContext* /*context*/, + std::string* /*error_details*/, + std::unique_ptr* /*details*/, + std::unique_ptr /*callback*/) override { + return quic::QUIC_SUCCESS; + } + +protected: + // EnvoyQuicProofVerifierBase + bool verifySignature(const std::string& /*server_config*/, absl::string_view /*chlo_hash*/, + const std::string& /*cert*/, const std::string& /*signature*/, + std::string* /*error_details*/) override { + return true; + } +}; + +} // namespace Quic +} // namespace Envoy diff --git a/test/mocks/ssl/mocks.h b/test/mocks/ssl/mocks.h index c3bc9b2f8ecd..7567e5807cff 100644 --- a/test/mocks/ssl/mocks.h +++ b/test/mocks/ssl/mocks.h @@ -129,6 +129,23 @@ class MockTlsCertificateConfig : public TlsCertificateConfig { MOCK_METHOD(Envoy::Ssl::PrivateKeyMethodProviderSharedPtr, privateKeyMethod, (), (const)); }; +class MockCertificateValidationContextConfig : public CertificateValidationContextConfig { +public: + MOCK_METHOD(const std::string&, caCert, (), (const)); + MOCK_METHOD(const std::string&, caCertPath, (), (const)); + MOCK_METHOD(const std::string&, certificateRevocationList, (), (const)); + MOCK_METHOD(const std::string&, certificateRevocationListPath, (), (const)); + MOCK_METHOD(const std::vector&, verifySubjectAltNameList, (), (const)); + MOCK_METHOD(const std::vector&, subjectAltNameMatchers, + (), (const)); + MOCK_METHOD(const std::vector&, verifyCertificateHashList, (), (const)); + MOCK_METHOD(const std::vector&, verifyCertificateSpkiList, (), (const)); + MOCK_METHOD(bool, allowExpiredCertificate, (), (const)); + MOCK_METHOD(envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext:: + TrustChainVerification, + trustChainVerification, (), (const)); +}; + class MockPrivateKeyMethodManager : public PrivateKeyMethodManager { public: MockPrivateKeyMethodManager(); diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index d855084a1fd7..cf99f6b3f17a 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -990,6 +990,7 @@ sched schedulable schemas scopekey +secp sendmsg sendmmsg sendto