From 735fe34c33922e8df7648d698d9441b3ec480cc5 Mon Sep 17 00:00:00 2001 From: Keith Mattix II Date: Thu, 15 Aug 2024 14:31:30 -0500 Subject: [PATCH] Add new stream formatters for cert chain identifiers (#35513) Adds stream formatters to print the fingerprints and serial for entire downstream cert chain Fixes #35452 Signed-off-by: Keith Mattix II Signed-off-by: duxin40 --- changelogs/current.yaml | 4 + .../observability/access_log/usage.rst | 18 +++ envoy/ssl/connection.h | 23 ++++ .../common/formatter/stream_info_formatter.cc | 24 ++++ .../common/tls/connection_info_impl_base.cc | 69 +++++++++++ source/common/tls/connection_info_impl_base.h | 6 + source/common/tls/utility.cc | 25 ++++ source/common/tls/utility.h | 9 ++ .../formatter/substitution_formatter_test.cc | 103 ++++++++++++++++ test/common/tls/ssl_socket_test.cc | 112 ++++++++++++++++++ test/common/tls/test_data/no_san_cert_info.h | 7 ++ test/common/tls/utility_test.cc | 21 ++++ test/mocks/ssl/mocks.h | 3 + 13 files changed, 424 insertions(+) diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 4d58495ab5807..9c75222d83e3e 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -181,6 +181,10 @@ new_features: ` to allow overriding TLS certificate selection behavior. An extension can select certificate base on the incoming SNI, in both sync and async mode. +- area: access log + change: | + Added support for :ref:`%DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_1% `, + ``%DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_256``, and ``%DOWNSTREAM_PEER_CHAIN_SERIALS%``, as access log formatters. - area: matching change: | Added dynamic metadata matcher support :ref:`Dynamic metadata input ` diff --git a/docs/root/configuration/observability/access_log/usage.rst b/docs/root/configuration/observability/access_log/usage.rst index 657c485a9b7d6..d0fc7098e53e6 100644 --- a/docs/root/configuration/observability/access_log/usage.rst +++ b/docs/root/configuration/observability/access_log/usage.rst @@ -1111,6 +1111,24 @@ UDP UDP Not implemented ("-"). +%DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_256% + HTTP/TCP/THRIFT + The comma-separated hex-encoded SHA256 fingerprints of all client certificates used to establish the downstream TLS connection. + UDP + Not implemented ("-"). + +%DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_1% + HTTP/TCP/THRIFT + The comma-separated hex-encoded SHA1 fingerprints of all client certificates used to establish the downstream TLS connection. + UDP + Not implemented ("-"). + +%DOWNSTREAM_PEER_CHAIN_SERIALS% + HTTP/TCP/THRIFT + The comma-separated wserial numbers of all client certificates used to establish the downstream TLS connection. + UDP + Not implemented ("-"). + %DOWNSTREAM_PEER_CERT% HTTP/TCP/THRIFT The client certificate in the URL-encoded PEM format used to establish the downstream TLS connection. diff --git a/envoy/ssl/connection.h b/envoy/ssl/connection.h index 9cc4a77a25ac6..cc4efd8ed242d 100644 --- a/envoy/ssl/connection.h +++ b/envoy/ssl/connection.h @@ -60,6 +60,29 @@ class ConnectionInfo { **/ virtual const std::string& serialNumberPeerCertificate() const PURE; + /** + * @return absl::Span the SHA256 digests of all peer certificates. + * Returns an empty vector if there is no peer certificate which can happen in + * TLS (non mTLS) connections. + */ + virtual absl::Span sha256PeerCertificateChainDigests() const PURE; + + /** + * @return absl::Span the SHA1 digest of all peer certificates. + * Returns an empty vector if there is no peer certificate which can happen in + * TLS (non mTLS) connections. + */ + virtual absl::Span sha1PeerCertificateChainDigests() const PURE; + + /** + * @return absl::Span the serial numbers of all peer certificates. + * An empty vector indicates that there were no peer certificates which can happen + * in TLS (non mTLS) connections. + * A vector element with a "" value indicates that the certificate at that index in + * the cert chain did not have a serial number. + **/ + virtual absl::Span serialNumbersPeerCertificates() const PURE; + /** * @return std::string the issuer field of the peer certificate in RFC 2253 format. Returns "" if * there is no peer certificate, or no issuer. diff --git a/source/common/formatter/stream_info_formatter.cc b/source/common/formatter/stream_info_formatter.cc index f3c49418dafdc..50681ffcda061 100644 --- a/source/common/formatter/stream_info_formatter.cc +++ b/source/common/formatter/stream_info_formatter.cc @@ -1532,6 +1532,30 @@ const StreamInfoFormatterProviderLookupTable& getKnownStreamInfoFormatterProvide return connection_info.serialNumberPeerCertificate(); }); }}}, + {"DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_256", + {CommandSyntaxChecker::COMMAND_ONLY, + [](const absl::string_view, absl::optional) { + return std::make_unique( + [](const Ssl::ConnectionInfo& connection_info) { + return absl::StrJoin(connection_info.sha256PeerCertificateChainDigests(), ","); + }); + }}}, + {"DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_1", + {CommandSyntaxChecker::COMMAND_ONLY, + [](const absl::string_view, absl::optional) { + return std::make_unique( + [](const Ssl::ConnectionInfo& connection_info) { + return absl::StrJoin(connection_info.sha1PeerCertificateChainDigests(), ","); + }); + }}}, + {"DOWNSTREAM_PEER_CHAIN_SERIALS", + {CommandSyntaxChecker::COMMAND_ONLY, + [](const absl::string_view, absl::optional) { + return std::make_unique( + [](const Ssl::ConnectionInfo& connection_info) { + return absl::StrJoin(connection_info.serialNumbersPeerCertificates(), ","); + }); + }}}, {"DOWNSTREAM_PEER_ISSUER", {CommandSyntaxChecker::COMMAND_ONLY, [](absl::string_view, absl::optional) { diff --git a/source/common/tls/connection_info_impl_base.cc b/source/common/tls/connection_info_impl_base.cc index 5ae15002163da..af03e2cc68530 100644 --- a/source/common/tls/connection_info_impl_base.cc +++ b/source/common/tls/connection_info_impl_base.cc @@ -1,10 +1,14 @@ #include "source/common/tls/connection_info_impl_base.h" +#include + #include "source/common/common/hex.h" #include "absl/strings/str_replace.h" #include "openssl/err.h" +#include "openssl/safestack.h" #include "openssl/x509v3.h" +#include "utility.h" namespace Envoy { namespace Extensions { @@ -77,6 +81,29 @@ const std::string& ConnectionInfoImplBase::sha256PeerCertificateDigest() const { return cached_sha_256_peer_certificate_digest_; } +absl::Span ConnectionInfoImplBase::sha256PeerCertificateChainDigests() const { + if (!cached_sha_256_peer_certificate_digests_.empty()) { + return cached_sha_256_peer_certificate_digests_; + } + + STACK_OF(X509)* cert_chain = SSL_get_peer_full_cert_chain(ssl()); + if (cert_chain == nullptr) { + ASSERT(cached_sha_256_peer_certificate_digests_.empty()); + return cached_sha_256_peer_certificate_digests_; + } + + cached_sha_256_peer_certificate_digests_ = + Utility::mapX509Stack(*cert_chain, [](X509& cert) -> std::string { + std::vector computed_hash(SHA256_DIGEST_LENGTH); + unsigned int n; + X509_digest(&cert, EVP_sha256(), computed_hash.data(), &n); + RELEASE_ASSERT(n == computed_hash.size(), ""); + return Hex::encode(computed_hash); + }); + + return cached_sha_256_peer_certificate_digests_; +} + const std::string& ConnectionInfoImplBase::sha1PeerCertificateDigest() const { if (!cached_sha_1_peer_certificate_digest_.empty()) { return cached_sha_1_peer_certificate_digest_; @@ -95,6 +122,29 @@ const std::string& ConnectionInfoImplBase::sha1PeerCertificateDigest() const { return cached_sha_1_peer_certificate_digest_; } +absl::Span ConnectionInfoImplBase::sha1PeerCertificateChainDigests() const { + if (!cached_sha_1_peer_certificate_digests_.empty()) { + return cached_sha_1_peer_certificate_digests_; + } + + STACK_OF(X509)* cert_chain = SSL_get_peer_full_cert_chain(ssl()); + if (cert_chain == nullptr) { + ASSERT(cached_sha_1_peer_certificate_digests_.empty()); + return cached_sha_1_peer_certificate_digests_; + } + + cached_sha_1_peer_certificate_digests_ = + Utility::mapX509Stack(*cert_chain, [](X509& cert) -> std::string { + std::vector computed_hash(SHA_DIGEST_LENGTH); + unsigned int n; + X509_digest(&cert, EVP_sha1(), computed_hash.data(), &n); + RELEASE_ASSERT(n == computed_hash.size(), ""); + return Hex::encode(computed_hash); + }); + + return cached_sha_1_peer_certificate_digests_; +} + const std::string& ConnectionInfoImplBase::urlEncodedPemEncodedPeerCertificate() const { if (!cached_url_encoded_pem_encoded_peer_certificate_.empty()) { return cached_url_encoded_pem_encoded_peer_certificate_; @@ -253,6 +303,25 @@ const std::string& ConnectionInfoImplBase::serialNumberPeerCertificate() const { return cached_serial_number_peer_certificate_; } +absl::Span ConnectionInfoImplBase::serialNumbersPeerCertificates() const { + if (!cached_serial_numbers_peer_certificates_.empty()) { + return cached_serial_numbers_peer_certificates_; + } + + STACK_OF(X509)* cert_chain = SSL_get_peer_full_cert_chain(ssl()); + if (cert_chain == nullptr) { + ASSERT(cached_serial_numbers_peer_certificates_.empty()); + return cached_serial_numbers_peer_certificates_; + } + + cached_serial_numbers_peer_certificates_ = + Utility::mapX509Stack(*cert_chain, [](X509& cert) -> std::string { + return Utility::getSerialNumberFromCertificate(cert); + }); + + return cached_serial_numbers_peer_certificates_; +} + const std::string& ConnectionInfoImplBase::issuerPeerCertificate() const { if (!cached_issuer_peer_certificate_.empty()) { return cached_issuer_peer_certificate_; diff --git a/source/common/tls/connection_info_impl_base.h b/source/common/tls/connection_info_impl_base.h index 8bcacdb80f7fb..475fa21b6d0c2 100644 --- a/source/common/tls/connection_info_impl_base.h +++ b/source/common/tls/connection_info_impl_base.h @@ -22,8 +22,11 @@ class ConnectionInfoImplBase : public Ssl::ConnectionInfo { bool peerCertificatePresented() const override; absl::Span uriSanLocalCertificate() const override; const std::string& sha256PeerCertificateDigest() const override; + absl::Span sha256PeerCertificateChainDigests() const override; const std::string& sha1PeerCertificateDigest() const override; + absl::Span sha1PeerCertificateChainDigests() const override; const std::string& serialNumberPeerCertificate() const override; + absl::Span serialNumbersPeerCertificates() const override; const std::string& issuerPeerCertificate() const override; const std::string& subjectPeerCertificate() const override; const std::string& subjectLocalCertificate() const override; @@ -48,8 +51,11 @@ class ConnectionInfoImplBase : public Ssl::ConnectionInfo { protected: mutable std::vector cached_uri_san_local_certificate_; mutable std::string cached_sha_256_peer_certificate_digest_; + mutable std::vector cached_sha_256_peer_certificate_digests_; mutable std::string cached_sha_1_peer_certificate_digest_; + mutable std::vector cached_sha_1_peer_certificate_digests_; mutable std::string cached_serial_number_peer_certificate_; + mutable std::vector cached_serial_numbers_peer_certificates_; mutable std::string cached_issuer_peer_certificate_; mutable std::string cached_subject_peer_certificate_; mutable std::string cached_subject_local_certificate_; diff --git a/source/common/tls/utility.cc b/source/common/tls/utility.cc index 6df4276a4e996..619f5da999e62 100644 --- a/source/common/tls/utility.cc +++ b/source/common/tls/utility.cc @@ -1,6 +1,7 @@ #include "source/common/tls/utility.h" #include +#include #include "source/common/common/assert.h" #include "source/common/common/empty_string.h" @@ -463,6 +464,30 @@ std::string Utility::getX509VerificationErrorInfo(X509_STORE_CTX* ctx) { return error_details; } +std::vector Utility::mapX509Stack(stack_st_X509& stack, + std::function field_extractor) { + std::vector result; + if (sk_X509_num(&stack) <= 0) { + IS_ENVOY_BUG("x509 stack is empty or NULL"); + return result; + } + if (field_extractor == nullptr) { + IS_ENVOY_BUG("field_extractor is nullptr"); + return result; + } + + for (uint64_t i = 0; i < sk_X509_num(&stack); i++) { + X509* cert = sk_X509_value(&stack, i); + if (!cert) { + result.push_back(""); // Add an empty string so it's clear something was omitted. + } else { + result.push_back(field_extractor(*cert)); + } + } + + return result; +} + } // namespace Tls } // namespace TransportSockets } // namespace Extensions diff --git a/source/common/tls/utility.h b/source/common/tls/utility.h index da9be3441174b..4123e860bd340 100644 --- a/source/common/tls/utility.h +++ b/source/common/tls/utility.h @@ -48,6 +48,15 @@ bool labelWildcardMatch(absl::string_view dns_label, absl::string_view pattern); */ std::string getSerialNumberFromCertificate(X509& cert); +/** + * Maps a stack of x509 certificates to a vector of strings extracted from the certificates. + * @param stack the stack of certificates + * @param field_extractor the function to extract the field from each certificate. + * @return std::vector returns the list of fields extracted from the certificates. + */ +std::vector mapX509Stack(stack_st_X509& stack, + std::function field_extractor); + /** * Retrieves the subject alternate names of a certificate. * @param cert the certificate diff --git a/test/common/formatter/substitution_formatter_test.cc b/test/common/formatter/substitution_formatter_test.cc index d48d36c3d40e5..a87f2f447e53d 100644 --- a/test/common/formatter/substitution_formatter_test.cc +++ b/test/common/formatter/substitution_formatter_test.cc @@ -1699,6 +1699,109 @@ TEST(SubstitutionFormatterTest, streamInfoFormatterWithSsl) { EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), ProtoEq(ValueUtil::nullValue())); } + { + NiceMock stream_info; + StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_256"); + auto connection_info = std::make_shared(); + std::vector expected_shas{ + "685a2db593d5f86d346cb1a297009c3b467ad77f1944aa799039a2fb3d531f3f", + "1af1dfa857bf1d8814fe1af8983c18080019922e557f15a8a"}; + auto joined_shas = absl::StrJoin(expected_shas, ","); + EXPECT_CALL(*connection_info, sha256PeerCertificateChainDigests()) + .WillRepeatedly(Return(expected_shas)); + stream_info.downstream_connection_info_provider_->setSslConnection(connection_info); + EXPECT_EQ(joined_shas, upstream_format.formatWithContext({}, stream_info)); + EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), + ProtoEq(ValueUtil::stringValue(joined_shas))); + } + { + NiceMock stream_info; + StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_256"); + auto connection_info = std::make_shared(); + std::vector expected_shas; + EXPECT_CALL(*connection_info, sha256PeerCertificateChainDigests()) + .WillRepeatedly(Return(expected_shas)); + stream_info.downstream_connection_info_provider_->setSslConnection(connection_info); + EXPECT_EQ(absl::nullopt, upstream_format.formatWithContext({}, stream_info)); + EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), + ProtoEq(ValueUtil::nullValue())); + } + { + NiceMock stream_info; + stream_info.downstream_connection_info_provider_->setSslConnection(nullptr); + StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_256"); + EXPECT_EQ(absl::nullopt, upstream_format.formatWithContext({}, stream_info)); + EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), + ProtoEq(ValueUtil::nullValue())); + } + { + NiceMock stream_info; + StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_1"); + auto connection_info = std::make_shared(); + std::vector expected_shas{ + "685a2db593d5f86d346cb1a297009c3b467ad77f1944aa799039a2fb3d531f3f", + "1af1dfa857bf1d8814fe1af8983c18080019922e557f15a8a"}; + auto joined_shas = absl::StrJoin(expected_shas, ","); + EXPECT_CALL(*connection_info, sha1PeerCertificateChainDigests()) + .WillRepeatedly(Return(expected_shas)); + stream_info.downstream_connection_info_provider_->setSslConnection(connection_info); + EXPECT_EQ(joined_shas, upstream_format.formatWithContext({}, stream_info)); + EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), + ProtoEq(ValueUtil::stringValue(joined_shas))); + } + { + NiceMock stream_info; + StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_1"); + auto connection_info = std::make_shared(); + std::vector expected_shas; + EXPECT_CALL(*connection_info, sha1PeerCertificateChainDigests()) + .WillRepeatedly(Return(expected_shas)); + stream_info.downstream_connection_info_provider_->setSslConnection(connection_info); + EXPECT_EQ(absl::nullopt, upstream_format.formatWithContext({}, stream_info)); + EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), + ProtoEq(ValueUtil::nullValue())); + } + { + NiceMock stream_info; + stream_info.downstream_connection_info_provider_->setSslConnection(nullptr); + StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_1"); + EXPECT_EQ(absl::nullopt, upstream_format.formatWithContext({}, stream_info)); + EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), + ProtoEq(ValueUtil::nullValue())); + } + { + NiceMock stream_info; + StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_SERIALS"); + auto connection_info = std::make_shared(); + std::vector serial_numbers{"b8b5ecc898f2124a", "9bf18bd79b46589902639871"}; + auto joined_serials = absl::StrJoin(serial_numbers, ","); + EXPECT_CALL(*connection_info, serialNumbersPeerCertificates()) + .WillRepeatedly(Return(serial_numbers)); + stream_info.downstream_connection_info_provider_->setSslConnection(connection_info); + EXPECT_EQ(joined_serials, upstream_format.formatWithContext({}, stream_info)); + EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), + ProtoEq(ValueUtil::stringValue(joined_serials))); + } + { + NiceMock stream_info; + StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_SERIALS"); + std::vector empty_vec; + auto connection_info = std::make_shared(); + EXPECT_CALL(*connection_info, serialNumbersPeerCertificates()) + .WillRepeatedly(Return(empty_vec)); + stream_info.downstream_connection_info_provider_->setSslConnection(connection_info); + EXPECT_EQ(absl::nullopt, upstream_format.formatWithContext({}, stream_info)); + EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), + ProtoEq(ValueUtil::nullValue())); + } + { + NiceMock stream_info; + stream_info.downstream_connection_info_provider_->setSslConnection(nullptr); + StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_SERIALS"); + EXPECT_EQ(absl::nullopt, upstream_format.formatWithContext({}, stream_info)); + EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info), + ProtoEq(ValueUtil::nullValue())); + } { NiceMock stream_info; StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_ISSUER"); diff --git a/test/common/tls/ssl_socket_test.cc b/test/common/tls/ssl_socket_test.cc index 4ee59996b4d97..ba1ca0b655984 100644 --- a/test/common/tls/ssl_socket_test.cc +++ b/test/common/tls/ssl_socket_test.cc @@ -171,6 +171,12 @@ class TestUtilOptions : public TestUtilOptionsBase { const std::string& expectedSha256Digest() const { return expected_sha256_digest_; } + TestUtilOptions& setExpectedSha256Digests(std::vector& expected_sha256_digests) { + expected_sha256_digests_ = expected_sha256_digests; + return *this; + } + const std::vector expectedSha256Digests() const { return expected_sha256_digests_; } + TestUtilOptions& setExpectedSha1Digest(const std::string& expected_sha1_digest) { expected_sha1_digest_ = expected_sha1_digest; return *this; @@ -178,6 +184,13 @@ class TestUtilOptions : public TestUtilOptionsBase { const std::string& expectedSha1Digest() const { return expected_sha1_digest_; } + TestUtilOptions& setExpectedSha1Digests(std::vector& expected_sha1_digests) { + expected_sha1_digests_ = expected_sha1_digests; + return *this; + } + + const std::vector expectedSha1Digests() const { return expected_sha1_digests_; } + TestUtilOptions& setExpectedLocalUri(const std::string& expected_local_uri) { expected_local_uri_ = {expected_local_uri}; return *this; @@ -192,6 +205,13 @@ class TestUtilOptions : public TestUtilOptionsBase { const std::string& expectedSerialNumber() const { return expected_serial_number_; } + TestUtilOptions& setExpectedSerialNumbers(std::vector& expected_serial_numbers) { + expected_serial_numbers_ = expected_serial_numbers; + return *this; + } + + const std::vector expectedSerialNumbers() const { return expected_serial_numbers_; } + TestUtilOptions& setExpectedPeerIssuer(const std::string& expected_peer_issuer) { expected_peer_issuer_ = expected_peer_issuer; return *this; @@ -313,9 +333,12 @@ class TestUtilOptions : public TestUtilOptionsBase { NiceMock runtime_; Network::ConnectionEvent expected_server_close_event_{Network::ConnectionEvent::RemoteClose}; std::string expected_sha256_digest_; + std::vector expected_sha256_digests_; std::string expected_sha1_digest_; + std::vector expected_sha1_digests_; std::vector expected_local_uri_; std::string expected_serial_number_; + std::vector expected_serial_numbers_; std::string expected_peer_issuer_; std::string expected_peer_subject_; std::string expected_local_subject_; @@ -442,6 +465,13 @@ void testUtil(const TestUtilOptions& options) { EXPECT_EQ(options.expectedSha256Digest(), server_connection->ssl()->sha256PeerCertificateDigest()); } + if (!options.expectedSha256Digests().empty()) { + // Assert twice to ensure a cached value is returned and still valid. + EXPECT_EQ(options.expectedSha256Digests(), + server_connection->ssl()->sha256PeerCertificateChainDigests()); + EXPECT_EQ(options.expectedSha256Digests(), + server_connection->ssl()->sha256PeerCertificateChainDigests()); + } if (!options.expectedSha1Digest().empty()) { // Assert twice to ensure a cached value is returned and still valid. EXPECT_EQ(options.expectedSha1Digest(), @@ -449,6 +479,13 @@ void testUtil(const TestUtilOptions& options) { EXPECT_EQ(options.expectedSha1Digest(), server_connection->ssl()->sha1PeerCertificateDigest()); } + if (!options.expectedSha1Digests().empty()) { + // Assert twice to ensure a cached value is returned and still valid. + EXPECT_EQ(options.expectedSha1Digests(), + server_connection->ssl()->sha1PeerCertificateChainDigests()); + EXPECT_EQ(options.expectedSha1Digests(), + server_connection->ssl()->sha1PeerCertificateChainDigests()); + } // Assert twice to ensure a cached value is returned and still valid. EXPECT_EQ(options.expectedClientCertUri(), server_connection->ssl()->uriSanPeerCertificate()); EXPECT_EQ(options.expectedClientCertUri(), server_connection->ssl()->uriSanPeerCertificate()); @@ -458,18 +495,34 @@ void testUtil(const TestUtilOptions& options) { EXPECT_EQ(options.expectedLocalUri(), server_connection->ssl()->uriSanLocalCertificate()); EXPECT_EQ(options.expectedLocalUri(), server_connection->ssl()->uriSanLocalCertificate()); } + + EXPECT_EQ(options.expectedSerialNumber(), + server_connection->ssl()->serialNumberPeerCertificate()); EXPECT_EQ(options.expectedSerialNumber(), server_connection->ssl()->serialNumberPeerCertificate()); + if (!options.expectedSerialNumbers().empty()) { + // Assert twice to ensure a cached value is returned and still valid. + EXPECT_EQ(options.expectedSerialNumbers(), + server_connection->ssl()->serialNumbersPeerCertificates()); + EXPECT_EQ(options.expectedSerialNumbers(), + server_connection->ssl()->serialNumbersPeerCertificates()); + } if (!options.expectedPeerIssuer().empty()) { + // Assert twice to ensure a cached value is returned and still valid. + EXPECT_EQ(options.expectedPeerIssuer(), server_connection->ssl()->issuerPeerCertificate()); EXPECT_EQ(options.expectedPeerIssuer(), server_connection->ssl()->issuerPeerCertificate()); } if (!options.expectedPeerSubject().empty()) { EXPECT_EQ(options.expectedPeerSubject(), server_connection->ssl()->subjectPeerCertificate()); + EXPECT_EQ(options.expectedPeerSubject(), + server_connection->ssl()->subjectPeerCertificate()); } if (!options.expectedLocalSubject().empty()) { EXPECT_EQ(options.expectedLocalSubject(), server_connection->ssl()->subjectLocalCertificate()); + EXPECT_EQ(options.expectedLocalSubject(), + server_connection->ssl()->subjectLocalCertificate()); } if (!options.expectedPeerCert().empty()) { std::string urlencoded = absl::StrReplaceAll( @@ -817,6 +870,8 @@ void testUtilV2(const TestUtilOptionsV2& options) { if (!options.expectedALPNProtocol().empty()) { EXPECT_EQ(options.expectedALPNProtocol(), client_connection->nextProtocol()); } + // Assert twice to ensure a cached value is returned and still valid. + EXPECT_EQ(options.expectedClientCertUri(), server_connection->ssl()->uriSanPeerCertificate()); EXPECT_EQ(options.expectedClientCertUri(), server_connection->ssl()->uriSanPeerCertificate()); EXPECT_EQ(options.expectedClientCertIpSans(), server_connection->ssl()->ipSansPeerCertificate()); @@ -1098,6 +1153,63 @@ TEST_P(SslSocketTest, GetCertDigestInvalidFiles) { "")); } +TEST_P(SslSocketTest, GetCertDigests) { + const std::string client_ctx_yaml = R"EOF( + common_tls_context: + tls_certificates: + certificate_chain: + filename: "{{ test_rundir }}/test/common/tls/test_data/no_san_chain.pem" + private_key: + filename: "{{ test_rundir }}/test/common/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/common/tls/test_data/no_san_cert.pem" + private_key: + filename: "{{ test_rundir }}/test/common/tls/test_data/no_san_key.pem" + validation_context: + trusted_ca: + filename: "{{ test_rundir }}/test/common/tls/test_data/ca_cert.pem" +)EOF"; + + TestUtilOptions test_options(client_ctx_yaml, server_ctx_yaml, true, version_); + std::vector sha256Digests = absl::StrSplit(TEST_NO_SAN_CERT_CHAIN_256_HASHES, ','); + std::vector sha1Digests = absl::StrSplit(TEST_NO_SAN_CERT_CHAIN_1_HASHES, ','); + std::vector serialNumbers = absl::StrSplit(TEST_NO_SAN_CERT_CHAIN_SERIALS, ','); + testUtil(test_options.setExpectedSha256Digests(sha256Digests) + .setExpectedSha1Digests(sha1Digests) + .setExpectedSerialNumber(TEST_NO_SAN_CERT_SERIAL) // test checks first serial # + .setExpectedSerialNumbers(serialNumbers)); +} + +TEST_P(SslSocketTest, GetCertDigestsInvalidFiles) { + const std::string client_ctx_yaml = R"EOF( + common_tls_context: + tls_certificates: +)EOF"; + + const std::string server_ctx_yaml = R"EOF( + common_tls_context: + tls_certificates: + certificate_chain: + filename: "{{ test_rundir }}/test/common/tls/test_data/san_dns_cert.pem" + private_key: + filename: "{{ test_rundir }}/test/common/tls/test_data/san_dns_key.pem" + validation_context: + trusted_ca: + filename: "{{ test_rundir }}/test/common/tls/test_data/ca_cert.pem" +)EOF"; + + TestUtilOptions test_options(client_ctx_yaml, server_ctx_yaml, true, version_); + std::vector emptyStringVec; + testUtil(test_options.setExpectedSha256Digests(emptyStringVec) + .setExpectedSha1Digests(emptyStringVec) + .setExpectedSerialNumbers(emptyStringVec)); +} + TEST_P(SslSocketTest, GetCertDigestInline) { envoy::config::listener::v3::Listener listener; envoy::config::listener::v3::FilterChain* filter_chain = listener.add_filter_chains(); diff --git a/test/common/tls/test_data/no_san_cert_info.h b/test/common/tls/test_data/no_san_cert_info.h index 22f6718416188..9072c54c5509b 100644 --- a/test/common/tls/test_data/no_san_cert_info.h +++ b/test/common/tls/test_data/no_san_cert_info.h @@ -3,8 +3,15 @@ // NOLINT(namespace-envoy) constexpr char TEST_NO_SAN_CERT_256_HASH[] = "1fa3b1626367eda0b93b20cab52a08ace81aeab55245abf29b7920873658ce11"; +constexpr char TEST_NO_SAN_CERT_CHAIN_256_HASHES[] = + "1fa3b1626367eda0b93b20cab52a08ace81aeab55245abf29b7920873658ce11," + "91b048a0941a41740a243a8db4509ba31abdf0c00a1aa9fa36fe2e28d029b22f"; constexpr char TEST_NO_SAN_CERT_1_HASH[] = "c773be7b9f642ceadaa4ac8b8c8417e900435955"; +constexpr char TEST_NO_SAN_CERT_CHAIN_1_HASHES[] = "c773be7b9f642ceadaa4ac8b8c8417e900435955," + "635057edf7e2eabf863ab7fb5748a443aaeffee6"; constexpr char TEST_NO_SAN_CERT_SPKI[] = "eMMC8S2gS0LSZAF9bFmxP4YrI5NeUp/T+UzDKhJEiGA="; constexpr char TEST_NO_SAN_CERT_SERIAL[] = "7c252c75e95aa57a88f1f1c5cc3ff4fa9c5aa4c1"; +constexpr char TEST_NO_SAN_CERT_CHAIN_SERIALS[] = "7c252c75e95aa57a88f1f1c5cc3ff4fa9c5aa4c1," + "7c252c75e95aa57a88f1f1c5cc3ff4fa9c5aa4bf"; constexpr char TEST_NO_SAN_CERT_NOT_BEFORE[] = "Aug 22 07:51:29 2022 GMT"; constexpr char TEST_NO_SAN_CERT_NOT_AFTER[] = "Aug 21 07:51:29 2024 GMT"; diff --git a/test/common/tls/utility_test.cc b/test/common/tls/utility_test.cc index 005ca1fe58ca7..7f028922fa31b 100644 --- a/test/common/tls/utility_test.cc +++ b/test/common/tls/utility_test.cc @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -209,6 +211,25 @@ TEST(UtilityTest, TestGetX509ErrorInfo) { "verification error"); } +TEST(UtilityTest, TestMapX509Stack) { + bssl::UniquePtr cert_chain = readCertChainFromFile( + TestEnvironment::substitute("{{ test_rundir }}/test/common/tls/test_data/no_san_chain.pem")); + + std::vector expected_subject{ + "CN=Test Server,OU=Lyft Engineering,O=Lyft,L=San " + "Francisco,ST=California,C=US", + "CN=Test Intermediate CA,OU=Lyft Engineering,O=Lyft,L=San Francisco," + "ST=California,C=US"}; + auto func = [](X509& cert) -> std::string { return Utility::getSubjectFromCertificate(cert); }; + EXPECT_EQ(expected_subject, Utility::mapX509Stack(*cert_chain, func)); + + EXPECT_ENVOY_BUG(Utility::mapX509Stack(*sk_X509_new_null(), func), "x509 stack is empty or NULL"); + EXPECT_ENVOY_BUG(Utility::mapX509Stack(*cert_chain, nullptr), "field_extractor is nullptr"); + bssl::UniquePtr fakeCertChain(sk_X509_new_null()); + sk_X509_push(fakeCertChain.get(), nullptr); + EXPECT_EQ(std::vector{""}, Utility::mapX509Stack(*fakeCertChain, func)); +} + } // namespace } // namespace Tls } // namespace TransportSockets diff --git a/test/mocks/ssl/mocks.h b/test/mocks/ssl/mocks.h index 211c7afae6030..69e27613edae4 100644 --- a/test/mocks/ssl/mocks.h +++ b/test/mocks/ssl/mocks.h @@ -46,8 +46,11 @@ class MockConnectionInfo : public ConnectionInfo { MOCK_METHOD(bool, peerCertificateValidated, (), (const)); MOCK_METHOD(absl::Span, uriSanLocalCertificate, (), (const)); MOCK_METHOD(const std::string&, sha256PeerCertificateDigest, (), (const)); + MOCK_METHOD(absl::Span, sha256PeerCertificateChainDigests, (), (const)); MOCK_METHOD(const std::string&, sha1PeerCertificateDigest, (), (const)); + MOCK_METHOD(absl::Span, sha1PeerCertificateChainDigests, (), (const)); MOCK_METHOD(const std::string&, serialNumberPeerCertificate, (), (const)); + MOCK_METHOD(absl::Span, serialNumbersPeerCertificates, (), (const)); MOCK_METHOD(const std::string&, issuerPeerCertificate, (), (const)); MOCK_METHOD(const std::string&, subjectPeerCertificate, (), (const)); MOCK_METHOD(absl::Span, uriSanPeerCertificate, (), (const));