#include #include "envoy/common/exception.h" #include "envoy/config/core/v3/config_source.pb.h" #include "envoy/extensions/transport_sockets/tls/v3/cert.pb.h" #include "envoy/service/discovery/v3/discovery.pb.h" #include "envoy/service/secret/v3/sds.pb.h" #include "common/config/datasource.h" #include "common/config/filesystem_subscription_impl.h" #include "common/secret/sds_api.h" #include "common/ssl/certificate_validation_context_config_impl.h" #include "common/ssl/tls_certificate_config_impl.h" #include "test/common/stats/stat_test_utility.h" #include "test/mocks/config/mocks.h" #include "test/mocks/event/mocks.h" #include "test/mocks/filesystem/mocks.h" #include "test/mocks/grpc/mocks.h" #include "test/mocks/init/mocks.h" #include "test/mocks/protobuf/mocks.h" #include "test/mocks/secret/mocks.h" #include "test/test_common/environment.h" #include "test/test_common/logging.h" #include "test/test_common/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" using ::testing::_; using ::testing::InSequence; using ::testing::Invoke; using ::testing::InvokeWithoutArgs; using ::testing::NiceMock; using ::testing::Return; using ::testing::Throw; namespace Envoy { namespace Secret { namespace { class SdsApiTestBase { protected: SdsApiTestBase() { api_ = Api::createApiForTest(); dispatcher_ = api_->allocateDispatcher("test_thread"); } void initialize() { init_target_handle_->initialize(init_watcher_); } void setupMocks() { EXPECT_CALL(init_manager_, add(_)).WillOnce(Invoke([this](const Init::Target& target) { init_target_handle_ = target.createHandle("test"); })); } Api::ApiPtr api_; NiceMock validation_visitor_; NiceMock subscription_factory_; NiceMock init_manager_; NiceMock init_watcher_; Event::GlobalTimeSystem time_system_; Init::TargetHandlePtr init_target_handle_; Event::DispatcherPtr dispatcher_; Stats::TestUtil::TestStore stats_; }; class SdsApiTest : public testing::Test, public SdsApiTestBase {}; // Validate that SdsApi object is created and initialized successfully. TEST_F(SdsApiTest, BasicTest) { ::testing::InSequence s; const envoy::service::secret::v3::SdsDummy dummy; envoy::config::core::v3::ConfigSource config_source; setupMocks(); TlsCertificateSdsApi sds_api( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); sds_api.registerInitTarget(init_manager_); initialize(); } // Validate that a noop init manager is used if the InitManger passed into the constructor // has been already initialized. This is a regression test for // https://github.com/envoyproxy/envoy/issues/12013 TEST_F(SdsApiTest, InitManagerInitialised) { std::string sds_config = R"EOF( resources: - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret" name: "abc.com" tls_certificate: certificate_chain: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_cert.pem" private_key: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_key.pem" )EOF"; const std::string sds_config_path = TestEnvironment::writeStringToFileForTest( "sds.yaml", TestEnvironment::substitute(sds_config), false); NiceMock callbacks; TestUtility::TestOpaqueResourceDecoderImpl resource_decoder("name"); Config::SubscriptionStats stats(Config::Utility::generateStats(stats_)); NiceMock validation_visitor; envoy::config::core::v3::ConfigSource config_source; EXPECT_CALL(subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _)) .WillOnce(Invoke([this, &sds_config_path, &resource_decoder, &stats](const envoy::config::core::v3::ConfigSource&, absl::string_view, Stats::Scope&, Config::SubscriptionCallbacks& cbs, Config::OpaqueResourceDecoder&) -> Config::SubscriptionPtr { return std::make_unique(*dispatcher_, sds_config_path, cbs, resource_decoder, stats, validation_visitor_, *api_); })); auto init_manager = Init::ManagerImpl("testing"); auto noop_init_target = Init::TargetImpl(fmt::format("noop test init target"), [] { /*Do nothing.*/ }); init_manager.add(noop_init_target); auto noop_watcher = Init::WatcherImpl(fmt::format("noop watcher"), []() { /*Do nothing.*/ }); init_manager.initialize(noop_watcher); EXPECT_EQ(Init::Manager::State::Initializing, init_manager.state()); TlsCertificateSdsApi sds_api( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); EXPECT_NO_THROW(sds_api.registerInitTarget(init_manager)); } // Validate that bad ConfigSources are caught at construction time. This is a regression test for // https://github.com/envoyproxy/envoy/issues/10976. TEST_F(SdsApiTest, BadConfigSource) { ::testing::InSequence s; envoy::config::core::v3::ConfigSource config_source; EXPECT_CALL(subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _)) .WillOnce(InvokeWithoutArgs([]() -> Config::SubscriptionPtr { throw EnvoyException("bad config"); return nullptr; })); EXPECT_THROW_WITH_MESSAGE(TlsCertificateSdsApi( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_), EnvoyException, "bad config"); } // Validate that TlsCertificateSdsApi updates secrets successfully if a good secret // is passed to onConfigUpdate(). TEST_F(SdsApiTest, DynamicTlsCertificateUpdateSuccess) { envoy::config::core::v3::ConfigSource config_source; setupMocks(); TlsCertificateSdsApi sds_api( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); sds_api.registerInitTarget(init_manager_); initialize(); NiceMock secret_callback; auto handle = sds_api.addUpdateCallback([&secret_callback]() { secret_callback.onAddOrUpdateSecret(); }); std::string yaml = R"EOF( name: "abc.com" tls_certificate: certificate_chain: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_cert.pem" private_key: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_key.pem" )EOF"; envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; TestUtility::loadFromYaml(TestEnvironment::substitute(yaml), typed_secret); const auto decoded_resources = TestUtility::decodeResources({typed_secret}); EXPECT_CALL(secret_callback, onAddOrUpdateSecret()); subscription_factory_.callbacks_->onConfigUpdate(decoded_resources.refvec_, ""); Ssl::TlsCertificateConfigImpl tls_config(*sds_api.secret(), nullptr, *api_); const std::string cert_pem = "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_cert.pem"; EXPECT_EQ(TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(cert_pem)), tls_config.certificateChain()); const std::string key_pem = "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_key.pem"; EXPECT_EQ(TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(key_pem)), tls_config.privateKey()); handle->remove(); } class SdsRotationApiTest : public SdsApiTestBase { protected: SdsRotationApiTest() { api_ = Api::createApiForTest(filesystem_); setupMocks(); EXPECT_CALL(filesystem_, splitPathFromFilename(_)) .WillRepeatedly(Invoke([](absl::string_view path) -> Filesystem::PathSplitResult { return Filesystem::fileSystemForTest().splitPathFromFilename(path); })); } Secret::MockSecretCallbacks secret_callback_; Common::CallbackHandle* handle_{}; std::vector watch_cbs_; Event::MockDispatcher mock_dispatcher_; Filesystem::MockInstance filesystem_; }; class TlsCertificateSdsRotationApiTest : public testing::TestWithParam, public SdsRotationApiTest { protected: TlsCertificateSdsRotationApiTest() : watched_directory_(GetParam()), cert_path_("/foo/bar/cert.pem"), key_path_("/foo/bar/key.pem"), expected_watch_path_("/foo/bar/"), trigger_path_("/foo") { envoy::config::core::v3::ConfigSource config_source; sds_api_ = std::make_unique( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, mock_dispatcher_, *api_); sds_api_->registerInitTarget(init_manager_); initialize(); handle_ = sds_api_->addUpdateCallback([this]() { secret_callback_.onAddOrUpdateSecret(); }); } ~TlsCertificateSdsRotationApiTest() override { handle_->remove(); } void onConfigUpdate(const std::string& cert_value, const std::string& key_value) { const std::string yaml = fmt::format( R"EOF( name: "abc.com" tls_certificate: certificate_chain: filename: "{}" private_key: filename: "{}" )EOF", cert_path_, key_path_); envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; TestUtility::loadFromYaml(yaml, typed_secret); if (watched_directory_) { typed_secret.mutable_tls_certificate()->mutable_watched_directory()->set_path(trigger_path_); } const auto decoded_resources = TestUtility::decodeResources({typed_secret}); auto* watcher = new Filesystem::MockWatcher(); if (watched_directory_) { EXPECT_CALL(mock_dispatcher_, createFilesystemWatcher_()).WillOnce(Return(watcher)); EXPECT_CALL(*watcher, addWatch(trigger_path_ + "/", Filesystem::Watcher::Events::MovedTo, _)) .WillOnce( Invoke([this](absl::string_view, uint32_t, Filesystem::Watcher::OnChangedCb cb) { watch_cbs_.push_back(cb); })); EXPECT_CALL(filesystem_, fileReadToEnd(cert_path_)).WillOnce(Return(cert_value)); EXPECT_CALL(filesystem_, fileReadToEnd(key_path_)).WillOnce(Return(key_value)); EXPECT_CALL(secret_callback_, onAddOrUpdateSecret()); } else { EXPECT_CALL(filesystem_, fileReadToEnd(cert_path_)).WillOnce(Return(cert_value)); EXPECT_CALL(filesystem_, fileReadToEnd(key_path_)).WillOnce(Return(key_value)); EXPECT_CALL(secret_callback_, onAddOrUpdateSecret()); EXPECT_CALL(mock_dispatcher_, createFilesystemWatcher_()).WillOnce(Return(watcher)); EXPECT_CALL(*watcher, addWatch(expected_watch_path_, Filesystem::Watcher::Events::MovedTo, _)) .Times(2) .WillRepeatedly( Invoke([this](absl::string_view, uint32_t, Filesystem::Watcher::OnChangedCb cb) { watch_cbs_.push_back(cb); })); } subscription_factory_.callbacks_->onConfigUpdate(decoded_resources.refvec_, ""); } const bool watched_directory_; std::string cert_path_; std::string key_path_; std::string expected_watch_path_; std::string trigger_path_; std::unique_ptr sds_api_; }; INSTANTIATE_TEST_SUITE_P(TlsCertificateSdsRotationApiTestParams, TlsCertificateSdsRotationApiTest, testing::Values(false, true)); class CertificateValidationContextSdsRotationApiTest : public testing::TestWithParam, public SdsRotationApiTest { protected: CertificateValidationContextSdsRotationApiTest() { envoy::config::core::v3::ConfigSource config_source; sds_api_ = std::make_unique( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, mock_dispatcher_, *api_); sds_api_->registerInitTarget(init_manager_); initialize(); handle_ = sds_api_->addUpdateCallback([this]() { secret_callback_.onAddOrUpdateSecret(); }); } ~CertificateValidationContextSdsRotationApiTest() override { handle_->remove(); } void onConfigUpdate(const std::string& trusted_ca_path, const std::string& trusted_ca_value, const std::string& watch_path) { const std::string yaml = fmt::format( R"EOF( name: "abc.com" validation_context: trusted_ca: filename: "{}" allow_expired_certificate: true )EOF", trusted_ca_path); envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; TestUtility::loadFromYaml(yaml, typed_secret); const auto decoded_resources = TestUtility::decodeResources({typed_secret}); auto* watcher = new Filesystem::MockWatcher(); EXPECT_CALL(filesystem_, fileReadToEnd(trusted_ca_path)).WillOnce(Return(trusted_ca_value)); EXPECT_CALL(secret_callback_, onAddOrUpdateSecret()); EXPECT_CALL(mock_dispatcher_, createFilesystemWatcher_()).WillOnce(Return(watcher)); EXPECT_CALL(*watcher, addWatch(watch_path, Filesystem::Watcher::Events::MovedTo, _)) .WillOnce(Invoke([this](absl::string_view, uint32_t, Filesystem::Watcher::OnChangedCb cb) { watch_cbs_.push_back(cb); })); subscription_factory_.callbacks_->onConfigUpdate(decoded_resources.refvec_, ""); } std::unique_ptr sds_api_; }; INSTANTIATE_TEST_SUITE_P(CertificateValidationContextSdsRotationApiTestParams, CertificateValidationContextSdsRotationApiTest, testing::Values(false, true)); // Initial onConfigUpdate() of TlsCertificate secret. TEST_P(TlsCertificateSdsRotationApiTest, InitialUpdate) { InSequence s; onConfigUpdate("a", "b"); const auto& secret = *sds_api_->secret(); EXPECT_EQ("a", secret.certificate_chain().inline_bytes()); EXPECT_EQ("b", secret.private_key().inline_bytes()); } // Two distinct updates with onConfigUpdate() of TlsCertificate secret. TEST_P(TlsCertificateSdsRotationApiTest, MultiUpdate) { InSequence s; onConfigUpdate("a", "b"); { const auto& secret = *sds_api_->secret(); EXPECT_EQ("a", secret.certificate_chain().inline_bytes()); EXPECT_EQ("b", secret.private_key().inline_bytes()); } cert_path_ = "/new/foo/bar/cert.pem"; key_path_ = "/new/foo/bar/key.pem"; expected_watch_path_ = "/new/foo/bar/"; onConfigUpdate("c", "d"); { const auto& secret = *sds_api_->secret(); EXPECT_EQ("c", secret.certificate_chain().inline_bytes()); EXPECT_EQ("d", secret.private_key().inline_bytes()); } } // Watch trigger without file change has no effect. TEST_P(TlsCertificateSdsRotationApiTest, NopWatchTrigger) { InSequence s; onConfigUpdate("a", "b"); for (const auto& cb : watch_cbs_) { EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("a")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("b")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("a")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("b")); cb(Filesystem::Watcher::Events::MovedTo); } const auto& secret = *sds_api_->secret(); EXPECT_EQ("a", secret.certificate_chain().inline_bytes()); EXPECT_EQ("b", secret.private_key().inline_bytes()); } // Basic rotation of TlsCertificate. TEST_P(TlsCertificateSdsRotationApiTest, RotationWatchTrigger) { InSequence s; onConfigUpdate("a", "b"); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(secret_callback_, onAddOrUpdateSecret()); watch_cbs_[0](Filesystem::Watcher::Events::MovedTo); if (!watched_directory_) { EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); watch_cbs_[1](Filesystem::Watcher::Events::MovedTo); } const auto& secret = *sds_api_->secret(); EXPECT_EQ("c", secret.certificate_chain().inline_bytes()); EXPECT_EQ("d", secret.private_key().inline_bytes()); } // Failed rotation of TlsCertificate. TEST_P(TlsCertificateSdsRotationApiTest, FailedRotation) { InSequence s; onConfigUpdate("a", "b"); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")) .WillOnce(Throw(EnvoyException("fail"))); EXPECT_LOG_CONTAINS("warn", "Failed to reload certificates: ", watch_cbs_[0](Filesystem::Watcher::Events::MovedTo)); EXPECT_EQ(1U, stats_.counter("sds.abc.com.key_rotation_failed").value()); const auto& secret = *sds_api_->secret(); EXPECT_EQ("a", secret.certificate_chain().inline_bytes()); EXPECT_EQ("b", secret.private_key().inline_bytes()); } // Basic rotation of CertificateValidationContext. TEST_P(CertificateValidationContextSdsRotationApiTest, CertificateValidationContext) { InSequence s; onConfigUpdate("/foo/bar/ca.pem", "a", "/foo/bar/"); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/ca.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/ca.pem")).WillOnce(Return("c")); EXPECT_CALL(secret_callback_, onAddOrUpdateSecret()); watch_cbs_[0](Filesystem::Watcher::Events::MovedTo); const auto& secret = *sds_api_->secret(); EXPECT_EQ("c", secret.trusted_ca().inline_bytes()); } // Hash consistency verification prevents races. TEST_P(TlsCertificateSdsRotationApiTest, RotationConsistency) { InSequence s; onConfigUpdate("a", "b"); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("a")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(secret_callback_, onAddOrUpdateSecret()); watch_cbs_[0](Filesystem::Watcher::Events::MovedTo); if (!watched_directory_) { EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); watch_cbs_[1](Filesystem::Watcher::Events::MovedTo); } const auto& secret = *sds_api_->secret(); EXPECT_EQ("c", secret.certificate_chain().inline_bytes()); EXPECT_EQ("d", secret.private_key().inline_bytes()); } // Hash consistency verification failure, no callback. TEST_P(TlsCertificateSdsRotationApiTest, RotationConsistencyExhaustion) { InSequence s; onConfigUpdate("a", "b"); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("a")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("c")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("e")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("f")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("d")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("f")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("g")); // We've exhausted the bounded retries, but continue with the non-atomic rotation. EXPECT_CALL(secret_callback_, onAddOrUpdateSecret()); EXPECT_LOG_CONTAINS( "warn", "Unable to atomically refresh secrets due to > 5 non-atomic rotations observed", watch_cbs_[0](Filesystem::Watcher::Events::MovedTo)); if (!watched_directory_) { EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("f")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("g")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/cert.pem")).WillOnce(Return("f")); EXPECT_CALL(filesystem_, fileReadToEnd("/foo/bar/key.pem")).WillOnce(Return("g")); watch_cbs_[1](Filesystem::Watcher::Events::MovedTo); } const auto& secret = *sds_api_->secret(); EXPECT_EQ("f", secret.certificate_chain().inline_bytes()); EXPECT_EQ("g", secret.private_key().inline_bytes()); } class PartialMockSds : public SdsApi { public: PartialMockSds(Stats::Store& stats, NiceMock& init_manager, envoy::config::core::v3::ConfigSource& config_source, Config::SubscriptionFactory& subscription_factory, TimeSource& time_source, Event::Dispatcher& dispatcher, Api::Api& api) : SdsApi( config_source, "abc.com", subscription_factory, time_source, validation_visitor_, stats, []() {}, dispatcher, api) { registerInitTarget(init_manager); } MOCK_METHOD(void, onConfigUpdate, (const std::vector&, const std::string&)); void onConfigUpdate(const std::vector& added, const Protobuf::RepeatedPtrField& removed, const std::string& version) override { SdsApi::onConfigUpdate(added, removed, version); } void setSecret(const envoy::extensions::transport_sockets::tls::v3::Secret&) override {} void validateConfig(const envoy::extensions::transport_sockets::tls::v3::Secret&) override {} std::vector getDataSourceFilenames() override { return {}; } Config::WatchedDirectory* getWatchedDirectory() override { return nullptr; } NiceMock validation_visitor_; }; // Basic test of delta's passthrough call to the state-of-the-world variant, to // increase coverage. TEST_F(SdsApiTest, Delta) { auto secret = std::make_unique(); secret->set_name("secret_1"); Config::DecodedResourceImpl resource(std::move(secret), "name", {}, "version1"); std::vector resources{resource}; envoy::config::core::v3::ConfigSource config_source; Event::GlobalTimeSystem time_system; setupMocks(); PartialMockSds sds(stats_, init_manager_, config_source, subscription_factory_, time_system, *dispatcher_, *api_); initialize(); EXPECT_CALL(sds, onConfigUpdate(DecodedResourcesEq(resources), "version1")); subscription_factory_.callbacks_->onConfigUpdate(resources, {}, "ignored"); // An attempt to remove a resource logs an error, but otherwise just carries on (ignoring the // removal attempt). auto secret_again = std::make_unique(); secret_again->set_name("secret_1"); Config::DecodedResourceImpl resource_v2(std::move(secret_again), "name", {}, "version2"); std::vector resources_v2{resource_v2}; EXPECT_CALL(sds, onConfigUpdate(DecodedResourcesEq(resources_v2), "version2")); Protobuf::RepeatedPtrField removals; *removals.Add() = "route_0"; subscription_factory_.callbacks_->onConfigUpdate(resources_v2, removals, "ignored"); } // Tests SDS's use of the delta variant of onConfigUpdate(). TEST_F(SdsApiTest, DeltaUpdateSuccess) { envoy::config::core::v3::ConfigSource config_source; setupMocks(); TlsCertificateSdsApi sds_api( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); sds_api.registerInitTarget(init_manager_); NiceMock secret_callback; auto handle = sds_api.addUpdateCallback([&secret_callback]() { secret_callback.onAddOrUpdateSecret(); }); std::string yaml = R"EOF( name: "abc.com" tls_certificate: certificate_chain: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_cert.pem" private_key: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_key.pem" )EOF"; envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; TestUtility::loadFromYaml(TestEnvironment::substitute(yaml), typed_secret); const auto decoded_resources = TestUtility::decodeResources({typed_secret}); EXPECT_CALL(secret_callback, onAddOrUpdateSecret()); initialize(); subscription_factory_.callbacks_->onConfigUpdate(decoded_resources.refvec_, {}, ""); Ssl::TlsCertificateConfigImpl tls_config(*sds_api.secret(), nullptr, *api_); const std::string cert_pem = "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_cert.pem"; EXPECT_EQ(TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(cert_pem)), tls_config.certificateChain()); const std::string key_pem = "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_key.pem"; EXPECT_EQ(TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(key_pem)), tls_config.privateKey()); handle->remove(); } // Validate that CertificateValidationContextSdsApi updates secrets successfully if // a good secret is passed to onConfigUpdate(). TEST_F(SdsApiTest, DynamicCertificateValidationContextUpdateSuccess) { envoy::config::core::v3::ConfigSource config_source; setupMocks(); CertificateValidationContextSdsApi sds_api( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); sds_api.registerInitTarget(init_manager_); NiceMock secret_callback; auto handle = sds_api.addUpdateCallback([&secret_callback]() { secret_callback.onAddOrUpdateSecret(); }); std::string yaml = R"EOF( name: "abc.com" validation_context: trusted_ca: { filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/ca_cert.pem" } allow_expired_certificate: true )EOF"; envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; TestUtility::loadFromYaml(TestEnvironment::substitute(yaml), typed_secret); const auto decoded_resources = TestUtility::decodeResources({typed_secret}); EXPECT_CALL(secret_callback, onAddOrUpdateSecret()); initialize(); subscription_factory_.callbacks_->onConfigUpdate(decoded_resources.refvec_, ""); Ssl::CertificateValidationContextConfigImpl cvc_config(*sds_api.secret(), *api_); const std::string ca_cert = "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/ca_cert.pem"; EXPECT_EQ(TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(ca_cert)), cvc_config.caCert()); handle->remove(); } class CvcValidationCallback { public: virtual ~CvcValidationCallback() = default; virtual void validateCvc( const envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext&) PURE; }; class MockCvcValidationCallback : public CvcValidationCallback { public: MockCvcValidationCallback() = default; ~MockCvcValidationCallback() override = default; MOCK_METHOD(void, validateCvc, (const envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext&)); }; // Validate that CertificateValidationContextSdsApi updates secrets successfully if // a good secret is passed to onConfigUpdate(), and that merged CertificateValidationContext // provides correct information. TEST_F(SdsApiTest, DefaultCertificateValidationContextTest) { envoy::config::core::v3::ConfigSource config_source; setupMocks(); CertificateValidationContextSdsApi sds_api( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); sds_api.registerInitTarget(init_manager_); NiceMock secret_callback; auto handle = sds_api.addUpdateCallback([&secret_callback]() { secret_callback.onAddOrUpdateSecret(); }); NiceMock validation_callback; auto validation_handle = sds_api.addValidationCallback( [&validation_callback]( const envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext& cvc) { validation_callback.validateCvc(cvc); }); envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; typed_secret.set_name("abc.com"); auto* dynamic_cvc = typed_secret.mutable_validation_context(); dynamic_cvc->set_allow_expired_certificate(false); dynamic_cvc->mutable_trusted_ca()->set_filename(TestEnvironment::substitute( "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/ca_cert.pem")); dynamic_cvc->add_match_subject_alt_names()->set_exact("second san"); const std::string dynamic_verify_certificate_spki = "QGJRPdmx/r5EGOFLb2MTiZp2isyC0Whht7iazhzXaCM="; dynamic_cvc->add_verify_certificate_spki(dynamic_verify_certificate_spki); EXPECT_CALL(secret_callback, onAddOrUpdateSecret()); EXPECT_CALL(validation_callback, validateCvc(_)); const auto decoded_resources = TestUtility::decodeResources({typed_secret}); initialize(); subscription_factory_.callbacks_->onConfigUpdate(decoded_resources.refvec_, ""); const std::string default_verify_certificate_hash = "0000000000000000000000000000000000000000000000000000000000000000"; envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext default_cvc; default_cvc.set_allow_expired_certificate(true); default_cvc.mutable_trusted_ca()->set_inline_bytes("fake trusted ca"); default_cvc.add_match_subject_alt_names()->set_exact("first san"); default_cvc.add_verify_certificate_hash(default_verify_certificate_hash); envoy::extensions::transport_sockets::tls::v3::CertificateValidationContext merged_cvc = default_cvc; merged_cvc.MergeFrom(*sds_api.secret()); Ssl::CertificateValidationContextConfigImpl cvc_config(merged_cvc, *api_); // Verify that merging CertificateValidationContext applies logical OR to bool // field. EXPECT_TRUE(cvc_config.allowExpiredCertificate()); // Verify that singular fields are overwritten. const std::string ca_cert = "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/ca_cert.pem"; EXPECT_EQ(TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(ca_cert)), cvc_config.caCert()); // Verify that repeated fields are concatenated. EXPECT_EQ(2, cvc_config.subjectAltNameMatchers().size()); EXPECT_EQ("first san", cvc_config.subjectAltNameMatchers()[0].exact()); EXPECT_EQ("second san", cvc_config.subjectAltNameMatchers()[1].exact()); // Verify that if dynamic CertificateValidationContext does not set certificate hash list, the new // secret contains hash list from default CertificateValidationContext. EXPECT_EQ(1, cvc_config.verifyCertificateHashList().size()); EXPECT_EQ(default_verify_certificate_hash, cvc_config.verifyCertificateHashList()[0]); // Verify that if default CertificateValidationContext does not set certificate SPKI list, the new // secret contains SPKI list from dynamic CertificateValidationContext. EXPECT_EQ(1, cvc_config.verifyCertificateSpkiList().size()); EXPECT_EQ(dynamic_verify_certificate_spki, cvc_config.verifyCertificateSpkiList()[0]); handle->remove(); validation_handle->remove(); } class GenericSecretValidationCallback { public: virtual ~GenericSecretValidationCallback() = default; virtual void validateGenericSecret(const envoy::extensions::transport_sockets::tls::v3::GenericSecret&) PURE; }; class MockGenericSecretValidationCallback : public GenericSecretValidationCallback { public: MockGenericSecretValidationCallback() = default; ~MockGenericSecretValidationCallback() override = default; MOCK_METHOD(void, validateGenericSecret, (const envoy::extensions::transport_sockets::tls::v3::GenericSecret&)); }; // Validate that GenericSecretSdsApi updates secrets successfully if // a good secret is passed to onConfigUpdate(). TEST_F(SdsApiTest, GenericSecretSdsApiTest) { envoy::config::core::v3::ConfigSource config_source; setupMocks(); GenericSecretSdsApi sds_api( config_source, "encryption_key", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); sds_api.registerInitTarget(init_manager_); NiceMock secret_callback; auto handle = sds_api.addUpdateCallback([&secret_callback]() { secret_callback.onAddOrUpdateSecret(); }); NiceMock validation_callback; auto validation_handle = sds_api.addValidationCallback( [&validation_callback]( const envoy::extensions::transport_sockets::tls::v3::GenericSecret& secret) { validation_callback.validateGenericSecret(secret); }); std::string yaml = R"EOF( name: "encryption_key" generic_secret: secret: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/aes_128_key" )EOF"; envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; TestUtility::loadFromYaml(TestEnvironment::substitute(yaml), typed_secret); const auto decoded_resources = TestUtility::decodeResources({typed_secret}); EXPECT_CALL(secret_callback, onAddOrUpdateSecret()); EXPECT_CALL(validation_callback, validateGenericSecret(_)); initialize(); subscription_factory_.callbacks_->onConfigUpdate(decoded_resources.refvec_, ""); const envoy::extensions::transport_sockets::tls::v3::GenericSecret generic_secret( *sds_api.secret()); const std::string secret_path = "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/aes_128_key"; EXPECT_EQ(TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(secret_path)), Config::DataSource::read(generic_secret.secret(), true, *api_)); handle->remove(); validation_handle->remove(); } // Validate that SdsApi throws exception if an empty secret is passed to onConfigUpdate(). TEST_F(SdsApiTest, EmptyResource) { envoy::config::core::v3::ConfigSource config_source; setupMocks(); TlsCertificateSdsApi sds_api( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); sds_api.registerInitTarget(init_manager_); initialize(); EXPECT_THROW_WITH_MESSAGE(subscription_factory_.callbacks_->onConfigUpdate({}, ""), EnvoyException, "Missing SDS resources for abc.com in onConfigUpdate()"); } // Validate that SdsApi throws exception if multiple secrets are passed to onConfigUpdate(). TEST_F(SdsApiTest, SecretUpdateWrongSize) { envoy::config::core::v3::ConfigSource config_source; setupMocks(); TlsCertificateSdsApi sds_api( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); sds_api.registerInitTarget(init_manager_); std::string yaml = R"EOF( name: "abc.com" tls_certificate: certificate_chain: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_cert.pem" private_key: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_key.pem" )EOF"; envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; TestUtility::loadFromYaml(TestEnvironment::substitute(yaml), typed_secret); const auto decoded_resources = TestUtility::decodeResources({typed_secret, typed_secret}); initialize(); EXPECT_THROW_WITH_MESSAGE( subscription_factory_.callbacks_->onConfigUpdate(decoded_resources.refvec_, ""), EnvoyException, "Unexpected SDS secrets length: 2"); } // Validate that SdsApi throws exception if secret name passed to onConfigUpdate() // does not match configured name. TEST_F(SdsApiTest, SecretUpdateWrongSecretName) { envoy::config::core::v3::ConfigSource config_source; setupMocks(); TlsCertificateSdsApi sds_api( config_source, "abc.com", subscription_factory_, time_system_, validation_visitor_, stats_, []() {}, *dispatcher_, *api_); sds_api.registerInitTarget(init_manager_); std::string yaml = R"EOF( name: "wrong.name.com" tls_certificate: certificate_chain: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_cert.pem" private_key: filename: "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/selfsigned_key.pem" )EOF"; envoy::extensions::transport_sockets::tls::v3::Secret typed_secret; TestUtility::loadFromYaml(TestEnvironment::substitute(yaml), typed_secret); const auto decoded_resources = TestUtility::decodeResources({typed_secret}); initialize(); EXPECT_THROW_WITH_MESSAGE( subscription_factory_.callbacks_->onConfigUpdate(decoded_resources.refvec_, ""), EnvoyException, "Unexpected SDS secret (expecting abc.com): wrong.name.com"); } } // namespace } // namespace Secret } // namespace Envoy