Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 7 additions & 71 deletions source/extensions/filters/http/gcp_authn/crypto_utils.cc
Original file line number Diff line number Diff line change
@@ -1,29 +1,16 @@
#include "source/extensions/filters/http/gcp_authn/crypto_utils.h"

#include <openssl/asn1.h>
#include <openssl/bio.h>
#include <openssl/mem.h>
#include <openssl/nid.h>
#include <openssl/pem.h>
#include <openssl/sha2.h>
#include <openssl/stack.h>
#include <openssl/x509.h>

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "envoy/api/api.h"
#include "envoy/secret/secret_provider.h"

#include "source/common/common/base64.h"
#include "source/common/common/logger.h"
#include "source/common/common/matchers.h"
#include "source/common/common/thread.h"
#include "source/common/config/datasource.h"
#include "source/common/tls/utility.h"

#include "absl/status/status.h"
#include "absl/status/statusor.h"
Expand All @@ -34,27 +21,6 @@ namespace HttpFilters {
namespace GcpAuthn {
namespace {

std::vector<std::string> getSubjectAltNames(X509* cert) {
std::vector<std::string> sans =
Envoy::Extensions::TransportSockets::Tls::Utility::getSubjectAltNames(*cert, GEN_URI);
std::vector<std::string> dns_sans =
Envoy::Extensions::TransportSockets::Tls::Utility::getSubjectAltNames(*cert, GEN_DNS);
sans.insert(sans.end(), dns_sans.begin(), dns_sans.end());
return sans;
}

bool validateSubjectAltNames(const std::vector<std::string>& sans,
const std::vector<Matchers::StringMatcherImpl>& san_matchers) {
for (const auto& san : sans) {
for (const auto& matcher : san_matchers) {
if (matcher.match(san)) {
return true;
}
}
}
return false;
}

absl::StatusOr<std::string> calculateFingerprint(X509* cert) {
unsigned char* der = nullptr;
int len = i2d_X509(cert, &der);
Expand All @@ -73,47 +39,17 @@ absl::StatusOr<std::string> calculateFingerprint(X509* cert) {

} // namespace

absl::StatusOr<std::string> getBase64EncodedCertificateFingerprint(
Secret::TlsCertificateConfigProviderSharedPtr tls_cert_provider,
const std::vector<Matchers::StringMatcherImpl>& san_matchers, Api::Api& api) {
// Config::DataSource::read() is blocking and should only be called on the
// main thread.
ASSERT_IS_MAIN_OR_TEST_THREAD();

if (san_matchers.empty()) {
return absl::InvalidArgumentError("SAN matchers are empty");
}

if (tls_cert_provider == nullptr) {
return absl::InvalidArgumentError("TLS certificate provider is null");
}
const auto* secret = tls_cert_provider->secret();
if (secret == nullptr) {
return absl::NotFoundError("Secret is null");
}

// Read certificate from secret.
const auto& cert_chain = secret->certificate_chain();
auto file_content_or_error = Config::DataSource::read(cert_chain, true, api);
if (!file_content_or_error.ok()) {
return absl::InternalError("Failed to read certificate from data source");
absl::StatusOr<std::string>
CertFingerprinterImpl::getFingerprintFromPem(const std::string& pem) const {
if (pem.empty()) {
return absl::InvalidArgumentError("Certificate PEM content is empty");
}
std::string file_content = file_content_or_error.value();

if (file_content.empty()) {
return absl::InvalidArgumentError("Certificate file content is empty");
}

bssl::UniquePtr<BIO> bio(BIO_new_mem_buf(file_content.data(), file_content.size()));
bssl::UniquePtr<BIO> bio(BIO_new_mem_buf(pem.data(), pem.size()));
bssl::UniquePtr<X509> cert(PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr));
if (!cert) {
ENVOY_LOG_MISC(error, "Failed to parse certificate");
return absl::InvalidArgumentError("Failed to parse certificate");
}

std::vector<std::string> sans = getSubjectAltNames(cert.get());
if (!validateSubjectAltNames(sans, san_matchers)) {
return absl::InvalidArgumentError("Subject Alternative Names do not match");
ENVOY_LOG_MISC(error, "Failed to parse certificate from PEM");
return absl::InvalidArgumentError("Failed to parse certificate from PEM");
}

return calculateFingerprint(cert.get());
Expand Down
21 changes: 11 additions & 10 deletions source/extensions/filters/http/gcp_authn/crypto_utils.h
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
#pragma once

#include <optional>
#include <string>
#include <vector>

#include "envoy/api/api.h"
#include "envoy/secret/secret_provider.h"

#include "source/common/common/matchers.h"

#include "absl/status/statusor.h"

Expand All @@ -16,9 +9,17 @@ namespace Extensions {
namespace HttpFilters {
namespace GcpAuthn {

absl::StatusOr<std::string> getBase64EncodedCertificateFingerprint(
Secret::TlsCertificateConfigProviderSharedPtr tls_cert_provider,
const std::vector<Matchers::StringMatcherImpl>& san_matchers, Api::Api& api);
class CertFingerprinter {
public:
virtual ~CertFingerprinter() = default;
virtual absl::StatusOr<std::string> getFingerprintFromPem(const std::string& pem) const = 0;
};
using CertFingerprinterSharedPtr = std::shared_ptr<const CertFingerprinter>;

class CertFingerprinterImpl : public CertFingerprinter {
public:
absl::StatusOr<std::string> getFingerprintFromPem(const std::string& pem) const override;
};

} // namespace GcpAuthn
} // namespace HttpFilters
Expand Down
3 changes: 0 additions & 3 deletions test/extensions/filters/http/gcp_authn/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,9 @@ envoy_extension_cc_test(
extension_names = ["envoy.filters.http.gcp_authn"],
deps = [
"//envoy/api:api_interface",
"//source/common/common:matchers_lib",
"//source/extensions/filters/http/gcp_authn:crypto_utils",
"//test/mocks/secret:secret_mocks",
"//test/test_common:environment_lib",
"//test/test_common:utility_lib",
"@abseil-cpp//absl/status",
"@envoy_api//envoy/extensions/transport_sockets/tls/v3:pkg_cc_proto",
],
)
173 changes: 12 additions & 161 deletions test/extensions/filters/http/gcp_authn/crypto_utils_test.cc
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
#include <functional>
#include <memory>
#include <string>
#include <vector>

#include "envoy/api/api.h"
#include "envoy/common/callback.h"
#include "envoy/extensions/transport_sockets/tls/v3/common.pb.h"
#include "envoy/extensions/transport_sockets/tls/v3/secret.pb.h"
#include "envoy/init/target.h"
#include "envoy/secret/secret_provider.h"

#include "source/common/common/matchers.h"
#include "source/extensions/filters/http/gcp_authn/crypto_utils.h"

#include "test/test_common/environment.h"
Expand All @@ -21,183 +12,43 @@
#include "gtest/gtest.h"

namespace Envoy {
namespace Secret {

class MockTlsCertificateConfigProvider : public TlsCertificateConfigProvider {
public:
MockTlsCertificateConfigProvider();
~MockTlsCertificateConfigProvider() override;

MOCK_METHOD(const envoy::extensions::transport_sockets::tls::v3::TlsCertificate*, secret, (),
(const));
MOCK_METHOD(Common::CallbackHandlePtr, addValidationCallback,
(std::function<absl::Status(
const envoy::extensions::transport_sockets::tls::v3::TlsCertificate&)>));
MOCK_METHOD(Common::CallbackHandlePtr, addUpdateCallback, (std::function<absl::Status()>));
MOCK_METHOD(Common::CallbackHandlePtr, addRemoveCallback, (std::function<absl::Status()>));
MOCK_METHOD(const Init::Target*, initTarget, ());
MOCK_METHOD(void, start, ());
};

MockTlsCertificateConfigProvider::MockTlsCertificateConfigProvider() = default;
MockTlsCertificateConfigProvider::~MockTlsCertificateConfigProvider() = default;

} // namespace Secret

namespace Extensions {
namespace HttpFilters {
namespace GcpAuthn {
namespace {

using testing::NiceMock;
using testing::Return;

class CryptoUtilsTest : public testing::Test {
public:
CryptoUtilsTest() : api_(Api::createApiForTest()) {}

Api::ApiPtr api_;
};

TEST_F(CryptoUtilsTest, GetBase64EncodedCertificateFingerprintSuccess) {
auto secret_provider = std::make_shared<NiceMock<Secret::MockTlsCertificateConfigProvider>>();
auto secret = std::make_unique<envoy::extensions::transport_sockets::tls::v3::TlsCertificate>();

TEST_F(CryptoUtilsTest, GetFingerprintFromPemSuccess) {
CertFingerprinterImpl fingerprinter;
std::string cert_path =
TestEnvironment::runfilesPath("test/config/integration/certs/clientcert.pem");
secret->mutable_certificate_chain()->set_filename(cert_path);

EXPECT_CALL(*secret_provider, secret()).WillRepeatedly(Return(secret.get()));
std::string cert_pem = TestEnvironment::readFileToStringForTest(cert_path);

std::vector<Matchers::StringMatcherImpl> san_matchers;
san_matchers.emplace_back(
Matchers::StringMatcherImpl::createExactMatcher("spiffe://lyft.com/frontend-team"));

auto fingerprint = getBase64EncodedCertificateFingerprint(secret_provider, san_matchers, *api_);
auto fingerprint = fingerprinter.getFingerprintFromPem(cert_pem);
ASSERT_TRUE(fingerprint.ok());
// SHA256 fingerprint of
// test/config/integration/certs/clientcert.pem
// SPELLCHECKER(off)
EXPECT_EQ(fingerprint.value(), "wH4U/EO5x7PZLxAE+R06ngcdnJOlivx2tMFDA646DzQ");
// SPELLCHECKER(on)
}

TEST_F(CryptoUtilsTest, GetBase64EncodedCertificateFingerprintSuccessDnsSan) {
auto secret_provider = std::make_shared<NiceMock<Secret::MockTlsCertificateConfigProvider>>();
auto secret = std::make_unique<envoy::extensions::transport_sockets::tls::v3::TlsCertificate>();

std::string cert_path =
TestEnvironment::runfilesPath("test/config/integration/certs/upstreamlocalhostcert.pem");
secret->mutable_certificate_chain()->set_filename(cert_path);

EXPECT_CALL(*secret_provider, secret()).WillRepeatedly(Return(secret.get()));

std::vector<Matchers::StringMatcherImpl> san_matchers;
san_matchers.emplace_back(Matchers::StringMatcherImpl::createExactMatcher("localhost"));

auto fingerprint = getBase64EncodedCertificateFingerprint(secret_provider, san_matchers, *api_);
ASSERT_TRUE(fingerprint.ok());
}

TEST_F(CryptoUtilsTest, GetBase64EncodedCertificateFingerprintNoSecret) {
auto secret_provider = std::make_shared<NiceMock<Secret::MockTlsCertificateConfigProvider>>();
EXPECT_CALL(*secret_provider, secret()).WillRepeatedly(Return(nullptr));

std::vector<Matchers::StringMatcherImpl> san_matchers;
san_matchers.emplace_back(
Matchers::StringMatcherImpl::createExactMatcher("spiffe://lyft.com/frontend-team"));
auto fingerprint = getBase64EncodedCertificateFingerprint(secret_provider, san_matchers, *api_);
EXPECT_FALSE(fingerprint.ok());
EXPECT_EQ(fingerprint.status().message(), "Secret is null");
}

TEST_F(CryptoUtilsTest, GetBase64EncodedCertificateFingerprintSanMismatch) {
auto secret_provider = std::make_shared<NiceMock<Secret::MockTlsCertificateConfigProvider>>();
auto secret = std::make_unique<envoy::extensions::transport_sockets::tls::v3::TlsCertificate>();

std::string cert_path =
TestEnvironment::runfilesPath("test/config/integration/certs/clientcert.pem");
secret->mutable_certificate_chain()->set_filename(cert_path);

EXPECT_CALL(*secret_provider, secret()).WillRepeatedly(Return(secret.get()));

std::vector<Matchers::StringMatcherImpl> san_matchers;
san_matchers.emplace_back(
Matchers::StringMatcherImpl::createExactMatcher("spiffe://example.com/other-team"));

auto fingerprint = getBase64EncodedCertificateFingerprint(secret_provider, san_matchers, *api_);
EXPECT_FALSE(fingerprint.ok());
EXPECT_EQ(fingerprint.status().message(), "Subject Alternative Names do not match");
}

TEST_F(CryptoUtilsTest, GetBase64EncodedCertificateFingerprintEmptyMatchers) {
auto secret_provider = std::make_shared<NiceMock<Secret::MockTlsCertificateConfigProvider>>();
auto secret = std::make_unique<envoy::extensions::transport_sockets::tls::v3::TlsCertificate>();

std::string cert_path =
TestEnvironment::runfilesPath("test/config/integration/certs/clientcert.pem");
secret->mutable_certificate_chain()->set_filename(cert_path);

EXPECT_CALL(*secret_provider, secret()).WillRepeatedly(Return(secret.get()));

std::vector<Matchers::StringMatcherImpl> san_matchers; // Empty
auto fingerprint = getBase64EncodedCertificateFingerprint(secret_provider, san_matchers, *api_);
EXPECT_FALSE(fingerprint.ok());
EXPECT_EQ(fingerprint.status().message(), "SAN matchers are empty");
}

TEST_F(CryptoUtilsTest, GetBase64EncodedCertificateFingerprintNullProvider) {
std::vector<Matchers::StringMatcherImpl> san_matchers;
san_matchers.emplace_back(
Matchers::StringMatcherImpl::createExactMatcher("spiffe://lyft.com/frontend-team"));
auto fingerprint = getBase64EncodedCertificateFingerprint(nullptr, san_matchers, *api_);
TEST_F(CryptoUtilsTest, GetFingerprintFromPemEmpty) {
CertFingerprinterImpl fingerprinter;
auto fingerprint = fingerprinter.getFingerprintFromPem("");
EXPECT_FALSE(fingerprint.ok());
EXPECT_EQ(fingerprint.status().message(), "TLS certificate provider is null");
EXPECT_EQ(fingerprint.status().message(), "Certificate PEM content is empty");
}

TEST_F(CryptoUtilsTest, GetBase64EncodedCertificateFingerprintDataSourceReadFailure) {
auto secret_provider = std::make_shared<NiceMock<Secret::MockTlsCertificateConfigProvider>>();
auto secret = std::make_unique<envoy::extensions::transport_sockets::tls::v3::TlsCertificate>();

secret->mutable_certificate_chain()->set_filename("/nonexistent_file.pem");
EXPECT_CALL(*secret_provider, secret()).WillRepeatedly(Return(secret.get()));

std::vector<Matchers::StringMatcherImpl> san_matchers;
san_matchers.emplace_back(
Matchers::StringMatcherImpl::createExactMatcher("spiffe://lyft.com/frontend-team"));
auto fingerprint = getBase64EncodedCertificateFingerprint(secret_provider, san_matchers, *api_);
EXPECT_FALSE(fingerprint.ok());
EXPECT_EQ(fingerprint.status().message(), "Failed to read certificate from data source");
}

TEST_F(CryptoUtilsTest, GetBase64EncodedCertificateFingerprintEmptyContent) {
auto secret_provider = std::make_shared<NiceMock<Secret::MockTlsCertificateConfigProvider>>();
auto secret = std::make_unique<envoy::extensions::transport_sockets::tls::v3::TlsCertificate>();

secret->mutable_certificate_chain()->set_inline_string("");
EXPECT_CALL(*secret_provider, secret()).WillRepeatedly(Return(secret.get()));

std::vector<Matchers::StringMatcherImpl> san_matchers;
san_matchers.emplace_back(
Matchers::StringMatcherImpl::createExactMatcher("spiffe://lyft.com/frontend-team"));
auto fingerprint = getBase64EncodedCertificateFingerprint(secret_provider, san_matchers, *api_);
EXPECT_FALSE(fingerprint.ok());
EXPECT_EQ(fingerprint.status().message(), "Certificate file content is empty");
}

TEST_F(CryptoUtilsTest, GetBase64EncodedCertificateFingerprintParseFailure) {
auto secret_provider = std::make_shared<NiceMock<Secret::MockTlsCertificateConfigProvider>>();
auto secret = std::make_unique<envoy::extensions::transport_sockets::tls::v3::TlsCertificate>();

secret->mutable_certificate_chain()->set_inline_string("invalid cert content");
EXPECT_CALL(*secret_provider, secret()).WillRepeatedly(Return(secret.get()));

std::vector<Matchers::StringMatcherImpl> san_matchers;
san_matchers.emplace_back(
Matchers::StringMatcherImpl::createExactMatcher("spiffe://lyft.com/frontend-team"));
auto fingerprint = getBase64EncodedCertificateFingerprint(secret_provider, san_matchers, *api_);
TEST_F(CryptoUtilsTest, GetFingerprintFromPemInvalid) {
CertFingerprinterImpl fingerprinter;
auto fingerprint = fingerprinter.getFingerprintFromPem("invalid cert content");
EXPECT_FALSE(fingerprint.ok());
EXPECT_EQ(fingerprint.status().message(), "Failed to parse certificate");
EXPECT_EQ(fingerprint.status().message(), "Failed to parse certificate from PEM");
}

} // namespace
Expand Down
Loading