Skip to content

Commit 759eacb

Browse files
PiotrSikoramattklein123
authored andcommitted
tls: add support for verify_certificate_spki. (envoyproxy#3475)
Signed-off-by: Piotr Sikora <[email protected]>
1 parent a3ddcf1 commit 759eacb

File tree

10 files changed

+326
-16
lines changed

10 files changed

+326
-16
lines changed

api/envoy/api/v2/auth/cert.proto

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,39 @@ message CertificateValidationContext {
127127
// system CA locations.
128128
core.DataSource trusted_ca = 1;
129129

130+
// An optional list of base64-encoded SHA-256 hashes. If specified, Envoy will verify that the
131+
// SHA-256 of the DER-encoded Subject Public Key Information (SPKI) of the presented certificate
132+
// matches one of the specified values.
133+
//
134+
// A base64-encoded SHA-256 of the Subject Public Key Information (SPKI) of the certificate
135+
// can be generated with the following command:
136+
//
137+
// .. code-block:: bash
138+
//
139+
// $ openssl x509 -in path/to/client.crt -noout -pubkey \
140+
// | openssl pkey -pubin -outform DER \
141+
// | openssl dgst -sha256 -binary \
142+
// | openssl enc -base64
143+
// NvqYIYSbgK2vCJpQhObf77vv+bQWtc5ek5RIOwPiC9A=
144+
//
145+
// This is the format used in HTTP Public Key Pinning.
146+
//
147+
// When both:
148+
// :ref:`verify_certificate_hash
149+
// <envoy_api_field_auth.CertificateValidationContext.verify_certificate_hash>` and
150+
// :ref:`verify_certificate_spki
151+
// <envoy_api_field_auth.CertificateValidationContext.verify_certificate_spki>` are specified,
152+
// a hash matching value from either of the lists will result in the certificate being accepted.
153+
//
154+
// .. attention::
155+
//
156+
// This option is preferred over :ref:`verify_certificate_hash
157+
// <envoy_api_field_auth.CertificateValidationContext.verify_certificate_hash>`,
158+
// because SPKI is tied to a private key, so it doesn't change when the certificate
159+
// is renewed using the same private key.
160+
repeated string verify_certificate_spki = 3
161+
[(validate.rules).repeated .items.string = {min_bytes: 44, max_bytes: 44}];
162+
130163
// An optional list of hex-encoded SHA-256 hashes. If specified, Envoy will verify that
131164
// the SHA-256 of the DER-encoded presented certificate matches one of the specified values.
132165
//
@@ -146,15 +179,16 @@ message CertificateValidationContext {
146179
// DF:6F:F7:2F:E9:11:65:21:26:8F:6F:2D:D4:96:6F:51:DF:47:98:83:FE:70:37:B3:9F:75:91:6A:C3:04:9D:1A
147180
//
148181
// Both of those formats are acceptable.
182+
//
183+
// When both:
184+
// :ref:`verify_certificate_hash
185+
// <envoy_api_field_auth.CertificateValidationContext.verify_certificate_hash>` and
186+
// :ref:`verify_certificate_spki
187+
// <envoy_api_field_auth.CertificateValidationContext.verify_certificate_spki>` are specified,
188+
// a hash matching value from either of the lists will result in the certificate being accepted.
149189
repeated string verify_certificate_hash = 2
150190
[(validate.rules).repeated .items.string = {min_bytes: 64, max_bytes: 95}];
151191

152-
// If specified, Envoy will verify (pin) base64-encoded SHA-256 hash of
153-
// the Subject Public Key Information (SPKI) of the presented certificate.
154-
// This is the same format as used in HTTP Public Key Pinning.
155-
// [#not-implemented-hide:]
156-
repeated string verify_spki_sha256 = 3;
157-
158192
// An optional list of Subject Alternative Names. If specified, Envoy will verify that the
159193
// Subject Alternative Name of the presented certificate matches one of the specified values.
160194
repeated string verify_subject_alt_name = 4;

docs/root/intro/version_history.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ Version history
110110
* stats: added support for histograms.
111111
* stats: added :ref:`option to configure the statsd prefix<envoy_api_field_config.metrics.v2.StatsdSink.prefix>`
112112
* stats: updated stats sink interface to flush through a single call.
113+
* tls: added support for
114+
:ref:`verify_certificate_spki <envoy_api_field_auth.CertificateValidationContext.verify_certificate_spki>`.
113115
* tls: added support for multiple
114116
:ref:`verify_certificate_hash <envoy_api_field_auth.CertificateValidationContext.verify_certificate_hash>`
115117
values.

include/envoy/ssl/context_config.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ class ContextConfig {
9292
*/
9393
virtual const std::vector<std::string>& verifyCertificateHashList() const PURE;
9494

95+
/**
96+
* @return A list of a hex-encoded SHA-256 SPKI hashes to be verified.
97+
*/
98+
virtual const std::vector<std::string>& verifyCertificateSpkiList() const PURE;
99+
95100
/**
96101
* @return The minimum TLS protocol version to negotiate.
97102
*/

source/common/ssl/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ envoy_cc_library(
6262
"//include/envoy/stats:stats_interface",
6363
"//include/envoy/stats:stats_macros",
6464
"//source/common/common:assert_lib",
65+
"//source/common/common:base64_lib",
6566
"//source/common/common:hex_lib",
6667
],
6768
)

source/common/ssl/context_config_impl.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ ContextConfigImpl::ContextConfigImpl(const envoy::api::v2::auth::CommonTlsContex
6161
config.validation_context().verify_subject_alt_name().end()),
6262
verify_certificate_hash_list_(config.validation_context().verify_certificate_hash().begin(),
6363
config.validation_context().verify_certificate_hash().end()),
64+
verify_certificate_spki_list_(config.validation_context().verify_certificate_spki().begin(),
65+
config.validation_context().verify_certificate_spki().end()),
6466
min_protocol_version_(
6567
tlsVersionFromProto(config.tls_params().tls_minimum_protocol_version(), TLS1_VERSION)),
6668
max_protocol_version_(

source/common/ssl/context_config_impl.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ class ContextConfigImpl : public virtual Ssl::ContextConfig {
4646
const std::vector<std::string>& verifyCertificateHashList() const override {
4747
return verify_certificate_hash_list_;
4848
};
49+
const std::vector<std::string>& verifyCertificateSpkiList() const override {
50+
return verify_certificate_spki_list_;
51+
};
4952
unsigned minProtocolVersion() const override { return min_protocol_version_; };
5053
unsigned maxProtocolVersion() const override { return max_protocol_version_; };
5154

@@ -74,6 +77,7 @@ class ContextConfigImpl : public virtual Ssl::ContextConfig {
7477
const std::string private_key_path_;
7578
const std::vector<std::string> verify_subject_alt_name_list_;
7679
const std::vector<std::string> verify_certificate_hash_list_;
80+
const std::vector<std::string> verify_certificate_spki_list_;
7781
const unsigned min_protocol_version_;
7882
const unsigned max_protocol_version_;
7983
};

source/common/ssl/context_impl.cc

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "envoy/runtime/runtime.h"
1010

1111
#include "common/common/assert.h"
12+
#include "common/common/base64.h"
1213
#include "common/common/fmt.h"
1314
#include "common/common/hex.h"
1415

@@ -131,6 +132,17 @@ ContextImpl::ContextImpl(ContextManagerImpl& parent, Stats::Scope& scope,
131132
verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
132133
}
133134

135+
if (!config.verifyCertificateSpkiList().empty()) {
136+
for (auto hash : config.verifyCertificateSpkiList()) {
137+
const auto decoded = Base64::decode(hash);
138+
if (decoded.size() != SHA256_DIGEST_LENGTH) {
139+
throw EnvoyException(fmt::format("Invalid base64-encoded SHA-256 {}", hash));
140+
}
141+
verify_certificate_spki_list_.emplace_back(decoded.begin(), decoded.end());
142+
}
143+
verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
144+
}
145+
134146
if (verify_mode != SSL_VERIFY_NONE) {
135147
SSL_CTX_set_verify(ctx_.get(), verify_mode, nullptr);
136148
SSL_CTX_set_cert_verify_callback(ctx_.get(), ContextImpl::verifyCallback, this);
@@ -263,10 +275,18 @@ int ContextImpl::verifyCertificate(X509* cert) {
263275
return 0;
264276
}
265277

266-
if (!verify_certificate_hash_list_.empty() &&
267-
!verifyCertificateHashList(cert, verify_certificate_hash_list_)) {
268-
stats_.fail_verify_cert_hash_.inc();
269-
return 0;
278+
if (!verify_certificate_hash_list_.empty() || !verify_certificate_spki_list_.empty()) {
279+
const bool valid_certificate_hash =
280+
!verify_certificate_hash_list_.empty() &&
281+
verifyCertificateHashList(cert, verify_certificate_hash_list_);
282+
const bool valid_certificate_spki =
283+
!verify_certificate_spki_list_.empty() &&
284+
verifyCertificateSpkiList(cert, verify_certificate_spki_list_);
285+
286+
if (!valid_certificate_hash && !valid_certificate_spki) {
287+
stats_.fail_verify_cert_hash_.inc();
288+
return 0;
289+
}
270290
}
271291

272292
return 1;
@@ -334,13 +354,37 @@ bool ContextImpl::dNSNameMatch(const std::string& dNSName, const char* pattern)
334354
}
335355

336356
bool ContextImpl::verifyCertificateHashList(
337-
X509* cert, const std::vector<std::vector<uint8_t>>& certificate_hash_list) {
357+
X509* cert, const std::vector<std::vector<uint8_t>>& expected_hashes) {
338358
std::vector<uint8_t> computed_hash(SHA256_DIGEST_LENGTH);
339359
unsigned int n;
340360
X509_digest(cert, EVP_sha256(), computed_hash.data(), &n);
341361
RELEASE_ASSERT(n == computed_hash.size());
342362

343-
for (const auto& expected_hash : certificate_hash_list) {
363+
for (const auto& expected_hash : expected_hashes) {
364+
if (computed_hash == expected_hash) {
365+
return true;
366+
}
367+
}
368+
return false;
369+
}
370+
371+
bool ContextImpl::verifyCertificateSpkiList(
372+
X509* cert, const std::vector<std::vector<uint8_t>>& expected_hashes) {
373+
X509_PUBKEY* pubkey = X509_get_X509_PUBKEY(cert);
374+
if (pubkey == nullptr) {
375+
return false;
376+
}
377+
uint8_t* spki = nullptr;
378+
const int len = i2d_X509_PUBKEY(pubkey, &spki);
379+
if (len < 0) {
380+
return false;
381+
}
382+
bssl::UniquePtr<uint8_t> free_spki(spki);
383+
384+
std::vector<uint8_t> computed_hash(SHA256_DIGEST_LENGTH);
385+
SHA256(spki, len, computed_hash.data());
386+
387+
for (const auto& expected_hash : expected_hashes) {
344388
if (computed_hash == expected_hash) {
345389
return true;
346390
}
@@ -579,6 +623,14 @@ ServerContextImpl::ServerContextImpl(ContextManagerImpl& parent, Stats::Scope& s
579623
sizeof(std::remove_reference<decltype(hash)>::type::value_type));
580624
RELEASE_ASSERT(rc == 1);
581625
}
626+
627+
// verify_certificate_spki_ can only be set with a ca_cert
628+
for (const auto& hash : verify_certificate_spki_list_) {
629+
rc = EVP_DigestUpdate(&md, hash.data(),
630+
hash.size() *
631+
sizeof(std::remove_reference<decltype(hash)>::type::value_type));
632+
RELEASE_ASSERT(rc == 1);
633+
}
582634
}
583635

584636
// Hash configured SNIs for this context, so that sessions cannot be resumed across different

source/common/ssl/context_impl.h

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,22 @@ class ContextImpl : public virtual Context {
9292
* certificate.
9393
*
9494
* @param ssl the certificate to verify
95-
* @param certificate_hash_list the configured list of certificate hashes to match
95+
* @param expected_hashes the configured list of certificate hashes to match
9696
* @return true if the verification succeeds
9797
*/
98-
static bool
99-
verifyCertificateHashList(X509* cert,
100-
const std::vector<std::vector<uint8_t>>& certificate_hash_list);
98+
static bool verifyCertificateHashList(X509* cert,
99+
const std::vector<std::vector<uint8_t>>& expected_hashes);
100+
101+
/**
102+
* Verifies certificate hash for pinning. The hash is a base64-encoded SHA-256 of the DER-encoded
103+
* Subject Public Key Information (SPKI) of the certificate.
104+
*
105+
* @param ssl the certificate to verify
106+
* @param expected_hashes the configured list of certificate hashes to match
107+
* @return true if the verification succeeds
108+
*/
109+
static bool verifyCertificateSpkiList(X509* cert,
110+
const std::vector<std::vector<uint8_t>>& expected_hashes);
101111

102112
std::vector<uint8_t> parseAlpnProtocols(const std::string& alpn_protocols);
103113
static SslStats generateStats(Stats::Scope& scope);
@@ -110,6 +120,7 @@ class ContextImpl : public virtual Context {
110120
bssl::UniquePtr<SSL_CTX> ctx_;
111121
std::vector<std::string> verify_subject_alt_name_list_;
112122
std::vector<std::vector<uint8_t>> verify_certificate_hash_list_;
123+
std::vector<std::vector<uint8_t>> verify_certificate_spki_list_;
113124
Stats::Scope& scope_;
114125
SslStats stats_;
115126
std::vector<uint8_t> parsed_alpn_protocols_;

test/common/ssl/context_impl_test.cc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,21 @@ TEST(ClientContextConfigImplTest, InvalidCertificateHash) {
359359
EnvoyException, "Invalid hex-encoded SHA-256 .*");
360360
}
361361

362+
// Validate that values other than a base64-encoded SHA-256 fail config validation.
363+
TEST(ClientContextConfigImplTest, InvalidCertificateSpki) {
364+
envoy::api::v2::auth::UpstreamTlsContext tls_context;
365+
tls_context.mutable_common_tls_context()
366+
->mutable_validation_context()
367+
// Not a base64-encoded string.
368+
->add_verify_certificate_spki("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
369+
ClientContextConfigImpl client_context_config(tls_context);
370+
Runtime::MockLoader runtime;
371+
ContextManagerImpl manager(runtime);
372+
Stats::IsolatedStoreImpl store;
373+
EXPECT_THROW_WITH_REGEX(manager.createSslClientContext(store, client_context_config),
374+
EnvoyException, "Invalid base64-encoded SHA-256 .*");
375+
}
376+
362377
// Multiple TLS certificates are not yet supported.
363378
// TODO(PiotrSikora): Support multiple TLS certificates.
364379
TEST(ClientContextConfigImplTest, MultipleTlsCertificates) {

0 commit comments

Comments
 (0)