Skip to content

Commit

Permalink
Add new stream formatters for cert chain identifiers (envoyproxy#35513)
Browse files Browse the repository at this point in the history
Adds stream formatters to print the fingerprints and
serial for entire downstream cert chain
Fixes envoyproxy#35452

Signed-off-by: Keith Mattix II <[email protected]>
  • Loading branch information
keithmattix authored Aug 15, 2024
1 parent 3b73e46 commit e0c98ec
Show file tree
Hide file tree
Showing 13 changed files with 424 additions and 0 deletions.
4 changes: 4 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ new_features:
<envoy_v3_api_field_extensions.transport_sockets.tls.v3.CommonTlsContext.custom_tls_certificate_selector>`
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% <config_access_log_format_response_flags>`,
``%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 <extension_envoy.matching.inputs.dynamic_metadata>`
Expand Down
18 changes: 18 additions & 0 deletions docs/root/configuration/observability/access_log/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions envoy/ssl/connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ class ConnectionInfo {
**/
virtual const std::string& serialNumberPeerCertificate() const PURE;

/**
* @return absl::Span<const std::string> 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<const std::string> sha256PeerCertificateChainDigests() const PURE;

/**
* @return absl::Span<const std::string> 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<const std::string> sha1PeerCertificateChainDigests() const PURE;

/**
* @return absl::Span<const std::string> 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<const std::string> 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.
Expand Down
24 changes: 24 additions & 0 deletions source/common/formatter/stream_info_formatter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t>) {
return std::make_unique<StreamInfoSslConnectionInfoFormatterProvider>(
[](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<size_t>) {
return std::make_unique<StreamInfoSslConnectionInfoFormatterProvider>(
[](const Ssl::ConnectionInfo& connection_info) {
return absl::StrJoin(connection_info.sha1PeerCertificateChainDigests(), ",");
});
}}},
{"DOWNSTREAM_PEER_CHAIN_SERIALS",
{CommandSyntaxChecker::COMMAND_ONLY,
[](const absl::string_view, absl::optional<size_t>) {
return std::make_unique<StreamInfoSslConnectionInfoFormatterProvider>(
[](const Ssl::ConnectionInfo& connection_info) {
return absl::StrJoin(connection_info.serialNumbersPeerCertificates(), ",");
});
}}},
{"DOWNSTREAM_PEER_ISSUER",
{CommandSyntaxChecker::COMMAND_ONLY,
[](absl::string_view, absl::optional<size_t>) {
Expand Down
69 changes: 69 additions & 0 deletions source/common/tls/connection_info_impl_base.cc
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#include "source/common/tls/connection_info_impl_base.h"

#include <openssl/stack.h>

#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 {
Expand Down Expand Up @@ -77,6 +81,29 @@ const std::string& ConnectionInfoImplBase::sha256PeerCertificateDigest() const {
return cached_sha_256_peer_certificate_digest_;
}

absl::Span<const std::string> 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<uint8_t> 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_;
Expand All @@ -95,6 +122,29 @@ const std::string& ConnectionInfoImplBase::sha1PeerCertificateDigest() const {
return cached_sha_1_peer_certificate_digest_;
}

absl::Span<const std::string> 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<uint8_t> 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_;
Expand Down Expand Up @@ -253,6 +303,25 @@ const std::string& ConnectionInfoImplBase::serialNumberPeerCertificate() const {
return cached_serial_number_peer_certificate_;
}

absl::Span<const std::string> 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_;
Expand Down
6 changes: 6 additions & 0 deletions source/common/tls/connection_info_impl_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ class ConnectionInfoImplBase : public Ssl::ConnectionInfo {
bool peerCertificatePresented() const override;
absl::Span<const std::string> uriSanLocalCertificate() const override;
const std::string& sha256PeerCertificateDigest() const override;
absl::Span<const std::string> sha256PeerCertificateChainDigests() const override;
const std::string& sha1PeerCertificateDigest() const override;
absl::Span<const std::string> sha1PeerCertificateChainDigests() const override;
const std::string& serialNumberPeerCertificate() const override;
absl::Span<const std::string> serialNumbersPeerCertificates() const override;
const std::string& issuerPeerCertificate() const override;
const std::string& subjectPeerCertificate() const override;
const std::string& subjectLocalCertificate() const override;
Expand All @@ -48,8 +51,11 @@ class ConnectionInfoImplBase : public Ssl::ConnectionInfo {
protected:
mutable std::vector<std::string> cached_uri_san_local_certificate_;
mutable std::string cached_sha_256_peer_certificate_digest_;
mutable std::vector<std::string> cached_sha_256_peer_certificate_digests_;
mutable std::string cached_sha_1_peer_certificate_digest_;
mutable std::vector<std::string> cached_sha_1_peer_certificate_digests_;
mutable std::string cached_serial_number_peer_certificate_;
mutable std::vector<std::string> 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_;
Expand Down
25 changes: 25 additions & 0 deletions source/common/tls/utility.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "source/common/tls/utility.h"

#include <cstdint>
#include <vector>

#include "source/common/common/assert.h"
#include "source/common/common/empty_string.h"
Expand Down Expand Up @@ -463,6 +464,30 @@ std::string Utility::getX509VerificationErrorInfo(X509_STORE_CTX* ctx) {
return error_details;
}

std::vector<std::string> Utility::mapX509Stack(stack_st_X509& stack,
std::function<std::string(X509&)> field_extractor) {
std::vector<std::string> 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
Expand Down
9 changes: 9 additions & 0 deletions source/common/tls/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> returns the list of fields extracted from the certificates.
*/
std::vector<std::string> mapX509Stack(stack_st_X509& stack,
std::function<std::string(X509&)> field_extractor);

/**
* Retrieves the subject alternate names of a certificate.
* @param cert the certificate
Expand Down
103 changes: 103 additions & 0 deletions test/common/formatter/substitution_formatter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1699,6 +1699,109 @@ TEST(SubstitutionFormatterTest, streamInfoFormatterWithSsl) {
EXPECT_THAT(upstream_format.formatValueWithContext({}, stream_info),
ProtoEq(ValueUtil::nullValue()));
}
{
NiceMock<StreamInfo::MockStreamInfo> stream_info;
StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_256");
auto connection_info = std::make_shared<Ssl::MockConnectionInfo>();
std::vector<std::string> 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<StreamInfo::MockStreamInfo> stream_info;
StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_256");
auto connection_info = std::make_shared<Ssl::MockConnectionInfo>();
std::vector<std::string> 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<StreamInfo::MockStreamInfo> 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<StreamInfo::MockStreamInfo> stream_info;
StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_1");
auto connection_info = std::make_shared<Ssl::MockConnectionInfo>();
std::vector<std::string> 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<StreamInfo::MockStreamInfo> stream_info;
StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_FINGERPRINTS_1");
auto connection_info = std::make_shared<Ssl::MockConnectionInfo>();
std::vector<std::string> 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<StreamInfo::MockStreamInfo> 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<StreamInfo::MockStreamInfo> stream_info;
StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_SERIALS");
auto connection_info = std::make_shared<Ssl::MockConnectionInfo>();
std::vector<std::string> 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<StreamInfo::MockStreamInfo> stream_info;
StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_CHAIN_SERIALS");
std::vector<std::string> empty_vec;
auto connection_info = std::make_shared<Ssl::MockConnectionInfo>();
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<StreamInfo::MockStreamInfo> 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<StreamInfo::MockStreamInfo> stream_info;
StreamInfoFormatter upstream_format("DOWNSTREAM_PEER_ISSUER");
Expand Down
Loading

0 comments on commit e0c98ec

Please sign in to comment.