Skip to content

Commit

Permalink
fix(common): missed using CARootsFilePathOption
Browse files Browse the repository at this point in the history
A number of credentials failed to use the `CARootsFilePathOption` as
they should.

Use the storage integration test because it covers both gRPC and REST.
  • Loading branch information
coryan committed Oct 30, 2023
1 parent 86a09c6 commit 7c9646e
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 89 deletions.
3 changes: 1 addition & 2 deletions google/cloud/credentials.h
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,7 @@ struct AccessTokenLifetimeOption {
*
* @warning gRPC does not have a programmatic mechanism to set the CA
* certificates for the default credentials. This option only has no effect
* with `MakeGoogleDefaultCredentials()`, or
* `MakeServiceAccountCredentials()`.
* with `MakeGoogleDefaultCredentials()`.
* Consider using the `GRPC_DEFAULT_SSL_ROOTS_FILE_PATH` environment
* variable in these cases.
*
Expand Down
3 changes: 1 addition & 2 deletions google/cloud/internal/grpc_service_account_authentication.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ GrpcServiceAccountAuthentication::GrpcServiceAccountAuthentication(

std::shared_ptr<grpc::Channel> GrpcServiceAccountAuthentication::CreateChannel(
std::string const& endpoint, grpc::ChannelArguments const& arguments) {
// TODO(#6311) - support setting SSL options
auto credentials = grpc::SslCredentials(grpc::SslCredentialsOptions{});
auto credentials = grpc::SslCredentials(ssl_options_);
return grpc::CreateCustomChannel(endpoint, credentials, arguments);
}

Expand Down
5 changes: 4 additions & 1 deletion google/cloud/internal/unified_grpc_credentials.cc
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,12 @@ std::shared_ptr<GrpcAuthenticationStrategy> CreateAuthenticationStrategy(
cfg.json_object(), std::move(options));
}
void visit(ExternalAccountConfig const& cfg) override {
grpc::SslCredentialsOptions ssl_options;
auto cainfo = LoadCAInfo(options);
if (cainfo) ssl_options.pem_root_certs = std::move(*cainfo);
result = std::make_unique<GrpcChannelCredentialsAuthentication>(
grpc::CompositeChannelCredentials(
grpc::SslCredentials(grpc::SslCredentialsOptions()),
grpc::SslCredentials(ssl_options),
GrpcExternalAccountCredentials(cfg)));
}
} visitor(std::move(cq), std::move(options));
Expand Down
2 changes: 1 addition & 1 deletion google/cloud/storage/testing/temp_file.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class TempFile {

~TempFile();

std::string name() { return name_; }
std::string name() const { return name_; }

private:
std::string name_;
Expand Down
251 changes: 168 additions & 83 deletions google/cloud/storage/tests/unified_credentials_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,44 @@ using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::StartsWith;

// This is a properly formatted, but invalid, CA Certificate. We will use this
// as the *only* root of trust and try to contact *.google.com. This will
// (naturally) fail, which is the expected result. A separate test will verify
// that using a *valid* CA certificate (bundle) works.
//
// Created with:
//
// openssl genrsa -des3 -out cert.key 2048
// openssl req -x509 -new -nodes -key cert.key
// -sha256 -days 3650 -out cert.pem
auto constexpr kCACertificate = R"""(
-----BEGIN CERTIFICATE-----
MIIEPTCCAyWgAwIBAgIUXa/2HsbYrolo1Cox/1SOqLDnNYQwDQYJKoZIhvcNAQEL
BQAwga0xCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwI
TmV3IFlvcmsxGjAYBgNVBAoMEVRlc3QtT25seSBJbnZhbGlkMRIwEAYDVQQLDAlU
ZXN0LU9ubHkxGjAYBgNVBAMMEVRlc3QtT25seSBJbnZhbGlkMSwwKgYJKoZIhvcN
AQkBFh10ZXN0LW9ubHlAaW52YWxpZC5leGFtcGxlLmNvbTAeFw0yMTA1MjUxOTQy
MTdaFw0zMTA1MjMxOTQyMTdaMIGtMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3
IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMRowGAYDVQQKDBFUZXN0LU9ubHkgSW52
YWxpZDESMBAGA1UECwwJVGVzdC1Pbmx5MRowGAYDVQQDDBFUZXN0LU9ubHkgSW52
YWxpZDEsMCoGCSqGSIb3DQEJARYddGVzdC1vbmx5QGludmFsaWQuZXhhbXBsZS5j
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDL66K2V2OQHcb2Ab7o
ucWqb3iOF1IGvc6lzC2XeqrqCvYF5HB9jK+cWHDmeGjJMoYI35S2fp+Wzh3Qek0Y
ilQBylUq91y2ZnqAmFu7gc83wWgWPHCkKPKTS5tcK5sQTbhBuaQBQs5hWCeNfZOy
AtAU2ysNde79DwSXfq/e8NLRvaKsS8etqiLyfIuDWfXHzIDgAgyyi49m67fYnLYx
y2W555Zh0vAnd4MQYh0SYQ64BgaUg59WLRYhsHN5r9D06JEur9uxMLmtiQy7UyL2
xuw6u2y/+vcdTb9L9zaNEnITPl+3N23TG5KgBxLfKkHzNE7gS/w7ljS0ljNExuUO
UZutAgMBAAGjUzBRMB0GA1UdDgQWBBS7T7DuKDv1Hz1e693kY7gLXSu8PjAfBgNV
HSMEGDAWgBS7T7DuKDv1Hz1e693kY7gLXSu8PjAPBgNVHRMBAf8EBTADAQH/MA0G
CSqGSIb3DQEBCwUAA4IBAQC/4HmQwp7KjKF5FEnlHG5Chqob/yiPkwbMDV1yKA+i
ZMknQFgm8h0X4kLAbOiao71MPI5Zi5OQge6GoSXJJQYkesgdakPw6xkFfz9MsfCp
zMKm7sKIIGNaPMMqMJJ/cciCRIzXKl6gcOkZLlIFU1T2uE764Lc08yXIY7eXkVo1
8w4Gv6nH7uaQiESa2wt3TWGISG9wdDkcVG01tTr4jzq77yWuC1Ela2UvQK7AIWbK
OB6Sby1bvjYv0kqjinu9AcSCHUHJ1sJaPD5DvAyKP7W01YedP4iqLxkTlIRjaikD
KlXA1yQW/ClmnHVg57SN1g1rvOJCcnHBnSbT7kGFqUol
-----END CERTIFICATE-----
)""";

class UnifiedCredentialsIntegrationTest
: public ::google::cloud::storage::testing::StorageIntegrationTest,
public ::testing::WithParamInterface<std::string> {
Expand Down Expand Up @@ -77,7 +115,30 @@ class UnifiedCredentialsIntegrationTest
std::string const& bucket_name() const { return bucket_name_; }
std::string const& project_id() const { return project_id_; }
std::string const& service_account() const { return service_account_; }
std::string const& roots_pem() const { return roots_pem_; }
std::string roots_pem() const { return roots_pem_; }
std::string invalid_pem() const { return invalid_pem_.name(); }
std::string empty_file() const { return empty_file_.name(); }

static Options TestOptions() {
return Options{}.set<RetryPolicyOption>(
LimitedErrorCountRetryPolicy(3).clone());
}

Options EmptyTrustStoreOptions() {
return TestOptions()
// Use the trust store with no valid just an invalid CA certificate.
.set<CARootsFilePathOption>(invalid_pem())
// Disable the default CAPath in libcurl, no effect on gRPC.
.set<internal::CAPathOption>(empty_file());
}

Options CustomTrustStoreOptions() {
return TestOptions()
// Use the custom trust store with Google's root CA certificates.
.set<CARootsFilePathOption>(roots_pem())
// Disable the default CAPath in libcurl, no effect on gRPC.
.set<internal::CAPathOption>(empty_file());
}

void UseClient(Client client, std::string const& bucket_name,
std::string const& object_name, std::string const& payload) {
Expand All @@ -91,11 +152,21 @@ class UnifiedCredentialsIntegrationTest
EXPECT_EQ(payload, actual);
}

void ExpectInsertFailure(Client client, std::string const& bucket_name,
std::string const& object_name) {
auto meta = client.InsertObject(bucket_name, object_name, LoremIpsum(),
IfGenerationMatch(0));
EXPECT_THAT(meta, Not(IsOk()));
if (meta) ScheduleForDelete(*meta);
}

private:
std::string bucket_name_;
std::string project_id_;
std::string service_account_;
std::string roots_pem_;
TempFile invalid_pem_{kCACertificate};
TempFile empty_file_{std::string{}};
testing_util::ScopedEnvironment grpc_config_;
};

Expand All @@ -108,41 +179,43 @@ TEST_P(UnifiedCredentialsIntegrationTest, GoogleDefaultCredentials) {
UseClient(client, bucket_name(), MakeRandomObjectName(), LoremIpsum()));
}

TEST_P(UnifiedCredentialsIntegrationTest, AccessToken) {
TEST_P(UnifiedCredentialsIntegrationTest, SAImpersonation) {
if (UsingEmulator()) GTEST_SKIP();
// First use the default credentials to obtain an access token, then use the
// access token to test the DynamicAccessTokenCredentials() function. In a
// real application one would fetch access tokens from something more
// interesting, like the IAM credentials service. This is just a reasonably
// easy way to get a working access token for the test.
auto default_credentials = oauth2::GoogleDefaultCredentials();
ASSERT_THAT(default_credentials, IsOk());
auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1);
auto header = default_credentials.value()->AuthorizationHeader();
ASSERT_THAT(header, IsOk());

auto constexpr kPrefix = "Authorization: Bearer ";
ASSERT_THAT(*header, StartsWith(kPrefix));
auto token = header->substr(std::strlen(kPrefix));

auto client = MakeTestClient(Options{}.set<UnifiedCredentialsOption>(
MakeAccessTokenCredentials(token, expiration)));
MakeImpersonateServiceAccountCredentials(MakeGoogleDefaultCredentials(),
service_account())));

ASSERT_NO_FATAL_FAILURE(
UseClient(client, bucket_name(), MakeRandomObjectName(), LoremIpsum()));
}

TEST_P(UnifiedCredentialsIntegrationTest, ServiceAccountImpersonation) {
TEST_P(UnifiedCredentialsIntegrationTest, SAImpersonationCustomTrustStore) {
if (UsingEmulator()) GTEST_SKIP();

auto client = MakeTestClient(Options{}.set<UnifiedCredentialsOption>(
MakeImpersonateServiceAccountCredentials(MakeGoogleDefaultCredentials(),
service_account())));
auto client =
MakeTestClient(CustomTrustStoreOptions().set<UnifiedCredentialsOption>(
MakeImpersonateServiceAccountCredentials(
MakeGoogleDefaultCredentials(), service_account(),
CustomTrustStoreOptions())));

ASSERT_NO_FATAL_FAILURE(
UseClient(client, bucket_name(), MakeRandomObjectName(), LoremIpsum()));
}

TEST_P(UnifiedCredentialsIntegrationTest, SAImpersonationEmptyTrustStore) {
if (UsingEmulator()) GTEST_SKIP();

auto client =
MakeTestClient(EmptyTrustStoreOptions().set<UnifiedCredentialsOption>(
MakeImpersonateServiceAccountCredentials(
MakeGoogleDefaultCredentials(), service_account(),
EmptyTrustStoreOptions())));

ASSERT_NO_FATAL_FAILURE(
ExpectInsertFailure(client, bucket_name(), MakeRandomObjectName()));
}

TEST_P(UnifiedCredentialsIntegrationTest, ServiceAccount) {
if (UsingEmulator()) GTEST_SKIP();
auto keyfile = GetEnv("GOOGLE_CLOUD_CPP_STORAGE_TEST_KEY_FILE_JSON");
Expand All @@ -160,13 +233,49 @@ TEST_P(UnifiedCredentialsIntegrationTest, ServiceAccount) {
UseClient(client, bucket_name(), MakeRandomObjectName(), LoremIpsum()));
}

TEST_P(UnifiedCredentialsIntegrationTest, AccessTokenEmptyTrustStore) {
TEST_P(UnifiedCredentialsIntegrationTest, ServiceAccountCustomTrustStore) {
if (UsingEmulator()) GTEST_SKIP();
auto keyfile = GetEnv("GOOGLE_CLOUD_CPP_STORAGE_TEST_KEY_FILE_JSON");
if (!keyfile.has_value()) GTEST_SKIP();

auto contents = [](std::string const& filename) {
std::ifstream is(filename);
return std::string{std::istreambuf_iterator<char>{is}, {}};
}(keyfile.value());

auto client =
MakeTestClient(CustomTrustStoreOptions().set<UnifiedCredentialsOption>(
MakeServiceAccountCredentials(contents, CustomTrustStoreOptions())));

ASSERT_NO_FATAL_FAILURE(
UseClient(client, bucket_name(), MakeRandomObjectName(), LoremIpsum()));
}

TEST_P(UnifiedCredentialsIntegrationTest, ServiceAccountEmptyTrustStore) {
if (UsingEmulator()) GTEST_SKIP();
auto keyfile = GetEnv("GOOGLE_CLOUD_CPP_STORAGE_TEST_KEY_FILE_JSON");
if (!keyfile.has_value()) GTEST_SKIP();

auto contents = [](std::string const& filename) {
std::ifstream is(filename);
return std::string{std::istreambuf_iterator<char>{is}, {}};
}(keyfile.value());

auto client =
MakeTestClient(EmptyTrustStoreOptions().set<UnifiedCredentialsOption>(
MakeServiceAccountCredentials(contents, EmptyTrustStoreOptions())));

ASSERT_NO_FATAL_FAILURE(
ExpectInsertFailure(client, bucket_name(), MakeRandomObjectName()));
}

TEST_P(UnifiedCredentialsIntegrationTest, AccessToken) {
if (UsingEmulator()) GTEST_SKIP();
// First use the default credentials to obtain an access token, then use the
// access token to test the AccessTokenCredentials() function. In a real
// application one would fetch access tokens from something more interesting,
// like the IAM credentials service. This is just a reasonably easy way to get
// a working access token for the test.
// access token to test the DynamicAccessTokenCredentials() function. In a
// real application one would fetch access tokens from something more
// interesting, like the IAM credentials service. This is just a reasonably
// easy way to get a working access token for the test.
auto default_credentials = oauth2::GoogleDefaultCredentials();
ASSERT_THAT(default_credentials, IsOk());
auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1);
Expand All @@ -177,58 +286,11 @@ TEST_P(UnifiedCredentialsIntegrationTest, AccessTokenEmptyTrustStore) {
ASSERT_THAT(*header, StartsWith(kPrefix));
auto token = header->substr(std::strlen(kPrefix));

// This is a properly formatted, but invalid, CA Certificate. We will use this
// as the *only* root of trust and try to contact *.google.com. This will
// (naturally) fail, which is the expected result. A separate test will verify
// that using a *valid* CA certificate (bundle) works.
//
// Created with:
//
// openssl genrsa -des3 -out cert.key 2048
// openssl req -x509 -new -nodes -key cert.key
// -sha256 -days 3650 -out cert.pem
auto constexpr kCACertificate = R"""(
-----BEGIN CERTIFICATE-----
MIIEPTCCAyWgAwIBAgIUXa/2HsbYrolo1Cox/1SOqLDnNYQwDQYJKoZIhvcNAQEL
BQAwga0xCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwI
TmV3IFlvcmsxGjAYBgNVBAoMEVRlc3QtT25seSBJbnZhbGlkMRIwEAYDVQQLDAlU
ZXN0LU9ubHkxGjAYBgNVBAMMEVRlc3QtT25seSBJbnZhbGlkMSwwKgYJKoZIhvcN
AQkBFh10ZXN0LW9ubHlAaW52YWxpZC5leGFtcGxlLmNvbTAeFw0yMTA1MjUxOTQy
MTdaFw0zMTA1MjMxOTQyMTdaMIGtMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3
IFlvcmsxETAPBgNVBAcMCE5ldyBZb3JrMRowGAYDVQQKDBFUZXN0LU9ubHkgSW52
YWxpZDESMBAGA1UECwwJVGVzdC1Pbmx5MRowGAYDVQQDDBFUZXN0LU9ubHkgSW52
YWxpZDEsMCoGCSqGSIb3DQEJARYddGVzdC1vbmx5QGludmFsaWQuZXhhbXBsZS5j
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDL66K2V2OQHcb2Ab7o
ucWqb3iOF1IGvc6lzC2XeqrqCvYF5HB9jK+cWHDmeGjJMoYI35S2fp+Wzh3Qek0Y
ilQBylUq91y2ZnqAmFu7gc83wWgWPHCkKPKTS5tcK5sQTbhBuaQBQs5hWCeNfZOy
AtAU2ysNde79DwSXfq/e8NLRvaKsS8etqiLyfIuDWfXHzIDgAgyyi49m67fYnLYx
y2W555Zh0vAnd4MQYh0SYQ64BgaUg59WLRYhsHN5r9D06JEur9uxMLmtiQy7UyL2
xuw6u2y/+vcdTb9L9zaNEnITPl+3N23TG5KgBxLfKkHzNE7gS/w7ljS0ljNExuUO
UZutAgMBAAGjUzBRMB0GA1UdDgQWBBS7T7DuKDv1Hz1e693kY7gLXSu8PjAfBgNV
HSMEGDAWgBS7T7DuKDv1Hz1e693kY7gLXSu8PjAPBgNVHRMBAf8EBTADAQH/MA0G
CSqGSIb3DQEBCwUAA4IBAQC/4HmQwp7KjKF5FEnlHG5Chqob/yiPkwbMDV1yKA+i
ZMknQFgm8h0X4kLAbOiao71MPI5Zi5OQge6GoSXJJQYkesgdakPw6xkFfz9MsfCp
zMKm7sKIIGNaPMMqMJJ/cciCRIzXKl6gcOkZLlIFU1T2uE764Lc08yXIY7eXkVo1
8w4Gv6nH7uaQiESa2wt3TWGISG9wdDkcVG01tTr4jzq77yWuC1Ela2UvQK7AIWbK
OB6Sby1bvjYv0kqjinu9AcSCHUHJ1sJaPD5DvAyKP7W01YedP4iqLxkTlIRjaikD
KlXA1yQW/ClmnHVg57SN1g1rvOJCcnHBnSbT7kGFqUol
-----END CERTIFICATE-----
)""";
TempFile tmp(kCACertificate);
TempFile tmp2(std::string{});

auto client = MakeTestClient(
Options{}
.set<UnifiedCredentialsOption>(
MakeAccessTokenCredentials(token, expiration))
.set<CARootsFilePathOption>(tmp.name())
.set<internal::CAPathOption>(tmp2.name())
.set<RetryPolicyOption>(LimitedErrorCountRetryPolicy(2).clone()));
auto client = MakeTestClient(Options{}.set<UnifiedCredentialsOption>(
MakeAccessTokenCredentials(token, expiration)));

auto const object_name = MakeRandomObjectName();
auto meta = client.InsertObject(bucket_name(), object_name, LoremIpsum(),
IfGenerationMatch(0));
EXPECT_THAT(meta, Not(IsOk()));
ASSERT_NO_FATAL_FAILURE(
UseClient(client, bucket_name(), MakeRandomObjectName(), LoremIpsum()));
}

TEST_P(UnifiedCredentialsIntegrationTest, AccessTokenCustomTrustStore) {
Expand All @@ -251,19 +313,42 @@ TEST_P(UnifiedCredentialsIntegrationTest, AccessTokenCustomTrustStore) {

testing_util::ScopedEnvironment grpc_roots_pem(
"GRPC_DEFAULT_SSL_ROOTS_FILE_PATH", absl::nullopt);
TempFile tmp2(std::string{});

auto client =
MakeTestClient(Options{}
.set<UnifiedCredentialsOption>(
MakeAccessTokenCredentials(token, expiration))
.set<CARootsFilePathOption>(roots_pem())
.set<internal::CAPathOption>(tmp2.name()));
MakeTestClient(CustomTrustStoreOptions().set<UnifiedCredentialsOption>(
MakeAccessTokenCredentials(token, expiration)));

ASSERT_NO_FATAL_FAILURE(
UseClient(client, bucket_name(), MakeRandomObjectName(), LoremIpsum()));
}

TEST_P(UnifiedCredentialsIntegrationTest, AccessTokenEmptyTrustStore) {
if (UsingEmulator()) GTEST_SKIP();
// First use the default credentials to obtain an access token, then use the
// access token to test the AccessTokenCredentials() function. In a real
// application one would fetch access tokens from something more interesting,
// like the IAM credentials service. This is just a reasonably easy way to get
// a working access token for the test.
auto default_credentials = oauth2::GoogleDefaultCredentials();
ASSERT_THAT(default_credentials, IsOk());
auto expiration = std::chrono::system_clock::now() + std::chrono::hours(1);
auto header = default_credentials.value()->AuthorizationHeader();
ASSERT_THAT(header, IsOk());

auto constexpr kPrefix = "Authorization: Bearer ";
ASSERT_THAT(*header, StartsWith(kPrefix));
auto token = header->substr(std::strlen(kPrefix));

auto client = MakeTestClient(
EmptyTrustStoreOptions()
.set<UnifiedCredentialsOption>(
MakeAccessTokenCredentials(token, expiration))
.set<RetryPolicyOption>(LimitedErrorCountRetryPolicy(2).clone()));

EXPECT_NO_FATAL_FAILURE(
ExpectInsertFailure(client, bucket_name(), MakeRandomObjectName()));
}

#if GOOGLE_CLOUD_CPP_STORAGE_HAVE_GRPC
INSTANTIATE_TEST_SUITE_P(UnifiedCredentialsGrpcIntegrationTest,
UnifiedCredentialsIntegrationTest,
Expand Down

0 comments on commit 7c9646e

Please sign in to comment.