blob: 039ccf1a356e37ecacd296de9286824f85409e74 [file]
//
// Copyright 2025 gRPC authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include <grpc/grpc.h>
#include <grpc/grpc_security.h>
#include <grpc/support/alloc.h>
#include <grpc/support/time.h>
#include <grpcpp/channel.h>
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/audit_logging.h>
#include <grpcpp/security/tls_certificate_provider.h>
#include <grpcpp/server.h>
#include <grpcpp/server_builder.h>
#include <grpcpp/xds_server_builder.h>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/functional/function_ref.h"
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_join.h"
#include "absl/time/time.h"
#include "envoy/config/cluster/v3/cluster.pb.h"
#include "envoy/config/endpoint/v3/endpoint.pb.h"
#include "envoy/config/listener/v3/listener.pb.h"
#include "envoy/config/route/v3/route.pb.h"
#include "envoy/extensions/clusters/aggregate/v3/cluster.pb.h"
#include "envoy/extensions/filters/http/rbac/v3/rbac.pb.h"
#include "envoy/extensions/filters/http/router/v3/router.pb.h"
#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h"
#include "envoy/extensions/transport_sockets/tls/v3/tls.pb.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "src/core/config/config_vars.h"
#include "src/core/config/core_configuration.h"
#include "src/core/credentials/transport/fake/fake_credentials.h"
#include "src/core/credentials/transport/tls/certificate_provider_registry.h"
#include "src/core/credentials/transport/tls/grpc_tls_certificate_provider.h"
#include "src/core/ext/filters/http/client/http_client_filter.h"
#include "src/core/lib/security/authorization/audit_logging.h"
#include "src/core/util/env.h"
#include "src/core/util/ref_counted_ptr.h"
#include "src/core/util/string.h"
#include "src/core/util/sync.h"
#include "src/cpp/client/secure_credentials.h"
#include "src/cpp/server/secure_server_credentials.h"
#include "src/proto/grpc/testing/echo.grpc.pb.h"
#include "src/proto/grpc/testing/echo_messages.pb.h"
#include "test/core/test_util/audit_logging_utils.h"
#include "test/core/test_util/port.h"
#include "test/core/test_util/resolve_localhost_ip46.h"
#include "test/core/test_util/scoped_env_var.h"
#include "test/core/test_util/test_config.h"
#include "test/core/test_util/tls_utils.h"
#include "test/cpp/end2end/xds/xds_end2end_test_lib.h"
#include "test/cpp/util/test_config.h"
#include "test/cpp/util/tls_test_utils.h"
#include "xds/type/v3/typed_struct.pb.h"
namespace grpc {
namespace testing {
namespace {
using ::envoy::extensions::transport_sockets::tls::v3::UpstreamTlsContext;
using ::envoy::type::matcher::v3::StringMatcher;
constexpr char kClientKeyPath[] =
"test/core/tsi/test_creds/spiffe_end2end/client.key";
constexpr char kClientCertPath[] =
"test/core/tsi/test_creds/spiffe_end2end/client_spiffe.pem";
constexpr char kClientSpiffeBundleMapPath[] =
"test/core/tsi/test_creds/spiffe_end2end/client_spiffebundle.json";
constexpr char kServerSpiffeBundleMapPath[] =
"test/core/tsi/test_creds/spiffe_end2end/server_spiffebundle.json";
constexpr char kBadClientCertPath[] = "src/core/tsi/test_creds/badclient.pem";
constexpr char kBadClientKeyPath[] = "src/core/tsi/test_creds/badclient.key";
// Based on StaticDataCertificateProvider, but provides alternate certificates
// if the certificate name is not empty.
class FakeCertificateProvider final : public grpc_tls_certificate_provider {
public:
struct CertData {
std::string root_certificate;
grpc_core::PemKeyCertPairList identity_key_cert_pairs;
grpc_core::SpiffeBundleMap spiffe_bundle_map;
};
using CertDataMap = std::map<std::string /*cert_name */, CertData>;
class CertDataMapWrapper {
public:
CertDataMap Get() {
grpc_core::MutexLock lock(&mu_);
return cert_data_map_;
}
void Set(CertDataMap data) {
grpc_core::MutexLock lock(&mu_);
cert_data_map_ = std::move(data);
}
private:
grpc_core::Mutex mu_;
CertDataMap cert_data_map_ ABSL_GUARDED_BY(mu_);
};
explicit FakeCertificateProvider(CertDataMap cert_data_map)
: distributor_(
grpc_core::MakeRefCounted<grpc_tls_certificate_distributor>()),
cert_data_map_(std::move(cert_data_map)) {
distributor_->SetWatchStatusCallback([this](std::string cert_name,
bool root_being_watched,
bool identity_being_watched) {
if (!root_being_watched && !identity_being_watched) return;
auto it = cert_data_map_.find(cert_name);
if (it == cert_data_map_.end()) {
grpc_error_handle error = GRPC_ERROR_CREATE(absl::StrCat(
"No certificates available for cert_name \"", cert_name, "\""));
distributor_->SetErrorForCert(cert_name, error, error);
} else {
std::shared_ptr<RootCertInfo> root_cert_info;
std::optional<grpc_core::PemKeyCertPairList> pem_key_cert_pairs;
if (root_being_watched) {
if (it->second.spiffe_bundle_map.size() != 0) {
root_cert_info =
std::make_shared<RootCertInfo>(it->second.spiffe_bundle_map);
} else {
root_cert_info =
std::make_shared<RootCertInfo>(it->second.root_certificate);
}
}
if (identity_being_watched) {
pem_key_cert_pairs = it->second.identity_key_cert_pairs;
}
distributor_->SetKeyMaterials(cert_name, std::move(root_cert_info),
std::move(pem_key_cert_pairs));
}
});
}
~FakeCertificateProvider() override {
distributor_->SetWatchStatusCallback(nullptr);
}
grpc_core::RefCountedPtr<grpc_tls_certificate_distributor> distributor()
const override {
return distributor_;
}
grpc_core::UniqueTypeName type() const override {
static grpc_core::UniqueTypeName::Factory kFactory("fake");
return kFactory.Create();
}
private:
int CompareImpl(const grpc_tls_certificate_provider* other) const override {
// TODO(yashykt): Maybe do something better here.
return grpc_core::QsortCompare(
static_cast<const grpc_tls_certificate_provider*>(this), other);
}
grpc_core::RefCountedPtr<grpc_tls_certificate_distributor> distributor_;
CertDataMap cert_data_map_;
};
class FakeCertificateProviderFactory
: public grpc_core::CertificateProviderFactory {
public:
class Config : public grpc_core::CertificateProviderFactory::Config {
public:
explicit Config(absl::string_view name) : name_(name) {}
absl::string_view name() const override { return name_; }
std::string ToString() const override { return "{}"; }
private:
absl::string_view name_;
};
FakeCertificateProviderFactory(
absl::string_view name,
FakeCertificateProvider::CertDataMapWrapper* cert_data_map)
: name_(name), cert_data_map_(cert_data_map) {
CHECK_NE(cert_data_map, nullptr);
}
absl::string_view name() const override { return name_; }
grpc_core::RefCountedPtr<grpc_core::CertificateProviderFactory::Config>
CreateCertificateProviderConfig(
const grpc_core::Json& /*config_json*/,
const grpc_core::JsonArgs& /*args*/,
grpc_core::ValidationErrors* /*errors*/) override {
return grpc_core::MakeRefCounted<Config>(name_);
}
grpc_core::RefCountedPtr<grpc_tls_certificate_provider>
CreateCertificateProvider(
grpc_core::RefCountedPtr<grpc_core::CertificateProviderFactory::Config>
/*config*/) override {
CHECK_NE(cert_data_map_, nullptr);
return grpc_core::MakeRefCounted<FakeCertificateProvider>(
cert_data_map_->Get());
}
private:
absl::string_view name_;
FakeCertificateProvider::CertDataMapWrapper* cert_data_map_;
};
// Global variables for each provider.
FakeCertificateProvider::CertDataMapWrapper* g_fake1_cert_data_map = nullptr;
FakeCertificateProvider::CertDataMapWrapper* g_fake2_cert_data_map = nullptr;
//
// Client-side mTLS tests
//
class XdsSecurityTest : public XdsEnd2endTest {
protected:
void SetUp() override {
XdsBootstrapBuilder builder = MakeBootstrapBuilder();
builder.AddCertificateProviderPlugin("fake_plugin1", "fake1");
builder.AddCertificateProviderPlugin("fake_plugin2", "fake2");
std::vector<std::string> fields;
fields.push_back(absl::StrFormat(" \"certificate_file\": \"%s\"",
kClientCertPath));
fields.push_back(absl::StrFormat(" \"private_key_file\": \"%s\"",
kClientKeyPath));
fields.push_back(
absl::StrFormat(" \"spiffe_bundle_map_file\": \"%s\"",
kClientSpiffeBundleMapPath));
builder.AddCertificateProviderPlugin("file_plugin", "file_watcher",
absl::StrJoin(fields, ",\n"));
InitClient(builder, /*lb_expected_authority=*/"",
/*xds_resource_does_not_exist_timeout_ms=*/0,
/*balancer_authority_override=*/"", /*args=*/nullptr,
CreateSpiffeXdsChannelCredentials());
CreateAndStartBackends(2, /*xds_enabled=*/false,
CreateMtlsSpiffeServerCredentials());
root_cert_ = grpc_core::testing::GetFileContents(kSpiffeCaCertPath);
bad_root_cert_ = grpc_core::testing::GetFileContents(kBadClientCertPath);
identity_pair_ = ReadTlsIdentityPair(kClientKeyPath, kClientCertPath);
// TODO(yashykt): Use different client certs here instead of reusing
// server certs after https://github.com/grpc/grpc/pull/24876 is merged
fallback_identity_pair_ =
ReadTlsIdentityPair(kServerKeyPath, kServerCertPath);
bad_identity_pair_ =
ReadTlsIdentityPair(kBadClientKeyPath, kBadClientCertPath);
server_san_exact_.set_exact("*.test.google.fr");
server_san_prefix_.set_prefix("waterzooi.test.google");
server_san_suffix_.set_suffix("google.fr");
server_san_contains_.set_contains("google");
server_san_regex_.mutable_safe_regex()->mutable_google_re2();
server_san_regex_.mutable_safe_regex()->set_regex(
"(foo|waterzooi).test.google.(fr|be)");
bad_san_1_.set_exact("192.168.1.4");
bad_san_2_.set_exact("foo.test.google.in");
authenticated_identity_ = {
"spiffe://foo.bar.com/9eebccd2-12bf-40a6-b262-65fe0487d453"};
fallback_authenticated_identity_ = {"*.test.google.fr",
"waterzooi.test.google.be",
"*.test.youtube.com", "192.168.1.3"};
EdsResourceArgs args({
{"locality0", CreateEndpointsForBackends(0, 1)},
});
balancer_->ads_service()->SetEdsResource(BuildEdsResource(args));
}
void MaybeSetUpstreamTlsContextOnCluster(
absl::string_view root_instance_name,
absl::string_view root_certificate_name,
absl::string_view identity_instance_name,
absl::string_view identity_certificate_name,
const std::vector<StringMatcher>& san_matchers, Cluster* cluster) {
if (!identity_instance_name.empty() || !root_instance_name.empty()) {
auto* transport_socket = cluster->mutable_transport_socket();
transport_socket->set_name("envoy.transport_sockets.tls");
UpstreamTlsContext upstream_tls_context;
if (!identity_instance_name.empty()) {
upstream_tls_context.mutable_common_tls_context()
->mutable_tls_certificate_provider_instance()
->set_instance_name(std::string(identity_instance_name));
upstream_tls_context.mutable_common_tls_context()
->mutable_tls_certificate_provider_instance()
->set_certificate_name(std::string(identity_certificate_name));
}
if (!root_instance_name.empty()) {
upstream_tls_context.mutable_common_tls_context()
->mutable_validation_context()
->mutable_ca_certificate_provider_instance()
->set_instance_name(std::string(root_instance_name));
upstream_tls_context.mutable_common_tls_context()
->mutable_validation_context()
->mutable_ca_certificate_provider_instance()
->set_certificate_name(std::string(root_certificate_name));
}
if (!san_matchers.empty()) {
auto* validation_context =
upstream_tls_context.mutable_common_tls_context()
->mutable_validation_context();
for (const auto& san_matcher : san_matchers) {
*validation_context->add_match_subject_alt_names() = san_matcher;
}
}
transport_socket->mutable_typed_config()->PackFrom(upstream_tls_context);
}
}
// Sends CDS updates with the new security configuration and verifies that
// after propagation, this new configuration is used for connections. If \a
// identity_instance_name and \a root_instance_name are both empty,
// connections are expected to use fallback credentials.
// TODO(yashykt): The core of this logic should be inlined into the
// individual tests instead of being in this helper function.
void UpdateAndVerifyXdsSecurityConfiguration(
absl::string_view root_instance_name,
absl::string_view root_certificate_name,
absl::string_view identity_instance_name,
absl::string_view identity_certificate_name,
const std::vector<StringMatcher>& san_matchers,
const std::vector<std::string>& expected_authenticated_identity,
bool test_expects_failure = false) {
// Change the backend and use a unique service name to use so that we know
// that the CDS update was applied.
std::string service_name = absl::StrCat(
"eds_service_name",
absl::FormatTime("%H%M%E3S", absl::Now(), absl::LocalTimeZone()));
backend_index_ = (backend_index_ + 1) % 2;
EdsResourceArgs args({
{"locality0",
CreateEndpointsForBackends(backend_index_, backend_index_ + 1)},
});
balancer_->ads_service()->SetEdsResource(
BuildEdsResource(args, service_name.c_str()));
auto cluster = default_cluster_;
cluster.mutable_eds_cluster_config()->set_service_name(service_name);
MaybeSetUpstreamTlsContextOnCluster(
root_instance_name, root_certificate_name, identity_instance_name,
identity_certificate_name, san_matchers, &cluster);
balancer_->ads_service()->SetCdsResource(cluster);
// The updates might take time to have an effect, so use a retry loop.
if (test_expects_failure) {
SendRpcsUntilFailure(DEBUG_LOCATION, StatusCode::UNAVAILABLE,
// TODO(yashkt): Change individual test cases to
// expect the exact error message here.
".*", /*timeout_ms=*/20 * 1000,
RpcOptions().set_timeout_ms(5000));
} else {
backends_[backend_index_]->backend_service()->ResetCounters();
SendRpcsUntil(
DEBUG_LOCATION,
[&](const RpcResult& result) {
// Make sure that we are hitting the correct backend.
// TODO(yashykt): Even if we haven't moved to the correct backend
// and are still using the previous update, we should still check
// for the status and make sure that it fits our expectations.
if (backends_[backend_index_]->backend_service()->request_count() ==
0) {
return true;
}
EXPECT_TRUE(result.status.ok())
<< "code=" << result.status.error_code()
<< " message=" << result.status.error_message();
// Check that the identity is as expected.
EXPECT_EQ(backends_[backend_index_]
->backend_service()
->last_peer_identity(),
expected_authenticated_identity);
return false;
},
/* timeout_ms= */ 20 * 1000, RpcOptions().set_timeout_ms(5000));
}
}
std::string root_cert_;
std::string bad_root_cert_;
grpc_core::PemKeyCertPairList identity_pair_;
grpc_core::PemKeyCertPairList fallback_identity_pair_;
grpc_core::PemKeyCertPairList bad_identity_pair_;
grpc_core::SpiffeBundleMap spiffe_bundle_map_;
StringMatcher server_san_exact_;
StringMatcher server_san_prefix_;
StringMatcher server_san_suffix_;
StringMatcher server_san_contains_;
StringMatcher server_san_regex_;
StringMatcher bad_san_1_;
StringMatcher bad_san_2_;
std::vector<std::string> authenticated_identity_;
std::vector<std::string> fallback_authenticated_identity_;
int backend_index_ = 0;
};
INSTANTIATE_TEST_SUITE_P(XdsTest, XdsSecurityTest,
::testing::Values(XdsTestType()), &XdsTestType::Name);
TEST_P(XdsSecurityTest, TestMtlsConfigurationWithRootPluginUpdateSpiffe) {
auto map = grpc_core::SpiffeBundleMap::FromFile(kClientSpiffeBundleMapPath);
ASSERT_TRUE(map.ok());
auto bad_map =
grpc_core::SpiffeBundleMap::FromFile(kServerSpiffeBundleMapPath);
ASSERT_TRUE(bad_map.ok());
g_fake1_cert_data_map->Set({{"", {"", identity_pair_, *map}}});
g_fake2_cert_data_map->Set({{"", {"", bad_identity_pair_, *bad_map}}});
UpdateAndVerifyXdsSecurityConfiguration("fake_plugin1", "", "fake_plugin1",
"", {server_san_exact_},
authenticated_identity_);
UpdateAndVerifyXdsSecurityConfiguration("fake_plugin2" /* bad root */, "",
"fake_plugin1", "", {}, {},
true /* failure */);
UpdateAndVerifyXdsSecurityConfiguration("fake_plugin1", "", "fake_plugin1",
"", {server_san_exact_},
authenticated_identity_);
}
} // namespace
} // namespace testing
} // namespace grpc
int main(int argc, char** argv) {
grpc::testing::TestEnvironment env(&argc, argv);
::testing::InitGoogleTest(&argc, argv);
// Make the backup poller poll very frequently in order to pick up
// updates from all the subchannels's FDs.
grpc_core::ConfigVars::Overrides overrides;
overrides.client_channel_backup_poll_interval_ms = 1;
overrides.trace =
"call,channel,client_channel,client_channel_call,client_channel_lb_call,"
"handshaker";
grpc_core::ConfigVars::SetOverrides(overrides);
#if TARGET_OS_IPHONE
// Workaround Apple CFStream bug
grpc_core::SetEnv("grpc_cfstream", "0");
#endif
grpc::testing::FakeCertificateProvider::CertDataMapWrapper cert_data_map_1;
grpc::testing::g_fake1_cert_data_map = &cert_data_map_1;
grpc::testing::FakeCertificateProvider::CertDataMapWrapper cert_data_map_2;
grpc::testing::g_fake2_cert_data_map = &cert_data_map_2;
grpc_core::CoreConfiguration::RegisterEphemeralBuilder(
[](grpc_core::CoreConfiguration::Builder* builder) {
builder->certificate_provider_registry()
->RegisterCertificateProviderFactory(
std::make_unique<grpc::testing::FakeCertificateProviderFactory>(
"fake1", grpc::testing::g_fake1_cert_data_map));
builder->certificate_provider_registry()
->RegisterCertificateProviderFactory(
std::make_unique<grpc::testing::FakeCertificateProviderFactory>(
"fake2", grpc::testing::g_fake2_cert_data_map));
});
grpc_init();
const auto result = RUN_ALL_TESTS();
grpc_shutdown();
return result;
}