Skip to content

Commit 41b25b1

Browse files
committed
impl: add experimental client SSL certificate support
1 parent dd5d662 commit 41b25b1

File tree

10 files changed

+349
-3
lines changed

10 files changed

+349
-3
lines changed

ci/cloudbuild/builds/lib/integration.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,21 @@ function integration::bazel_args() {
132132
"--test_env=GOOGLE_CLOUD_CPP_STORAGE_TEST_KEY_FILE_P12=${KEY_DIR}/${key_base}.p12"
133133
)
134134
fi
135+
136+
# Adds environment variables for SSL testing.
137+
# Info on how to create/refresh these can be found at:
138+
# https://cloud.google.com/certificate-authority-service/docs/create-certificate
139+
gcloud storage cp --quiet "${SECRETS_BUCKET}/client.crt" "${KEY_DIR}/client.crt" >/dev/null 2>&1 || true
140+
gcloud storage cp --quiet "${SECRETS_BUCKET}/client.chain.crt" "${KEY_DIR}/client.chain.crt" >/dev/null 2>&1 || true
141+
gcloud storage cp --quiet "${SECRETS_BUCKET}/client.private.pem" "${KEY_DIR}/client.private.pem" >/dev/null 2>&1 || true
142+
if [[ -r "${KEY_DIR}/client.crt" ]] && [[ -r "${KEY_DIR}/client.chain.crt" ]] && [[ -r "${KEY_DIR}/client.private.pem" ]]; then
143+
args+=(
144+
"--test_env=GOOGLE_CLOUD_CPP_CLIENT_SSL_CERT_FILE=${KEY_DIR}/client.crt"
145+
"--test_env=GOOGLE_CLOUD_CPP_CLIENT_SSL_CERT_CHAIN_FILE=${KEY_DIR}/client.chain.crt"
146+
"--test_env=GOOGLE_CLOUD_CPP_CLIENT_SSL_KEY_FILE=${KEY_DIR}/client.private.pem"
147+
)
148+
fi
149+
135150
printf "%s\n" "${args[@]}"
136151
}
137152

google/cloud/credentials.h

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include "google/cloud/common_options.h"
1919
#include "google/cloud/options.h"
20+
#include "google/cloud/ssl_certificate.h"
2021
#include "google/cloud/version.h"
2122
#include <chrono>
2223
#include <memory>
@@ -30,6 +31,20 @@ namespace internal {
3031
class CredentialsVisitor;
3132
} // namespace internal
3233

34+
namespace experimental {
35+
/**
36+
* Represents a Client SSL certificate used in mTLS authentication.
37+
*
38+
* Providing this option enables both PEER and HOST verification.
39+
*
40+
* @note This option is currently experimental and only works with services
41+
* using JSON/HTTP transport.
42+
*/
43+
struct ClientSslCertificateOption {
44+
using Type = SslCertificate;
45+
};
46+
} // namespace experimental
47+
3348
/**
3449
* An opaque representation of the authentication configuration.
3550
*
@@ -433,7 +448,8 @@ struct CARootsFilePathOption {
433448
using UnifiedCredentialsOptionList =
434449
OptionList<AccessTokenLifetimeOption, CARootsFilePathOption,
435450
DelegatesOption, ScopesOption, LoggingComponentsOption,
436-
UnifiedCredentialsOption>;
451+
UnifiedCredentialsOption,
452+
experimental::ClientSslCertificateOption>;
437453

438454
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
439455
} // namespace cloud

google/cloud/google_cloud_cpp_common.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ google_cloud_cpp_common_hdrs = [
107107
"project.h",
108108
"retry_policy.h",
109109
"rpc_metadata.h",
110+
"ssl_certificate.h",
110111
"status.h",
111112
"status_or.h",
112113
"stream_range.h",

google/cloud/google_cloud_cpp_common.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ add_library(
167167
project.h
168168
retry_policy.h
169169
rpc_metadata.h
170+
ssl_certificate.h
170171
status.cc
171172
status.h
172173
status_or.h

google/cloud/internal/curl_impl.cc

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include "google/cloud/internal/curl_impl.h"
1616
#include "google/cloud/common_options.h"
17+
#include "google/cloud/credentials.h"
1718
#include "google/cloud/internal/absl_str_cat_quiet.h"
1819
#include "google/cloud/internal/absl_str_join_quiet.h"
1920
#include "google/cloud/internal/algorithm.h"
@@ -26,6 +27,7 @@
2627
#include "google/cloud/rest_options.h"
2728
#include "absl/strings/match.h"
2829
#include "absl/strings/strip.h"
30+
#include <curl/easy.h>
2931
#include <algorithm>
3032
#include <sstream>
3133
#include <thread>
@@ -198,6 +200,10 @@ CurlImpl::CurlImpl(CurlHandle handle,
198200
proxy_username_ = CurlOptProxyUsername(options);
199201
proxy_password_ = CurlOptProxyPassword(options);
200202

203+
if (options.has<experimental::ClientSslCertificateOption>()) {
204+
client_ssl_cert_ = options.get<experimental::ClientSslCertificateOption>();
205+
}
206+
201207
interface_ = CurlOptInterface(options);
202208
}
203209

@@ -327,6 +333,35 @@ Status CurlImpl::MakeRequest(HttpMethod method, RestContext& context,
327333
if (!status.ok()) return OnTransferError(context, std::move(status));
328334
}
329335

336+
if (client_ssl_cert_.has_value()) {
337+
status = handle_.SetOption(CURLOPT_SSL_VERIFYPEER, 1L);
338+
if (!status.ok()) return OnTransferError(context, std::move(status));
339+
status = handle_.SetOption(CURLOPT_SSL_VERIFYHOST, 2L);
340+
if (!status.ok()) return OnTransferError(context, std::move(status));
341+
342+
status = handle_.SetOption(CURLOPT_SSLCERTTYPE,
343+
experimental::SslCertificate::ToString(
344+
client_ssl_cert_->ssl_certificate_type())
345+
.c_str());
346+
if (!status.ok()) return OnTransferError(context, std::move(status));
347+
348+
struct curl_blob ssl_cert_blob;
349+
ssl_cert_blob.data =
350+
const_cast<char*>(client_ssl_cert_->ssl_certificate().data());
351+
ssl_cert_blob.len = client_ssl_cert_->ssl_certificate().length();
352+
ssl_cert_blob.flags = CURL_BLOB_COPY;
353+
status = handle_.SetOption(CURLOPT_SSLCERT_BLOB, &ssl_cert_blob);
354+
if (!status.ok()) return OnTransferError(context, std::move(status));
355+
356+
struct curl_blob ssl_key_blob;
357+
ssl_key_blob.data =
358+
const_cast<char*>(client_ssl_cert_->ssl_private_key().data());
359+
ssl_key_blob.len = client_ssl_cert_->ssl_private_key().length();
360+
ssl_key_blob.flags = CURL_BLOB_COPY;
361+
status = handle_.SetOption(CURLOPT_SSLKEY_BLOB, &ssl_key_blob);
362+
if (!status.ok()) return OnTransferError(context, std::move(status));
363+
}
364+
330365
if (method == HttpMethod::kGet) {
331366
status = handle_.SetOption(CURLOPT_NOPROGRESS, 1L);
332367
if (!status.ok()) return OnTransferError(context, std::move(status));

google/cloud/internal/curl_impl.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "google/cloud/internal/rest_request.h"
2424
#include "google/cloud/internal/rest_response.h"
2525
#include "google/cloud/options.h"
26+
#include "google/cloud/ssl_certificate.h"
2627
#include "google/cloud/status_or.h"
2728
#include "google/cloud/version.h"
2829
#include "absl/types/optional.h"
@@ -143,6 +144,8 @@ class CurlImpl {
143144
absl::optional<std::string> proxy_username_;
144145
absl::optional<std::string> proxy_password_;
145146

147+
absl::optional<experimental::SslCertificate> client_ssl_cert_ = absl::nullopt;
148+
146149
absl::optional<std::string> interface_;
147150

148151
CurlReceivedHeaders received_headers_;

google/cloud/ssl_certificate.h

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SSL_CERTIFICATE_H
16+
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SSL_CERTIFICATE_H
17+
18+
#include "google/cloud/version.h"
19+
#include <string>
20+
21+
namespace google {
22+
namespace cloud {
23+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
24+
namespace experimental {
25+
26+
/**
27+
* Represents an SSL certificate used in TLS authentication.
28+
*/
29+
class SslCertificate {
30+
public:
31+
enum class SslCertificateType { kPEM, kDER, kP12 };
32+
33+
/// Creates and empty certificate.
34+
SslCertificate() = default;
35+
36+
/// Creates a PEM certificate from the values provided.
37+
SslCertificate(std::string ssl_certificate, std::string ssl_private_key)
38+
: ssl_certificate_(std::move(ssl_certificate)),
39+
ssl_private_key_(std::move(ssl_private_key)) {}
40+
41+
/// Creates a user specified type of certificate from the values provided.
42+
SslCertificate(std::string ssl_certificate, std::string ssl_private_key,
43+
SslCertificateType ssl_certificate_type)
44+
: ssl_certificate_(std::move(ssl_certificate)),
45+
ssl_private_key_(std::move(ssl_private_key)),
46+
ssl_certificate_type_(ssl_certificate_type) {}
47+
48+
std::string const& ssl_certificate() const { return ssl_certificate_; }
49+
50+
std::string const& ssl_private_key() const { return ssl_private_key_; }
51+
52+
SslCertificateType ssl_certificate_type() const {
53+
return ssl_certificate_type_;
54+
}
55+
56+
static std::string ToString(SslCertificateType type) {
57+
switch (type) {
58+
case SslCertificateType::kPEM:
59+
return "PEM";
60+
case SslCertificateType::kDER:
61+
return "DER";
62+
case SslCertificateType::kP12:
63+
return "P12";
64+
}
65+
return {};
66+
}
67+
68+
private:
69+
std::string ssl_certificate_;
70+
std::string ssl_private_key_;
71+
SslCertificateType ssl_certificate_type_ = SslCertificateType::kPEM;
72+
};
73+
74+
} // namespace experimental
75+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
76+
} // namespace cloud
77+
} // namespace google
78+
79+
#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_SSL_CERTIFICATE_H

google/cloud/storage/tests/CMakeLists.txt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,11 @@ set(storage_client_integration_tests
7272
tracing_integration_test.cc)
7373
set(storage_client_integration_tests_production
7474
# cmake-format: sort
75-
alternative_endpoint_integration_test.cc key_file_integration_test.cc
76-
signed_url_integration_test.cc unified_credentials_integration_test.cc)
75+
alternative_endpoint_integration_test.cc
76+
key_file_integration_test.cc
77+
mtls_object_basic_crud_integration_test.cc
78+
signed_url_integration_test.cc
79+
unified_credentials_integration_test.cc)
7780
list(APPEND storage_client_integration_tests
7881
${storage_client_integration_tests_production})
7982
list(SORT storage_client_integration_tests)

0 commit comments

Comments
 (0)