From 8fc89ac647806786e3f9a735eea01db6d415811d Mon Sep 17 00:00:00 2001 From: Derek Brown <6845676+DerekTBrown@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:39:32 -0400 Subject: [PATCH] ssl: add OID map properties for proxy-wasm This change adds new methods to the SSL ConnectionInfo interface to expose certificate extension OID maps, which allow proxy-wasm filters to access certificate extension data. This implements the proposal from proxy-wasm/spec#89. The new properties are: - connection.oid_map_local_certificate (map) - connection.oid_map_peer_certificate (map) - upstream.oid_map_local_certificate (map) - upstream.oid_map_peer_certificate (map) Each property provides a map of OID strings to their values extracted from certificate extensions. Signed-off-by: Derek Brown <6845676+DerekTBrown@users.noreply.github.com> --- envoy/ssl/connection.h | 15 +++ .../common/tls/connection_info_impl_base.cc | 22 +++++ source/common/tls/connection_info_impl_base.h | 6 +- source/common/tls/utility.cc | 29 ++++++ source/common/tls/utility.h | 7 ++ .../extensions/filters/common/expr/context.cc | 93 ++++++++++++++++++ .../extensions/filters/common/expr/context.h | 2 + test/common/tls/utility_test.cc | 28 ++++++ .../filters/common/expr/context_test.cc | 96 +++++++++++++++++++ test/mocks/ssl/mocks.h | 2 + 10 files changed, 299 insertions(+), 1 deletion(-) diff --git a/envoy/ssl/connection.h b/envoy/ssl/connection.h index 5070d3e64766f..62e83f309b0ae 100644 --- a/envoy/ssl/connection.h +++ b/envoy/ssl/connection.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -191,6 +192,20 @@ class ConnectionInfo { **/ virtual absl::Span oidsLocalCertificate() const PURE; + /** + * @return const std::map& the map of OID entries to their values + * from peer certificate extensions. Returns empty map if there is no peer certificate, + * or no extensions. + **/ + virtual const std::map& oidMapPeerCertificate() const PURE; + + /** + * @return const std::map& the map of OID entries to their values + * from local certificate extensions. Returns empty map if there is no local certificate, + * or no extensions. + **/ + virtual const std::map& oidMapLocalCertificate() const PURE; + /** * @return absl::optional the time that the peer certificate was issued and should be * considered valid from. Returns empty absl::optional if there is no peer certificate. diff --git a/source/common/tls/connection_info_impl_base.cc b/source/common/tls/connection_info_impl_base.cc index e46a313e05bd0..d29560db843ca 100644 --- a/source/common/tls/connection_info_impl_base.cc +++ b/source/common/tls/connection_info_impl_base.cc @@ -457,6 +457,28 @@ const std::string& ConnectionInfoImplBase::sessionId() const { }); } +const std::map& ConnectionInfoImplBase::oidMapPeerCertificate() const { + return getCachedValueOrCreate>( + CachedValueTag::OidMapPeerCertificate, [](SSL* ssl) { + bssl::UniquePtr cert(SSL_get_peer_certificate(ssl)); + if (!cert) { + return std::map{}; + } + return Utility::getCertificateOidMap(*cert); + }); +} + +const std::map& ConnectionInfoImplBase::oidMapLocalCertificate() const { + return getCachedValueOrCreate>( + CachedValueTag::OidMapLocalCertificate, [](SSL* ssl) { + X509* cert = SSL_get_certificate(ssl); + if (!cert) { + return std::map{}; + } + return Utility::getCertificateOidMap(*cert); + }); +} + } // namespace Tls } // namespace TransportSockets } // namespace Extensions diff --git a/source/common/tls/connection_info_impl_base.h b/source/common/tls/connection_info_impl_base.h index 1a5135539728c..6273df21ac5e9 100644 --- a/source/common/tls/connection_info_impl_base.h +++ b/source/common/tls/connection_info_impl_base.h @@ -45,6 +45,8 @@ class ConnectionInfoImplBase : public Ssl::ConnectionInfo { absl::Span othernameSansLocalCertificate() const override; absl::Span oidsPeerCertificate() const override; absl::Span oidsLocalCertificate() const override; + const std::map& oidMapPeerCertificate() const override; + const std::map& oidMapLocalCertificate() const override; absl::optional validFromPeerCertificate() const override; absl::optional expirationPeerCertificate() const override; const std::string& sessionId() const override; @@ -88,6 +90,8 @@ class ConnectionInfoImplBase : public Ssl::ConnectionInfo { IpSansPeerCertificate, OidsPeerCertificate, OidsLocalCertificate, + OidMapPeerCertificate, + OidMapLocalCertificate, }; // Retrieve the given tag from the set of cached values, or create the value via the supplied @@ -101,7 +105,7 @@ class ConnectionInfoImplBase : public Ssl::ConnectionInfo { // table of cached values that are created on demand. Use a node_hash_map so that returned // references are not invalidated when additional items are added. using CachedValue = absl::variant, Ssl::ParsedX509NamePtr, - bssl::UniquePtr>; + bssl::UniquePtr, std::map>; mutable absl::node_hash_map cached_values_; }; diff --git a/source/common/tls/utility.cc b/source/common/tls/utility.cc index 6024c9fe97e5f..bbfb51c4609cd 100644 --- a/source/common/tls/utility.cc +++ b/source/common/tls/utility.cc @@ -493,6 +493,35 @@ absl::string_view Utility::getCertificateExtensionValue(X509& cert, static_cast(octet_string_length)}; } +std::map Utility::getCertificateOidMap(X509& cert) { + std::map extension_map; + + int count = X509_get_ext_count(&cert); + for (int pos = 0; pos < count; pos++) { + X509_EXTENSION* extension = X509_get_ext(&cert, pos); + RELEASE_ASSERT(extension != nullptr, ""); + + char oid[MAX_OID_LENGTH]; + int obj_len = OBJ_obj2txt(oid, MAX_OID_LENGTH, X509_EXTENSION_get_object(extension), + 1 /* always_return_oid */); + if (obj_len > 0 && obj_len < MAX_OID_LENGTH) { + const ASN1_OCTET_STRING* octet_string = X509_EXTENSION_get_data(extension); + RELEASE_ASSERT(octet_string != nullptr, ""); + + const unsigned char* octet_string_data = ASN1_STRING_get0_data(octet_string); + const int octet_string_length = ASN1_STRING_length(octet_string); + + std::string oid_str(oid); + std::string value_str(reinterpret_cast(octet_string_data), + static_cast(octet_string_length)); + + extension_map[oid_str] = value_str; + } + } + + return extension_map; +} + SystemTime Utility::getValidFrom(const X509& cert) { int days, seconds; int rc = ASN1_TIME_diff(&days, &seconds, &epochASN1Time(), X509_get0_notBefore(&cert)); diff --git a/source/common/tls/utility.h b/source/common/tls/utility.h index 3522efc9672e5..2fa7a16a3c007 100644 --- a/source/common/tls/utility.h +++ b/source/common/tls/utility.h @@ -116,6 +116,13 @@ std::vector getCertificateExtensionOids(X509& cert); */ absl::string_view getCertificateExtensionValue(X509& cert, absl::string_view extension_name); +/** + * Retrieves all OIDs and their values from a certificate's extensions as a map. + * @param cert the certificate. + * @return std::map a map of OID strings to their extension values. + */ +std::map getCertificateOidMap(X509& cert); + /** * Returns the seconds since unix epoch of the expiration time of this certificate. * @param cert the certificate diff --git a/source/extensions/filters/common/expr/context.cc b/source/extensions/filters/common/expr/context.cc index b46de5c899113..5544986b0d6dd 100644 --- a/source/extensions/filters/common/expr/context.cc +++ b/source/extensions/filters/common/expr/context.cc @@ -10,12 +10,45 @@ #include "absl/strings/numbers.h" #include "absl/time/time.h" +#include "eval/public/cel_value.h" +#include "eval/public/containers/container_backed_list_impl.h" + namespace Envoy { namespace Extensions { namespace Filters { namespace Common { namespace Expr { +// A simple constant map for CEL use +class ConstMap : public google::api::expr::runtime::CelMap { +public: + using GetValueFunction = std::function(CelValue)>; + using SizeFunction = std::function; + using KeysFunction = std::function()>; + + ConstMap(GetValueFunction get_value, SizeFunction size_func, KeysFunction keys_func = nullptr) + : get_value_(std::move(get_value)), size_func_(std::move(size_func)), keys_func_(std::move(keys_func)) {} + + absl::optional operator[](CelValue key) const override { return get_value_(key); } + int size() const override { return size_func_(); } + bool empty() const override { return size() == 0; } + + absl::StatusOr ListKeys() const override { + // Create a container-backed list with keys from the map + if (keys_func_ == nullptr) { + static const google::api::expr::runtime::ContainerBackedListImpl empty_list({}); + return &empty_list; + } + auto keys = keys_func_(); + return new google::api::expr::runtime::ContainerBackedListImpl(std::move(keys)); + } + +private: + GetValueFunction get_value_; + SizeFunction size_func_; + KeysFunction keys_func_; +}; + Http::RegisterCustomInlineHeader referer_handle(Http::CustomHeaders::get().Referer); @@ -89,6 +122,66 @@ const SslExtractorsValues& SslExtractorsValues::get() { return {}; } return CelValue::CreateString(&info.sha256PeerCertificateDigest()); + }}, + {OidMapLocalCertificate, + [](const Ssl::ConnectionInfo& info) -> absl::optional { + const auto& oid_map = info.oidMapLocalCertificate(); + if (oid_map.empty()) { + return {}; + } + // Create a map of OID strings to their values + auto cel_map = std::make_unique( + [&oid_map](CelValue key) -> absl::optional { + if (!key.IsString()) { + return {}; + } + auto str = std::string(key.StringOrDie().value()); + auto it = oid_map.find(str); + if (it == oid_map.end()) { + return {}; + } + return CelValue::CreateStringView(it->second); + }, + [&oid_map]() -> int { return oid_map.size(); }, + [&oid_map]() -> std::vector { + std::vector keys; + keys.reserve(oid_map.size()); + for (const auto& pair : oid_map) { + keys.push_back(CelValue::CreateStringView(pair.first)); + } + return keys; + }); + return CelValue::CreateMap(cel_map.release()); + }}, + {OidMapPeerCertificate, + [](const Ssl::ConnectionInfo& info) -> absl::optional { + const auto& oid_map = info.oidMapPeerCertificate(); + if (oid_map.empty()) { + return {}; + } + // Create a map of OID strings to their values + auto cel_map = std::make_unique( + [&oid_map](CelValue key) -> absl::optional { + if (!key.IsString()) { + return {}; + } + auto str = std::string(key.StringOrDie().value()); + auto it = oid_map.find(str); + if (it == oid_map.end()) { + return {}; + } + return CelValue::CreateStringView(it->second); + }, + [&oid_map]() -> int { return oid_map.size(); }, + [&oid_map]() -> std::vector { + std::vector keys; + keys.reserve(oid_map.size()); + for (const auto& pair : oid_map) { + keys.push_back(CelValue::CreateStringView(pair.first)); + } + return keys; + }); + return CelValue::CreateMap(cel_map.release()); }}}); } diff --git a/source/extensions/filters/common/expr/context.h b/source/extensions/filters/common/expr/context.h index 5f966f1e8d705..d0f8ce9daea01 100644 --- a/source/extensions/filters/common/expr/context.h +++ b/source/extensions/filters/common/expr/context.h @@ -71,6 +71,8 @@ constexpr absl::string_view DNSSanLocalCertificate = "dns_san_local_certificate" constexpr absl::string_view DNSSanPeerCertificate = "dns_san_peer_certificate"; constexpr absl::string_view SHA256PeerCertificateDigest = "sha256_peer_certificate_digest"; constexpr absl::string_view DownstreamTransportFailureReason = "transport_failure_reason"; +constexpr absl::string_view OidMapLocalCertificate = "oid_map_local_certificate"; +constexpr absl::string_view OidMapPeerCertificate = "oid_map_peer_certificate"; // Source properties constexpr absl::string_view Source = "source"; diff --git a/test/common/tls/utility_test.cc b/test/common/tls/utility_test.cc index d70bd3e7f5e67..34a5dac9f4d95 100644 --- a/test/common/tls/utility_test.cc +++ b/test/common/tls/utility_test.cc @@ -248,6 +248,34 @@ TEST(UtilityTest, TestGetCertificationExtensionValue) { EXPECT_EQ("", Utility::getCertificateExtensionValue(*cert, "foo")); } +TEST(UtilityTest, TestGetCertificateOidMap) { + bssl::UniquePtr cert = readCertFromFile(TestEnvironment::substitute( + "{{ test_rundir }}/test/common/tls/test_data/extensions_cert.pem")); + const auto& oid_map = Utility::getCertificateOidMap(*cert); + + EXPECT_EQ(7, oid_map.size()); + + // Check that all expected OIDs are present + std::vector expected_oids{ + "2.5.29.14", "2.5.29.15", "2.5.29.19", + "2.5.29.35", "2.5.29.37", + "1.2.3.4.5.6.7.8", "1.2.3.4.5.6.7.9"}; + + for (const auto& oid : expected_oids) { + EXPECT_NE(oid_map.find(oid), oid_map.end()); + } + + // Check specific values for custom OIDs + EXPECT_EQ("\xc\x9Something", oid_map.at("1.2.3.4.5.6.7.8")); + EXPECT_EQ("\x30\x3\x1\x1\xFF", oid_map.at("1.2.3.4.5.6.7.9")); + + // Test with a certificate that has no extensions + bssl::UniquePtr no_ext_cert = readCertFromFile(TestEnvironment::substitute( + "{{ test_rundir }}/test/common/tls/test_data/no_extension_cert.pem")); + const auto& empty_map = Utility::getCertificateOidMap(*no_ext_cert); + EXPECT_TRUE(empty_map.empty()); +} + TEST(UtilityTest, SslErrorDescriptionTest) { const std::vector> test_set = { {SSL_ERROR_NONE, "NONE"}, diff --git a/test/extensions/filters/common/expr/context_test.cc b/test/extensions/filters/common/expr/context_test.cc index 50d9d96f253f4..99fa45d347201 100644 --- a/test/extensions/filters/common/expr/context_test.cc +++ b/test/extensions/filters/common/expr/context_test.cc @@ -837,6 +837,102 @@ TEST(Context, ConnectionAttributes) { EXPECT_TRUE(Protobuf::util::MessageDifferencer::Equals(*value.value().MessageOrDie(), upstream_locality)); } + + // Test OID map properties + { + // Create maps of OIDs to values + std::map local_oid_map = { + {"1.2.840.113549.1.9.1", "test@example.com"}, + {"2.5.4.10", "Test Organization"} + }; + + std::map peer_oid_map = { + {"1.2.840.113549.1.9.1", "peer@example.com"}, + {"2.5.4.11", "Test OU"} + }; + + // Set up expectations for OID maps + EXPECT_CALL(*downstream_ssl_info, oidMapLocalCertificate()) + .WillRepeatedly(ReturnRef(local_oid_map)); + EXPECT_CALL(*downstream_ssl_info, oidMapPeerCertificate()) + .WillRepeatedly(ReturnRef(peer_oid_map)); + EXPECT_CALL(*upstream_ssl_info, oidMapLocalCertificate()) + .WillRepeatedly(ReturnRef(local_oid_map)); + EXPECT_CALL(*upstream_ssl_info, oidMapPeerCertificate()) + .WillRepeatedly(ReturnRef(peer_oid_map)); + + // Test downstream local OID map + { + auto value = connection[CelValue::CreateStringView(OidMapLocalCertificate)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsMap()); + auto& map = *value.value().MapOrDie(); + + auto email = map[CelValue::CreateStringView("1.2.840.113549.1.9.1")]; + EXPECT_TRUE(email.has_value()); + EXPECT_TRUE(email.value().IsString()); + EXPECT_EQ("test@example.com", email.value().StringOrDie().value()); + + auto org = map[CelValue::CreateStringView("2.5.4.10")]; + EXPECT_TRUE(org.has_value()); + EXPECT_TRUE(org.value().IsString()); + EXPECT_EQ("Test Organization", org.value().StringOrDie().value()); + } + + // Test downstream peer OID map + { + auto value = connection[CelValue::CreateStringView(OidMapPeerCertificate)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsMap()); + auto& map = *value.value().MapOrDie(); + + auto email = map[CelValue::CreateStringView("1.2.840.113549.1.9.1")]; + EXPECT_TRUE(email.has_value()); + EXPECT_TRUE(email.value().IsString()); + EXPECT_EQ("peer@example.com", email.value().StringOrDie().value()); + + auto ou = map[CelValue::CreateStringView("2.5.4.11")]; + EXPECT_TRUE(ou.has_value()); + EXPECT_TRUE(ou.value().IsString()); + EXPECT_EQ("Test OU", ou.value().StringOrDie().value()); + } + + // Test upstream local OID map + { + auto value = upstream[CelValue::CreateStringView(OidMapLocalCertificate)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsMap()); + auto& map = *value.value().MapOrDie(); + + auto email = map[CelValue::CreateStringView("1.2.840.113549.1.9.1")]; + EXPECT_TRUE(email.has_value()); + EXPECT_TRUE(email.value().IsString()); + EXPECT_EQ("test@example.com", email.value().StringOrDie().value()); + + auto org = map[CelValue::CreateStringView("2.5.4.10")]; + EXPECT_TRUE(org.has_value()); + EXPECT_TRUE(org.value().IsString()); + EXPECT_EQ("Test Organization", org.value().StringOrDie().value()); + } + + // Test upstream peer OID map + { + auto value = upstream[CelValue::CreateStringView(OidMapPeerCertificate)]; + EXPECT_TRUE(value.has_value()); + ASSERT_TRUE(value.value().IsMap()); + auto& map = *value.value().MapOrDie(); + + auto email = map[CelValue::CreateStringView("1.2.840.113549.1.9.1")]; + EXPECT_TRUE(email.has_value()); + EXPECT_TRUE(email.value().IsString()); + EXPECT_EQ("peer@example.com", email.value().StringOrDie().value()); + + auto ou = map[CelValue::CreateStringView("2.5.4.11")]; + EXPECT_TRUE(ou.has_value()); + EXPECT_TRUE(ou.value().IsString()); + EXPECT_EQ("Test OU", ou.value().StringOrDie().value()); + } + } } TEST(Context, FilterStateAttributes) { diff --git a/test/mocks/ssl/mocks.h b/test/mocks/ssl/mocks.h index ad7c72c3e1357..a2a10ce3c6bb1 100644 --- a/test/mocks/ssl/mocks.h +++ b/test/mocks/ssl/mocks.h @@ -71,6 +71,8 @@ class MockConnectionInfo : public ConnectionInfo { MOCK_METHOD(absl::Span, othernameSansLocalCertificate, (), (const)); MOCK_METHOD(absl::Span, oidsPeerCertificate, (), (const)); MOCK_METHOD(absl::Span, oidsLocalCertificate, (), (const)); + MOCK_METHOD((const std::map&), oidMapPeerCertificate, (), (const)); + MOCK_METHOD((const std::map&), oidMapLocalCertificate, (), (const)); MOCK_METHOD(absl::optional, validFromPeerCertificate, (), (const)); MOCK_METHOD(absl::optional, expirationPeerCertificate, (), (const)); MOCK_METHOD(const std::string&, sessionId, (), (const));