Skip to content

Commit 12d9697

Browse files
authored
impl: add experimental client SSL certificate support (#15062)
1 parent 04207ac commit 12d9697

File tree

10 files changed

+360
-3
lines changed

10 files changed

+360
-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: 19 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,22 @@ 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+
* @note Requires libcurl v7.71.0 or later.
44+
*/
45+
struct ClientSslCertificateOption {
46+
using Type = SslCertificate;
47+
};
48+
} // namespace experimental
49+
3350
/**
3451
* An opaque representation of the authentication configuration.
3552
*
@@ -433,7 +450,8 @@ struct CARootsFilePathOption {
433450
using UnifiedCredentialsOptionList =
434451
OptionList<AccessTokenLifetimeOption, CARootsFilePathOption,
435452
DelegatesOption, ScopesOption, LoggingComponentsOption,
436-
UnifiedCredentialsOption>;
453+
UnifiedCredentialsOption,
454+
experimental::ClientSslCertificateOption>;
437455

438456
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
439457
} // 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: 44 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,44 @@ 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+
#if CURL_AT_LEAST_VERSION(7, 71, 0)
338+
status = handle_.SetOption(CURLOPT_SSL_VERIFYPEER, 1L);
339+
if (!status.ok()) return OnTransferError(context, std::move(status));
340+
status = handle_.SetOption(CURLOPT_SSL_VERIFYHOST, 2L);
341+
if (!status.ok()) return OnTransferError(context, std::move(status));
342+
343+
status = handle_.SetOption(CURLOPT_SSLCERTTYPE,
344+
experimental::SslCertificate::ToString(
345+
client_ssl_cert_->ssl_certificate_type())
346+
.c_str());
347+
if (!status.ok()) return OnTransferError(context, std::move(status));
348+
349+
struct curl_blob ssl_cert_blob;
350+
ssl_cert_blob.data =
351+
const_cast<char*>(client_ssl_cert_->ssl_certificate().data());
352+
ssl_cert_blob.len = client_ssl_cert_->ssl_certificate().length();
353+
ssl_cert_blob.flags = CURL_BLOB_COPY;
354+
status = handle_.SetOption(CURLOPT_SSLCERT_BLOB, &ssl_cert_blob);
355+
if (!status.ok()) return OnTransferError(context, std::move(status));
356+
357+
struct curl_blob ssl_key_blob;
358+
ssl_key_blob.data =
359+
const_cast<char*>(client_ssl_cert_->ssl_private_key().data());
360+
ssl_key_blob.len = client_ssl_cert_->ssl_private_key().length();
361+
ssl_key_blob.flags = CURL_BLOB_COPY;
362+
status = handle_.SetOption(CURLOPT_SSLKEY_BLOB, &ssl_key_blob);
363+
if (!status.ok()) return OnTransferError(context, std::move(status));
364+
#else
365+
return OnTransferError(
366+
context,
367+
internal::InvalidArgumentError(
368+
"libcurl 7.71.0 or higher required to use ClientSslCertificate",
369+
GCP_ERROR_INFO().WithMetadata("current_libcurl_version",
370+
LIBCURL_VERSION)));
371+
#endif
372+
}
373+
330374
if (method == HttpMethod::kGet) {
331375
status = handle_.SetOption(CURLOPT_NOPROGRESS, 1L);
332376
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)