diff --git a/include/envoy/ssl/context_config.h b/include/envoy/ssl/context_config.h index 83df4dc70360e..c435454006d8d 100644 --- a/include/envoy/ssl/context_config.h +++ b/include/envoy/ssl/context_config.h @@ -49,6 +49,17 @@ class ContextConfig { */ virtual const std::string& caCertPath() const PURE; + /** + * @return The CRL to check if a cert is revoked. + */ + virtual const std::string& certificateRevocationList() const PURE; + + /** + * @return Path of the certificate revocation list, or "" if the CRL + * was inlined. + */ + virtual const std::string& certificateRevocationListPath() const PURE; + /** * @return The certificate chain used to identify the local side. */ diff --git a/source/common/config/tls_context_json.cc b/source/common/config/tls_context_json.cc index 0ec4e2e1c6b99..1c106056a93cc 100644 --- a/source/common/config/tls_context_json.cc +++ b/source/common/config/tls_context_json.cc @@ -48,6 +48,9 @@ void TlsContextJson::translateCommonTlsContext( validation_context->mutable_trusted_ca()->set_filename( json_tls_context.getString("ca_cert_file", "")); } + if (json_tls_context.hasObject("crl_file")) { + validation_context->mutable_crl()->set_filename(json_tls_context.getString("crl_file", "")); + } if (json_tls_context.hasObject("verify_certificate_hash")) { validation_context->add_verify_certificate_hash( json_tls_context.getString("verify_certificate_hash")); diff --git a/source/common/json/config_schemas.cc b/source/common/json/config_schemas.cc index f887d8e121d6d..d7c9f1c44ff83 100644 --- a/source/common/json/config_schemas.cc +++ b/source/common/json/config_schemas.cc @@ -156,7 +156,8 @@ const std::string Json::Schema::LISTENER_SCHEMA(R"EOF( } }, "cipher_suites" : {"type" : "string", "minLength" : 1}, - "ecdh_curves" : {"type" : "string", "minLength" : 1} + "ecdh_curves" : {"type" : "string", "minLength" : 1}, + "crl_file" : {"type" : "string"} }, "required": ["cert_chain_file", "private_key_file"], "additionalProperties": false diff --git a/source/common/ssl/context_config_impl.cc b/source/common/ssl/context_config_impl.cc index 9414002913228..666adc2771aa5 100644 --- a/source/common/ssl/context_config_impl.cc +++ b/source/common/ssl/context_config_impl.cc @@ -43,6 +43,8 @@ ContextConfigImpl::ContextConfigImpl(const envoy::api::v2::CommonTlsContext& con RepeatedPtrUtil::join(config.tls_params().ecdh_curves(), ":"), DEFAULT_ECDH_CURVES)), ca_cert_(readDataSource(config.validation_context().trusted_ca(), true)), ca_cert_path_(getDataSourcePath(config.validation_context().trusted_ca())), + certificate_revocation_list_(readDataSource(config.validation_context().crl(), true)), + certificate_revocation_list_path_(getDataSourcePath(config.validation_context().crl())), cert_chain_(config.tls_certificates().empty() ? "" : readDataSource(config.tls_certificates()[0].certificate_chain(), true)), @@ -66,6 +68,10 @@ ContextConfigImpl::ContextConfigImpl(const envoy::api::v2::CommonTlsContext& con tlsVersionFromProto(config.tls_params().tls_maximum_protocol_version(), TLS1_2_VERSION)) { // TODO(htuch): Support multiple hashes. ASSERT(config.validation_context().verify_certificate_hash().size() <= 1); + if (ca_cert_.empty() && !certificate_revocation_list_.empty()) { + throw EnvoyException(fmt::format("Failed to load CRL from {} without trusted CA certificates", + certificateRevocationListPath())); + } } const std::string ContextConfigImpl::readDataSource(const envoy::api::v2::DataSource& source, diff --git a/source/common/ssl/context_config_impl.h b/source/common/ssl/context_config_impl.h index 83212fa722e33..f213b387503bc 100644 --- a/source/common/ssl/context_config_impl.h +++ b/source/common/ssl/context_config_impl.h @@ -25,6 +25,14 @@ class ContextConfigImpl : public virtual Ssl::ContextConfig { const std::string& caCertPath() const override { return (ca_cert_path_.empty() && !ca_cert_.empty()) ? INLINE_STRING : ca_cert_path_; } + const std::string& certificateRevocationList() const override { + return certificate_revocation_list_; + } + const std::string& certificateRevocationListPath() const override { + return (certificate_revocation_list_path_.empty() && !certificate_revocation_list_.empty()) + ? INLINE_STRING + : certificate_revocation_list_path_; + } const std::string& certChain() const override { return cert_chain_; } const std::string& certChainPath() const override { return (cert_chain_path_.empty() && !cert_chain_.empty()) ? INLINE_STRING : cert_chain_path_; @@ -60,6 +68,8 @@ class ContextConfigImpl : public virtual Ssl::ContextConfig { const std::string ecdh_curves_; const std::string ca_cert_; const std::string ca_cert_path_; + const std::string certificate_revocation_list_; + const std::string certificate_revocation_list_path_; const std::string cert_chain_; const std::string cert_chain_path_; const std::string private_key_; diff --git a/source/common/ssl/context_impl.cc b/source/common/ssl/context_impl.cc index ea020a25eb85b..6cfc950ba203d 100644 --- a/source/common/ssl/context_impl.cc +++ b/source/common/ssl/context_impl.cc @@ -66,16 +66,18 @@ ContextImpl::ContextImpl(ContextManagerImpl& parent, Stats::Scope& scope, throw EnvoyException( fmt::format("Failed to load trusted CA certificates from {}", config.caCertPath())); } + + X509_STORE* store = SSL_CTX_get_cert_store(ctx_.get()); for (const X509_INFO* item : list.get()) { if (item->x509) { - X509_STORE_add_cert(SSL_CTX_get_cert_store(ctx_.get()), item->x509); + X509_STORE_add_cert(store, item->x509); if (ca_cert_ == nullptr) { X509_up_ref(item->x509); ca_cert_.reset(item->x509); } } if (item->crl) { - X509_STORE_add_crl(SSL_CTX_get_cert_store(ctx_.get()), item->crl); + X509_STORE_add_crl(store, item->crl); } } if (ca_cert_ == nullptr) { @@ -85,6 +87,30 @@ ContextImpl::ContextImpl(ContextManagerImpl& parent, Stats::Scope& scope, verify_mode = SSL_VERIFY_PEER; } + if (!config.certificateRevocationList().empty()) { + bssl::UniquePtr bio( + BIO_new_mem_buf(const_cast(config.certificateRevocationList().data()), + config.certificateRevocationList().size())); + RELEASE_ASSERT(bio != nullptr); + + // Based on BoringSSL's X509_load_cert_crl_file(). + bssl::UniquePtr list( + PEM_X509_INFO_read_bio(bio.get(), nullptr, nullptr, nullptr)); + if (list == nullptr) { + throw EnvoyException( + fmt::format("Failed to load CRL from {}", config.certificateRevocationListPath())); + } + + X509_STORE* store = SSL_CTX_get_cert_store(ctx_.get()); + for (const X509_INFO* item : list.get()) { + if (item->crl) { + X509_STORE_add_crl(store, item->crl); + } + } + + X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL); + } + if (!config.verifySubjectAltNameList().empty()) { verify_subject_alt_name_list_ = config.verifySubjectAltNameList(); verify_mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; diff --git a/test/common/ssl/context_impl_test.cc b/test/common/ssl/context_impl_test.cc index b842a5a09ccc9..cd1d73a8c625f 100644 --- a/test/common/ssl/context_impl_test.cc +++ b/test/common/ssl/context_impl_test.cc @@ -9,6 +9,7 @@ #include "test/common/ssl/ssl_certs_test.h" #include "test/mocks/runtime/mocks.h" #include "test/test_common/environment.h" +#include "test/test_common/utility.h" #include "gtest/gtest.h" @@ -284,5 +285,45 @@ TEST_F(SslServerContextImplTicketTest, TicketKeyInlineStringFailTooSmall) { EXPECT_THROW(loadConfigV2(cfg), EnvoyException); } +TEST_F(SslServerContextImplTicketTest, CRLSuccess) { + std::string json = R"EOF( + { + "cert_chain_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_cert.pem", + "private_key_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_key.pem", + "ca_cert_file": "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.pem", + "crl_file": "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.crl" + } + )EOF"; + + EXPECT_NO_THROW(loadConfigJson(json)); +} + +TEST_F(SslServerContextImplTicketTest, CRLInvalid) { + std::string json = R"EOF( + { + "cert_chain_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_cert.pem", + "private_key_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_key.pem", + "ca_cert_file": "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.pem", + "crl_file": "{{ test_rundir }}/test/common/ssl/test_data/not_a_crl.crl" + } + )EOF"; + + EXPECT_THROW_WITH_REGEX(loadConfigJson(json), EnvoyException, + "^Failed to load CRL from .*/not_a_crl.crl$"); +} + +TEST_F(SslServerContextImplTicketTest, CRLWithNoCA) { + std::string json = R"EOF( + { + "cert_chain_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_cert.pem", + "private_key_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_key.pem", + "crl_file": "{{ test_rundir }}/test/common/ssl/test_data/not_a_crl.crl" + } + )EOF"; + + EXPECT_THROW_WITH_REGEX(loadConfigJson(json), EnvoyException, + "^Failed to load CRL from .* without trusted CA certificates$"); +} + } // namespace Ssl } // namespace Envoy diff --git a/test/common/ssl/ssl_socket_test.cc b/test/common/ssl/ssl_socket_test.cc index 5a87edd4230b3..f1bde2f0fe41e 100644 --- a/test/common/ssl/ssl_socket_test.cc +++ b/test/common/ssl/ssl_socket_test.cc @@ -1842,6 +1842,37 @@ TEST_P(SslSocketTest, SniProtocolVersions) { "ssl.handshake", 2, GetParam()); } +TEST_P(SslSocketTest, RevokedCertificate) { + std::string server_ctx_json = R"EOF( + { + "cert_chain_file": "{{ test_tmpdir }}/unittestcert.pem", + "private_key_file": "{{ test_tmpdir }}/unittestkey.pem", + "ca_cert_file": "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.pem", + "crl_file": "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.crl" + } + )EOF"; + + // This should fail, since the certificate has been revoked. + std::string revoked_client_ctx_json = R"EOF( + { + "cert_chain_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_cert.pem", + "private_key_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_key.pem" + } + )EOF"; + testUtil(revoked_client_ctx_json, server_ctx_json, "", "", "", "", "", "ssl.fail_verify_error", + false, GetParam()); + + // This should succeed, since the cert isn't revoked. + std::string successful_client_ctx_json = R"EOF( + { + "cert_chain_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_cert2.pem", + "private_key_file": "{{ test_rundir }}/test/common/ssl/test_data/san_dns_key2.pem" + } + )EOF"; + testUtil(successful_client_ctx_json, server_ctx_json, "", "", "", "", "", "ssl.handshake", true, + GetParam()); +} + class SslReadBufferLimitTest : public SslCertsTest, public testing::WithParamInterface { public: diff --git a/test/common/ssl/test_data/README.md b/test/common/ssl/test_data/README.md index 5b8dcf372fc2c..46fb133641b9a 100644 --- a/test/common/ssl/test_data/README.md +++ b/test/common/ssl/test_data/README.md @@ -2,7 +2,8 @@ There are 10 identities: - **CA**: Certificate Authority for **No SAN**, **SAN With URI** and **SAN With DNS**. It has the self-signed certificate *ca_cert.pem*. *ca_key.pem* is its - private key. + private key. Additionally, we create a CRL for this CA (*ca_cert.crl*) that + revokes the certificate *san_dns_cert.pem*. - **Intermediate CA**: Intermediate Certificate Authority, signed by the **CA**. It has the certificate *intermediate_ca_cert.pem". *intermediate_ca_key.pem* is its private key. diff --git a/test/common/ssl/test_data/ca_cert.cfg b/test/common/ssl/test_data/ca_cert.cfg index 52f9521628f05..55f6f2832db8b 100644 --- a/test/common/ssl/test_data/ca_cert.cfg +++ b/test/common/ssl/test_data/ca_cert.cfg @@ -27,3 +27,19 @@ basicConstraints = critical, CA:TRUE keyUsage = critical, cRLSign, keyCertSign subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always + +[ca] +default_ca = CA_default + +[CA_default] +database = crl_index.txt +crlnumber = crl_number + +default_days = 3650 +default_crl_days = 3650 +default_md = sha256 +preserve = no +unique_subject = no + +[crl_ext] +authorityKeyIdentifier = keyid:always,issuer:always diff --git a/test/common/ssl/test_data/ca_cert.crl b/test/common/ssl/test_data/ca_cert.crl new file mode 100644 index 0000000000000..322daf1c122ec --- /dev/null +++ b/test/common/ssl/test_data/ca_cert.crl @@ -0,0 +1,10 @@ +-----BEGIN X509 CRL----- +MIIBbDCB1gIBATANBgkqhkiG9w0BAQsFADB2MQswCQYDVQQGEwJVUzETMBEGA1UE +CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChME +THlmdDEZMBcGA1UECxMQTHlmdCBFbmdpbmVlcmluZzEQMA4GA1UEAxMHVGVzdCBD +QRcNMTgwMTI0MjI1MzUxWhcNMjgwMTIyMjI1MzUxWjAcMBoCCQDzgo6yT9d50BcN +MTgwMTI0MjI1MzQ0WqAOMAwwCgYDVR0UBAMCAQAwDQYJKoZIhvcNAQELBQADgYEA +C5J09IOxdXzNEhkxgBu5uptb/l5NCZ3ajf1dYUkQsd0v7JBIx/LOz5XtluXJlet7 +OCCFs4a9UmCFGJoGgSAKTJX/FckprBUTnqRBfKxqGuJ/mM0ff+dMuu75yapvBrIT +ys5oVHYIdL8rk0SyIvmx20/hhu5g5AGL35Wku2YVCR4= +-----END X509 CRL----- diff --git a/test/common/ssl/test_data/certs.sh b/test/common/ssl/test_data/certs.sh index a442f31a5500a..ab7832afdfdbe 100755 --- a/test/common/ssl/test_data/certs.sh +++ b/test/common/ssl/test_data/certs.sh @@ -68,6 +68,14 @@ openssl req -new -x509 -days 730 -key selfsigned_key.pem -out selfsigned_cert.pe openssl req -new -key expired_key.pem -out expired_cert.csr -config selfsigned_cert.cfg -batch -sha256 openssl x509 -req -days -365 -in expired_cert.csr -signkey expired_key.pem -out expired_cert.pem +# Initialize information for CRL process +touch crl_index.txt crl_index.txt.attr +echo 00 > crl_number + +# Revoke the certificate and generate a CRL +openssl ca -revoke san_dns_cert.pem -keyfile ca_key.pem -cert ca_cert.pem -config ca_cert.cfg +openssl ca -gencrl -keyfile ca_key.pem -cert ca_cert.pem -out ca_cert.crl -config ca_cert.cfg + # Write session ticket key files openssl rand 80 > ticket_key_a openssl rand 80 > ticket_key_b @@ -75,3 +83,4 @@ openssl rand 79 > ticket_key_wrong_len rm *csr rm *srl +rm crl_* diff --git a/test/common/ssl/test_data/not_a_crl.crl b/test/common/ssl/test_data/not_a_crl.crl new file mode 100644 index 0000000000000..e2c83b8d3c1cd --- /dev/null +++ b/test/common/ssl/test_data/not_a_crl.crl @@ -0,0 +1,3 @@ +-----BEGIN X509 CRL----- +TOTALLY_NOT_A_CRL_HERE +-----END X509 CRL----- diff --git a/test/server/listener_manager_impl_test.cc b/test/server/listener_manager_impl_test.cc index 7009f4902d3b3..aa8a088a7d6e9 100644 --- a/test/server/listener_manager_impl_test.cc +++ b/test/server/listener_manager_impl_test.cc @@ -1295,5 +1295,89 @@ TEST_F(ListenerManagerImplWithRealFiltersTest, OriginalDstTestFilter) { EXPECT_EQ("127.0.0.2:2345", socket.localAddress()->asString()); } +TEST_F(ListenerManagerImplWithRealFiltersTest, CRLFilename) { + const std::string yaml = TestEnvironment::substitute(R"EOF( + address: + socket_address: { address: 127.0.0.1, port_value: 1234 } + filter_chains: + - tls_context: + common_tls_context: + tls_certificates: + - certificate_chain: { filename: "{{ test_rundir }}/test/common/ssl/test_data/san_dns_cert.pem" } + private_key: { filename: "{{ test_rundir }}/test/common/ssl/test_data/san_dns_key.pem" } + validation_context: + trusted_ca: { filename: "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.pem" } + crl: { filename: "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.crl" } + )EOF", + Network::Address::IpVersion::v4); + + EXPECT_CALL(server_.random_, uuid()); + EXPECT_CALL(listener_factory_, createListenSocket(_, true)); + manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), true); + EXPECT_EQ(1U, manager_->listeners().size()); +} + +TEST_F(ListenerManagerImplWithRealFiltersTest, CRLInline) { + const std::string yaml = TestEnvironment::substitute(R"EOF( + address: + socket_address: { address: 127.0.0.1, port_value: 1234 } + filter_chains: + - tls_context: + common_tls_context: + tls_certificates: + - certificate_chain: { filename: "{{ test_rundir }}/test/common/ssl/test_data/san_dns_cert.pem" } + private_key: { filename: "{{ test_rundir }}/test/common/ssl/test_data/san_dns_key.pem" } + validation_context: + trusted_ca: { filename: "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.pem" } + crl: { inline_string: "-----BEGIN X509 CRL-----\nMIIBbDCB1gIBATANBgkqhkiG9w0BAQsFADB2MQswCQYDVQQGEwJVUzETMBEGA1UE\nCBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzENMAsGA1UEChME\nTHlmdDEZMBcGA1UECxMQTHlmdCBFbmdpbmVlcmluZzEQMA4GA1UEAxMHVGVzdCBD\nQRcNMTcxMjIwMTcxNDA4WhcNMjcxMjE4MTcxNDA4WjAcMBoCCQDZy/Qp7iAfHxcN\nMTcxMjIwMTcxMjU0WqAOMAwwCgYDVR0UBAMCAQAwDQYJKoZIhvcNAQELBQADgYEA\nOTn5Fgb44xtFd9QGtbTElZ3iwdlcOxRHjgQMd+ydzEEZRMzMgb4/NmEsgXAsxbrx\ntKmpgll8TblscitkglvGk8s4obi/OtgxNIvn+7pOBTjmrgJkcktBUDEWRbLZjsZx\nyH+5teBZ0tH0tVy914QeGitZFV8awK1hlJwlAz9g/jo=\n-----END X509 CRL-----" } + )EOF", + Network::Address::IpVersion::v4); + + EXPECT_CALL(server_.random_, uuid()); + EXPECT_CALL(listener_factory_, createListenSocket(_, true)); + manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), true); + EXPECT_EQ(1U, manager_->listeners().size()); +} + +TEST_F(ListenerManagerImplWithRealFiltersTest, InvalidCRLInline) { + const std::string yaml = TestEnvironment::substitute(R"EOF( + address: + socket_address: { address: 127.0.0.1, port_value: 1234 } + filter_chains: + - tls_context: + common_tls_context: + tls_certificates: + - certificate_chain: { filename: "{{ test_rundir }}/test/common/ssl/test_data/san_dns_cert.pem" } + private_key: { filename: "{{ test_rundir }}/test/common/ssl/test_data/san_dns_key.pem" } + validation_context: + trusted_ca: { filename: "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.pem" } + crl: { inline_string: "-----BEGIN X509 CRL-----\nTOTALLY_NOT_A_CRL_HERE\n-----END X509 CRL-----\n" } + )EOF", + Network::Address::IpVersion::v4); + + EXPECT_THROW_WITH_MESSAGE(manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), true), + EnvoyException, "Failed to load CRL from "); +} + +TEST_F(ListenerManagerImplWithRealFiltersTest, CRLWithNoCA) { + const std::string yaml = TestEnvironment::substitute(R"EOF( + address: + socket_address: { address: 127.0.0.1, port_value: 1234 } + filter_chains: + - tls_context: + common_tls_context: + tls_certificates: + - certificate_chain: { filename: "{{ test_rundir }}/test/common/ssl/test_data/san_dns_cert.pem" } + private_key: { filename: "{{ test_rundir }}/test/common/ssl/test_data/san_dns_key.pem" } + validation_context: + crl: { filename: "{{ test_rundir }}/test/common/ssl/test_data/ca_cert.crl" } + )EOF", + Network::Address::IpVersion::v4); + + EXPECT_THROW_WITH_REGEX(manager_->addOrUpdateListener(parseListenerFromV2Yaml(yaml), true), + EnvoyException, + "^Failed to load CRL from .* without trusted CA certificates$"); +} + } // namespace Server } // namespace Envoy