From 6b07e7d192b8b8cf9cfe4154d9110e9e95519553 Mon Sep 17 00:00:00 2001 From: danzh Date: Tue, 18 Oct 2022 08:36:06 -0400 Subject: [PATCH] cert validation: use Android cert validation APIs (#2525) Description: add engine API to allow user config to use Android cert validation APIs. Risk Level: high Testing: added tests in Http2TestServerTest.java Docs Changes: Release Notes: Fixes #1575 Part of #2144 --- docs/root/api/starting_envoy.rst | 12 ++ docs/root/intro/version_history.rst | 1 + envoy_build_config/BUILD | 1 + envoy_build_config/extension_registry.cc | 2 + .../extensions_build_config.bzl | 1 + library/cc/BUILD | 1 + library/cc/engine_builder.cc | 19 ++ library/cc/engine_builder.h | 2 + library/common/config/config.cc | 107 ++++++----- library/common/config/templates.h | 12 ++ .../cert_validator/platform_bridge/BUILD | 48 +++++ .../cert_validator/platform_bridge/c_types.h | 42 +++++ .../cert_validator/platform_bridge/config.cc | 25 +++ .../cert_validator/platform_bridge/config.h | 30 ++++ .../platform_bridge_cert_validator.cc | 167 ++++++++++++++++++ .../platform_bridge_cert_validator.h | 122 +++++++++++++ library/common/jni/BUILD | 23 +++ library/common/jni/android_jni_interface.cc | 8 + library/common/jni/android_network_utility.cc | 149 ++++++++++++++++ library/common/jni/android_network_utility.h | 21 +++ .../common/jni/android_test_jni_interface.cc | 5 +- library/common/jni/jni_interface.cc | 101 ++--------- .../engine/EnvoyConfiguration.java | 11 +- .../envoymobile/engine/EnvoyEngineImpl.java | 4 +- .../envoymobile/engine/JniLibrary.java | 8 + .../chromium/net/AndroidNetworkLibrary.java | 16 +- .../java/org/chromium/net/FakeX509Util.java | 5 +- .../impl/NativeCronetEngineBuilderImpl.java | 4 +- .../envoyproxy/envoymobile/EngineBuilder.kt | 19 +- library/objective-c/EnvoyConfiguration.m | 4 + test/cc/unit/envoy_config_test.cc | 53 ++++-- test/common/jni/BUILD | 1 + .../engine/EnvoyConfigurationTest.kt | 35 +++- .../net/CertificateVerificationTest.java | 3 +- .../chromium/net/testing/CronetTestRule.java | 3 + .../net/testing/Http2TestServerTest.java | 123 ++++++++++++- 36 files changed, 1009 insertions(+), 179 deletions(-) create mode 100644 library/common/extensions/cert_validator/platform_bridge/BUILD create mode 100644 library/common/extensions/cert_validator/platform_bridge/c_types.h create mode 100644 library/common/extensions/cert_validator/platform_bridge/config.cc create mode 100644 library/common/extensions/cert_validator/platform_bridge/config.h create mode 100644 library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.cc create mode 100644 library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h create mode 100644 library/common/jni/android_network_utility.cc create mode 100644 library/common/jni/android_network_utility.h diff --git a/docs/root/api/starting_envoy.rst b/docs/root/api/starting_envoy.rst index 1d23570163..f3a5349daa 100644 --- a/docs/root/api/starting_envoy.rst +++ b/docs/root/api/starting_envoy.rst @@ -514,6 +514,18 @@ to use IPv6. Note this is an experimental option and should be enabled with caut builder.forceIPv6(true) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``enablePlatformCertificatesValidation`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Specify whether to use platform provided certificate validation interfaces. Currently only supported on Android. Defaults to false. + +**Example**:: + + // Kotlin + builder.enablePlatformCertificatesValidation(true) + + ~~~~~~~~~~~~~~~~~~~~~~~~~~ ``enableProxying`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 6d102cbb22..3f7fda8786 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -16,6 +16,7 @@ Bugfixes: Features: +- kotlin/c++: add option to support platform provided certificates validation interfaces on Android. (:issue `#2144 <2144>`) - api: Add a ``setPerTryIdleTimeoutSeconds()`` method to C++ EngineBuilder. - kotlin: add a way to tell Envoy Mobile to respect system proxy settings by calling an ``enableProxying(true)`` method on the engine builder. (:issue:`#2416 <2416>`) - kotlin: add a ``enableSkipDNSLookupForProxiedRequests(true)`` knob for controlling whether Envoy waits on DNS response in the dynamic forward proxy filter for proxied requests. (:issue:`#2602 <2602>`) diff --git a/envoy_build_config/BUILD b/envoy_build_config/BUILD index b59a7dd67c..8c9337320f 100644 --- a/envoy_build_config/BUILD +++ b/envoy_build_config/BUILD @@ -33,6 +33,7 @@ envoy_cc_library( "@envoy//source/extensions/transport_sockets/tls:config", "@envoy//source/extensions/transport_sockets/tls/cert_validator:cert_validator_lib", "@envoy//source/extensions/upstreams/http/generic:config", + "@envoy_mobile//library/common/extensions/cert_validator/platform_bridge:config", "@envoy_mobile//library/common/extensions/filters/http/assertion:config", "@envoy_mobile//library/common/extensions/filters/http/local_error:config", "@envoy_mobile//library/common/extensions/filters/http/network_configuration:config", diff --git a/envoy_build_config/extension_registry.cc b/envoy_build_config/extension_registry.cc index 6e0717e513..6110ba48a5 100644 --- a/envoy_build_config/extension_registry.cc +++ b/envoy_build_config/extension_registry.cc @@ -23,6 +23,7 @@ #include "source/extensions/upstreams/http/generic/config.h" #include "extension_registry_platform_additions.h" +#include "library/common/extensions/cert_validator/platform_bridge/config.h" #include "library/common/extensions/filters/http/assertion/config.h" #include "library/common/extensions/filters/http/local_error/config.h" #include "library/common/extensions/filters/http/network_configuration/config.h" @@ -63,6 +64,7 @@ void ExtensionRegistry::registerFactories() { Envoy::Extensions::TransportSockets::Http11Connect:: forceRegisterUpstreamHttp11ConnectSocketConfigFactory(); Envoy::Extensions::TransportSockets::Tls::forceRegisterDefaultCertValidatorFactory(); + Envoy::Extensions::TransportSockets::Tls::forceRegisterPlatformBridgeCertValidatorFactory(); Envoy::Extensions::Upstreams::Http::Generic::forceRegisterGenericGenericConnPoolFactory(); Envoy::Upstream::forceRegisterLogicalDnsClusterFactory(); ExtensionRegistryPlatformAdditions::registerFactories(); diff --git a/envoy_build_config/extensions_build_config.bzl b/envoy_build_config/extensions_build_config.bzl index d8b18b0db0..33eee6578b 100644 --- a/envoy_build_config/extensions_build_config.bzl +++ b/envoy_build_config/extensions_build_config.bzl @@ -25,5 +25,6 @@ EXTENSIONS = { "envoy.transport_sockets.raw_buffer": "//source/extensions/transport_sockets/raw_buffer:config", "envoy.transport_sockets.tls": "//source/extensions/transport_sockets/tls:config", "envoy.http.stateful_header_formatters.preserve_case": "//source/extensions/http/header_formatters/preserve_case:config", + "envoy_mobile.cert_validator.platform_bridge_cert_validator": "@envoy_mobile//library/common/extensions/cert_validator/platform_bridge:config", } WINDOWS_EXTENSIONS = {} diff --git a/library/cc/BUILD b/library/cc/BUILD index b6d8b9acc0..94111008ce 100644 --- a/library/cc/BUILD +++ b/library/cc/BUILD @@ -15,6 +15,7 @@ envoy_cc_library( repository = "@envoy", deps = [ ":envoy_engine_cc_lib_no_stamp", + "@envoy//source/common/common:assert_lib", ], ) diff --git a/library/cc/engine_builder.cc b/library/cc/engine_builder.cc index 61584731a0..7a97868438 100644 --- a/library/cc/engine_builder.cc +++ b/library/cc/engine_builder.cc @@ -2,6 +2,8 @@ #include +#include "source/common/common/assert.h" + #include "absl/strings/str_replace.h" #include "fmt/core.h" #include "library/common/main_interface.h" @@ -129,6 +131,17 @@ EngineBuilder& EngineBuilder::enableSocketTagging(bool socket_tagging_on) { return *this; } +EngineBuilder& +EngineBuilder::enablePlatformCertificatesValidation(bool platform_certificates_validation_on) { +#if defined(__APPLE__) + if (platform_certificates_validation_on) { + PANIC("Certificates validation using platform provided APIs is not supported in IOS."); + } +#endif + this->platform_certificates_validation_on_ = platform_certificates_validation_on; + return *this; +} + std::string EngineBuilder::generateConfigStr() { #if defined(__APPLE__) std::string dns_resolver_name = "envoy.network.dns_resolver.apple"; @@ -185,6 +198,12 @@ std::string EngineBuilder::generateConfigStr() { for (const auto& [key, value] : replacements) { config_builder << "- &" << key << " " << value << std::endl; } + + const std::string& cert_validation_template = + (this->platform_certificates_validation_on_ ? platform_cert_validation_context_template + : default_cert_validation_context_template); + config_builder << cert_validation_template << std::endl; + if (this->gzip_filter_) { absl::StrReplaceAll( {{"#{custom_filters}", absl::StrCat("#{custom_filters}\n", gzip_config_insert)}}, diff --git a/library/cc/engine_builder.h b/library/cc/engine_builder.h index 4b7061fcef..bb7573afc3 100644 --- a/library/cc/engine_builder.h +++ b/library/cc/engine_builder.h @@ -42,6 +42,7 @@ class EngineBuilder { EngineBuilder& enableGzip(bool gzip_on); EngineBuilder& enableBrotli(bool brotli_on); EngineBuilder& enableSocketTagging(bool socket_tagging_on); + EngineBuilder& enablePlatformCertificatesValidation(bool platform_certificates_validation_on); // this is separated from build() for the sake of testability std::string generateConfigStr(); @@ -84,6 +85,7 @@ class EngineBuilder { bool gzip_filter_ = true; bool brotli_filter_ = false; bool socket_tagging_filter_ = false; + bool platform_certificates_validation_on_ = false; absl::flat_hash_map key_value_stores_{}; diff --git a/library/common/config/config.cc b/library/common/config/config.cc index 62a4249b3a..d7feba78e4 100644 --- a/library/common/config/config.cc +++ b/library/common/config/config.cc @@ -75,6 +75,20 @@ const char* brotli_config_insert = R"( ignore_no_transform_header: true )"; +const char* default_cert_validation_context_template = R"( +- &validation_context + trusted_ca: + inline_string: *tls_root_certs + trust_chain_verification: *trust_chain_verification +)"; + +const char* platform_cert_validation_context_template = R"( +- &validation_context + custom_validator_config: + name: "envoy_mobile.cert_validator.platform_bridge_cert_validator" + trust_chain_verification: *trust_chain_verification +)"; + const char* socket_tag_config_insert = R"( - name: envoy.filters.http.socket_tag typed_config: @@ -143,21 +157,11 @@ R"(- &enable_drain_post_dns_refresh false #include "certificates.inc" R"( -!ignore tls_socket_defs: -- &base_tls_socket - name: envoy.transport_sockets.http_11_proxy - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport - transport_socket: - name: envoy.transport_sockets.tls - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext - common_tls_context: - tls_params: - tls_maximum_protocol_version: TLSv1_3 - validation_context: - trusted_ca: - inline_string: *tls_root_certs +!ignore validation_context_defs: +- &validation_context + trusted_ca: + inline_string: *tls_root_certs + trust_chain_verification: *trust_chain_verification )"; const char* config_template = R"( @@ -272,6 +276,45 @@ const char* config_template = R"( typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router +!ignore tls_socket_defs: +- &base_tls_socket + name: envoy.transport_sockets.http_11_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + tls_params: + tls_maximum_protocol_version: TLSv1_3 + validation_context: *validation_context +- &base_h2_socket + name: envoy.transport_sockets.http_11_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + common_tls_context: + alpn_protocols: [h2] + tls_params: + tls_maximum_protocol_version: TLSv1_3 + validation_context: *validation_context +- &base_h3_socket + name: envoy.transport_sockets.http_11_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport + transport_socket: + name: envoy.transport_sockets.quic + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.quic.v3.QuicUpstreamTransport + upstream_tls_context: + common_tls_context: + tls_params: + tls_maximum_protocol_version: TLSv1_3 + validation_context: *validation_context !ignore custom_cluster_defs: stats_cluster: &stats_cluster name: stats @@ -409,22 +452,7 @@ R"( connect_timeout: *connect_timeout lb_policy: CLUSTER_PROVIDED cluster_type: *base_cluster_type - transport_socket: - name: envoy.transport_sockets.http_11_proxy - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport - transport_socket: - name: envoy.transport_sockets.tls - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext - common_tls_context: - alpn_protocols: [h2] - tls_params: - tls_maximum_protocol_version: TLSv1_3 - validation_context: - trusted_ca: - inline_string: *tls_root_certs - trust_chain_verification: *trust_chain_verification + transport_socket: *base_h2_socket upstream_connection_options: *upstream_opts circuit_breakers: *circuit_breakers_settings typed_extension_protocol_options: *h2_protocol_options @@ -432,22 +460,7 @@ R"( connect_timeout: *connect_timeout lb_policy: CLUSTER_PROVIDED cluster_type: *base_cluster_type - transport_socket: - name: envoy.transport_sockets.http_11_proxy - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.http_11_proxy.v3.Http11ProxyUpstreamTransport - transport_socket: - name: envoy.transport_sockets.quic - typed_config: - "@type": type.googleapis.com/envoy.extensions.transport_sockets.quic.v3.QuicUpstreamTransport - upstream_tls_context: - common_tls_context: - tls_params: - tls_maximum_protocol_version: TLSv1_3 - validation_context: - trusted_ca: - inline_string: *tls_root_certs - trust_chain_verification: *trust_chain_verification + transport_socket: *base_h3_socket upstream_connection_options: *upstream_opts circuit_breakers: *circuit_breakers_settings typed_extension_protocol_options: *h3_protocol_options diff --git a/library/common/config/templates.h b/library/common/config/templates.h index 2e47813c51..eb9c1d53c8 100644 --- a/library/common/config/templates.h +++ b/library/common/config/templates.h @@ -85,3 +85,15 @@ extern const char* socket_tag_config_insert; * direct responses to mutate headers which are then later used for routing. */ extern const char* route_cache_reset_filter_insert; + +/** + * Config template which uses Envoy's built-in certificates validator to verify + * certificate chain. + */ +extern const char* default_cert_validation_context_template; + +/** + * Config template which uses platform's certificates APIs to verify certificate + * chain. + */ +extern const char* platform_cert_validation_context_template; diff --git a/library/common/extensions/cert_validator/platform_bridge/BUILD b/library/common/extensions/cert_validator/platform_bridge/BUILD new file mode 100644 index 0000000000..fb8dc4e616 --- /dev/null +++ b/library/common/extensions/cert_validator/platform_bridge/BUILD @@ -0,0 +1,48 @@ +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "c_types_lib", + hdrs = ["c_types.h"], + repository = "@envoy", + deps = [ + "//library/common/data:utility_lib", + ], +) + +envoy_cc_library( + name = "platform_bridge_cert_validator_lib", + srcs = ["platform_bridge_cert_validator.cc"], + hdrs = [ + "platform_bridge_cert_validator.h", + ], + repository = "@envoy", + deps = [ + ":c_types_lib", + "@envoy//source/extensions/transport_sockets/tls/cert_validator:cert_validator_lib", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = [ + "c_types.h", + "config.h", + ], + repository = "@envoy", + deps = [ + ":platform_bridge_cert_validator_lib", + "//library/common/api:external_api_lib", + "//library/common/data:utility_lib", + "@envoy//envoy/registry", + ], +) diff --git a/library/common/extensions/cert_validator/platform_bridge/c_types.h b/library/common/extensions/cert_validator/platform_bridge/c_types.h new file mode 100644 index 0000000000..3925ebe13b --- /dev/null +++ b/library/common/extensions/cert_validator/platform_bridge/c_types.h @@ -0,0 +1,42 @@ +#pragma once + +#include "library/common/types/c_types.h" + +// NOLINT(namespace-envoy) + +/** + * Certification validation binary result with corresponding boring SSL alert + * and error details if the result indicates failure. + */ +typedef struct { + envoy_status_t result; + uint8_t tls_alert; + const char* error_details; +} envoy_cert_validation_result; + +#ifdef __cplusplus +extern "C" { // function pointers +#endif + +/** + * Function signature for calling into platform APIs to validate certificates. + */ +typedef envoy_cert_validation_result (*envoy_validate_cert_f)(const envoy_data* certs, uint8_t size, + const char* host_name); + +/** + * Function signature for calling into platform APIs to clean up after validation completion. + */ +typedef void (*envoy_release_validator_f)(); + +#ifdef __cplusplus +} // function pointers +#endif + +/** + * A bag of function pointers to be registered in the platform registry. + */ +typedef struct { + envoy_validate_cert_f validate_cert; + envoy_release_validator_f release_validator; +} envoy_cert_validator; diff --git a/library/common/extensions/cert_validator/platform_bridge/config.cc b/library/common/extensions/cert_validator/platform_bridge/config.cc new file mode 100644 index 0000000000..187457a5fc --- /dev/null +++ b/library/common/extensions/cert_validator/platform_bridge/config.cc @@ -0,0 +1,25 @@ +#include "library/common/extensions/cert_validator/platform_bridge/config.h" + +#include "library/common/api/external.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { + +CertValidatorPtr PlatformBridgeCertValidatorFactory::createCertValidator( + const Envoy::Ssl::CertificateValidationContextConfig* config, SslStats& stats, + TimeSource& /*time_source*/) { + if (platform_validator_ == nullptr) { + platform_validator_ = + static_cast(Api::External::retrieveApi("platform_cert_validator")); + } + return std::make_unique(config, stats, platform_validator_); +} + +REGISTER_FACTORY(PlatformBridgeCertValidatorFactory, CertValidatorFactory); + +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/cert_validator/platform_bridge/config.h b/library/common/extensions/cert_validator/platform_bridge/config.h new file mode 100644 index 0000000000..51b9865bb6 --- /dev/null +++ b/library/common/extensions/cert_validator/platform_bridge/config.h @@ -0,0 +1,30 @@ +#include "envoy/registry/registry.h" + +#include "source/extensions/transport_sockets/tls/cert_validator/factory.h" + +#include "library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { + +class PlatformBridgeCertValidatorFactory : public CertValidatorFactory { +public: + CertValidatorPtr createCertValidator(const Envoy::Ssl::CertificateValidationContextConfig* config, + SslStats& stats, TimeSource& time_source) override; + + std::string name() const override { + return "envoy_mobile.cert_validator.platform_bridge_cert_validator"; + } + +private: + const envoy_cert_validator* platform_validator_ = nullptr; +}; + +DECLARE_FACTORY(PlatformBridgeCertValidatorFactory); + +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.cc b/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.cc new file mode 100644 index 0000000000..fa27a747fb --- /dev/null +++ b/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.cc @@ -0,0 +1,167 @@ +#include "library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h" + +#include +#include +#include + +#include "library/common/data/utility.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { + +PlatformBridgeCertValidator::PlatformBridgeCertValidator( + const Envoy::Ssl::CertificateValidationContextConfig* config, SslStats& stats, + const envoy_cert_validator* platform_validator) + : config_(config), stats_(stats), platform_validator_(platform_validator) { + ENVOY_BUG(config != nullptr && config->caCert().empty() && + config->certificateRevocationList().empty(), + "Invalid certificate validation context config."); + if (config_ != nullptr) { + allow_untrusted_certificate_ = config_->trustChainVerification() == + envoy::extensions::transport_sockets::tls::v3:: + CertificateValidationContext::ACCEPT_UNTRUSTED; + } +} + +PlatformBridgeCertValidator::~PlatformBridgeCertValidator() { + // Wait for validation threads to finish. + for (auto& [id, thread] : validation_threads_) { + if (thread.joinable()) { + thread.join(); + } + } +} + +int PlatformBridgeCertValidator::initializeSslContexts(std::vector /*contexts*/, + bool /*handshaker_provides_certificates*/) { + return SSL_VERIFY_PEER; +} + +ValidationResults PlatformBridgeCertValidator::doVerifyCertChain( + STACK_OF(X509) & cert_chain, Ssl::ValidateResultCallbackPtr callback, + Ssl::SslExtendedSocketInfo* ssl_extended_info, + const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options, + SSL_CTX& /*ssl_ctx*/, const CertValidator::ExtraValidationContext& /*validation_context*/, + bool is_server, absl::string_view host_name) { + ASSERT(!is_server); + if (sk_X509_num(&cert_chain) == 0) { + if (ssl_extended_info) { + ssl_extended_info->setCertificateValidationStatus( + Envoy::Ssl::ClientValidationStatus::NotValidated); + } + const char* error = "verify cert chain failed: empty cert chain."; + stats_.fail_verify_error_.inc(); + ENVOY_LOG(debug, error); + return {ValidationResults::ValidationStatus::Failed, absl::nullopt, error}; + } + if (callback == nullptr) { + callback = ssl_extended_info->createValidateResultCallback(); + } + + std::vector certs; + for (uint64_t i = 0; i < sk_X509_num(&cert_chain); i++) { + X509* cert = sk_X509_value(&cert_chain, i); + unsigned char* cert_in_der = nullptr; + int der_length = i2d_X509(cert, &cert_in_der); + ASSERT(der_length > 0 && cert_in_der != nullptr); + + absl::string_view cert_str(reinterpret_cast(cert_in_der), + static_cast(der_length)); + certs.push_back(Data::Utility::copyToBridgeData(cert_str)); + OPENSSL_free(cert_in_der); + } + + absl::string_view host; + if (transport_socket_options != nullptr && + !transport_socket_options->verifySubjectAltNameListOverride().empty()) { + host = transport_socket_options->verifySubjectAltNameListOverride()[0]; + } else { + host = host_name; + } + PendingValidation validation(*this, std::move(certs), host, std::move(transport_socket_options), + std::move(callback)); + auto insert_result = validations_.insert(std::move(validation)); + ASSERT(insert_result.second); + PendingValidation& ref = const_cast(*insert_result.first); + std::thread verification_thread(&PendingValidation::verifyCertsByPlatform, &ref); + std::thread::id thread_id = verification_thread.get_id(); + validation_threads_[thread_id] = std::move(verification_thread); + return {ValidationResults::ValidationStatus::Pending, absl::nullopt, absl::nullopt}; +} + +void PlatformBridgeCertValidator::verifyCertChainByPlatform( + std::vector& cert_chain, const std::string& host_name, + const std::vector& subject_alt_names, PendingValidation& pending_validation) { + ASSERT(!cert_chain.empty()); + ENVOY_LOG(trace, "Start verifyCertChainByPlatform for host {}", host_name); + // This is running in a stand alone thread other than the engine thread. + envoy_data leaf_cert_der = cert_chain[0]; + bssl::UniquePtr leaf_cert(d2i_X509( + nullptr, const_cast(&leaf_cert_der.bytes), leaf_cert_der.length)); + envoy_cert_validation_result result = + platform_validator_->validate_cert(cert_chain.data(), cert_chain.size(), host_name.c_str()); + bool success = result.result == ENVOY_SUCCESS; + if (!success) { + ENVOY_LOG(debug, result.error_details); + pending_validation.postVerifyResultAndCleanUp(/*success=*/allow_untrusted_certificate_, + result.error_details, result.tls_alert, + makeOptRef(stats_.fail_verify_error_)); + return; + } + + absl::string_view error_details; + // Verify that host name matches leaf cert. + success = DefaultCertValidator::verifySubjectAltName(leaf_cert.get(), subject_alt_names); + if (!success) { + error_details = "PlatformBridgeCertValidator_verifySubjectAltName failed: SNI mismatch."; + ENVOY_LOG(debug, error_details); + pending_validation.postVerifyResultAndCleanUp(/*success=*/allow_untrusted_certificate_, + error_details, SSL_AD_BAD_CERTIFICATE, + makeOptRef(stats_.fail_verify_san_)); + return; + } + pending_validation.postVerifyResultAndCleanUp(success, error_details, SSL_AD_CERTIFICATE_UNKNOWN, + {}); +} + +void PlatformBridgeCertValidator::PendingValidation::verifyCertsByPlatform() { + parent_.verifyCertChainByPlatform( + certs_, host_name_, + (transport_socket_options_ != nullptr + ? transport_socket_options_->verifySubjectAltNameListOverride() + : std::vector{host_name_}), + *this); +} + +void PlatformBridgeCertValidator::PendingValidation::postVerifyResultAndCleanUp( + bool success, absl::string_view error_details, uint8_t tls_alert, + OptRef error_counter) { + std::weak_ptr weak_alive_indicator(parent_.alive_indicator_); + result_callback_->dispatcher().post([this, weak_alive_indicator, success, + error = std::string(error_details), tls_alert, error_counter, + thread_id = std::this_thread::get_id()]() { + if (weak_alive_indicator.expired()) { + return; + } + ENVOY_LOG(trace, "Get validation result for {} from platform", host_name_); + parent_.validation_threads_[thread_id].join(); + parent_.validation_threads_.erase(thread_id); + if (error_counter.has_value()) { + const_cast(error_counter.ref()).inc(); + } + result_callback_->onCertValidationResult(success, error, tls_alert); + parent_.validations_.erase(*this); + }); + ENVOY_LOG(trace, + "Finished platform cert validation for {}, post result callback to network thread", + host_name_); + + parent_.platform_validator_->release_validator(); +} + +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h b/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h new file mode 100644 index 0000000000..ffa0c23fd9 --- /dev/null +++ b/library/common/extensions/cert_validator/platform_bridge/platform_bridge_cert_validator.h @@ -0,0 +1,122 @@ +#pragma once + +#include + +#include "source/extensions/transport_sockets/tls/cert_validator/default_validator.h" + +#include "absl/container/flat_hash_map.h" +#include "library/common/extensions/cert_validator/platform_bridge/c_types.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { + +// A certification validation implementation that uses the platform provided APIs to verify +// certificate chain. Since some platform APIs are slow blocking calls, in order not to block +// network events, this implementation creates stand-alone threads to make those calls for each +// validation. +class PlatformBridgeCertValidator : public CertValidator, Logger::Loggable { +public: + PlatformBridgeCertValidator(const Envoy::Ssl::CertificateValidationContextConfig* config, + SslStats& stats, const envoy_cert_validator* platform_validator); + + ~PlatformBridgeCertValidator() override; + + // CertValidator + // These are not very meaningful interfaces for cert validator on client side and are only called + // by server TLS context. Ideally they should be moved from CertValidator into an extended server + // cert validator interface. And this class only extends the client interface. But their owner + // (Tls::ContextImpl) doesn't have endpoint perspective today, so there will need more refactoring + // to achieve this. + void addClientValidationContext(SSL_CTX* /*context*/, bool /*require_client_cert*/) override { + PANIC("Should not be reached"); + } + void updateDigestForSessionId(bssl::ScopedEVP_MD_CTX& /*md*/, + uint8_t* /*hash_buffer[EVP_MAX_MD_SIZE]*/, + unsigned /*hash_length*/) override { + PANIC("Should not be reached"); + } + int doSynchronousVerifyCertChain( + X509_STORE_CTX* /*store_ctx*/, + Ssl::SslExtendedSocketInfo* + /*ssl_extended_info*/, + X509& /*leaf_cert*/, + const Network::TransportSocketOptions* /*transport_socket_options*/) override { + PANIC("Should not be reached"); + } + absl::optional daysUntilFirstCertExpires() const override { return absl::nullopt; } + Envoy::Ssl::CertificateDetailsPtr getCaCertInformation() const override { return nullptr; } + // Return empty string + std::string getCaFileName() const override { return ""; } + // Overridden to call into platform extension API asynchronously. + ValidationResults + doVerifyCertChain(STACK_OF(X509) & cert_chain, Ssl::ValidateResultCallbackPtr callback, + Ssl::SslExtendedSocketInfo* ssl_extended_info, + const Network::TransportSocketOptionsConstSharedPtr& transport_socket_options, + SSL_CTX& ssl_ctx, + const CertValidator::ExtraValidationContext& validation_context, bool is_server, + absl::string_view host_name) override; + // As CA path will not be configured, make sure the return value won’t be SSL_VERIFY_NONE because + // of that, so that doVerifyCertChain() will be called from the TLS stack. + int initializeSslContexts(std::vector contexts, + bool handshaker_provides_certificates) override; + +private: + class PendingValidation { + public: + PendingValidation(PlatformBridgeCertValidator& parent, std::vector certs, + absl::string_view host_name, + const Network::TransportSocketOptionsConstSharedPtr transport_socket_options, + Ssl::ValidateResultCallbackPtr result_callback) + : parent_(parent), certs_(std::move(certs)), host_name_(host_name), + result_callback_(std::move(result_callback)), + transport_socket_options_(std::move(transport_socket_options)) {} + + void verifyCertsByPlatform(); + + void postVerifyResultAndCleanUp(bool success, absl::string_view error_details, + uint8_t tls_alert, OptRef error_counter); + + struct Hash { + size_t operator()(const PendingValidation& p) const { + return reinterpret_cast(p.result_callback_.get()); + } + }; + struct Eq { + bool operator()(const PendingValidation& a, const PendingValidation& b) const { + return a.result_callback_.get() == b.result_callback_.get(); + } + }; + + private: + Event::SchedulableCallbackPtr next_iteration_callback_; + PlatformBridgeCertValidator& parent_; + std::vector certs_; + std::string host_name_; + Ssl::ValidateResultCallbackPtr result_callback_; + const Network::TransportSocketOptionsConstSharedPtr transport_socket_options_; + }; + + // Calls into platform APIs in a stand-alone thread to verify the given certs. + // Once the validation is done, the result will be posted back to the current + // thread to trigger callback and update verify stats. + void verifyCertChainByPlatform(std::vector& cert_chain, const std::string& host_name, + const std::vector& subject_alt_names, + PendingValidation& pending_validation); + + const Envoy::Ssl::CertificateValidationContextConfig* config_; + SslStats& stats_; + bool allow_untrusted_certificate_ = false; + // latches the platform extension API. + const envoy_cert_validator* platform_validator_; + absl::flat_hash_map validation_threads_; + absl::flat_hash_set + validations_; + std::shared_ptr alive_indicator_{new size_t(1)}; +}; + +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/jni/BUILD b/library/common/jni/BUILD index af259b4aa5..444e4d3229 100644 --- a/library/common/jni/BUILD +++ b/library/common/jni/BUILD @@ -41,6 +41,7 @@ cc_library( "//conditions:default": [], }), deps = [ + ":android_network_utility_lib", ":jni_utility_lib", ":ndk_jni_support", "//library/common:envoy_main_interface_lib", @@ -105,6 +106,7 @@ cc_binary( linkshared = True, deps = [ "base_java_jni_lib", + ":android_network_utility_lib", ":java_jni_support", "//library/common:envoy_main_interface_lib", "//library/common/api:c_types", @@ -125,6 +127,7 @@ cc_library( ], deps = [ "base_java_jni_lib", + ":android_network_utility_lib", ], alwayslink = True, ) @@ -220,3 +223,23 @@ cc_library( "//conditions:default": [], }), ) + +cc_library( + name = "android_network_utility_lib", + srcs = [ + "android_network_utility.cc", + ], + hdrs = [ + "android_network_utility.h", + ], + deps = [ + ":jni_utility_lib", + "//library/common/api:c_types", + "//library/common/data:utility_lib", + "//library/common/extensions/cert_validator/platform_bridge:c_types_lib", + "//library/common/jni/import:jni_import_lib", + "//library/common/types:c_types_lib", + "@envoy//bazel:boringssl", + "@envoy//source/common/common:assert_lib", + ], +) diff --git a/library/common/jni/android_jni_interface.cc b/library/common/jni/android_jni_interface.cc index 504939170f..9965195826 100644 --- a/library/common/jni/android_jni_interface.cc +++ b/library/common/jni/android_jni_interface.cc @@ -1,5 +1,6 @@ #include +#include "library/common/jni/android_network_utility.h" #include "library/common/jni/import/jni_import.h" #include "library/common/jni/jni_support.h" #include "library/common/jni/jni_utility.h" @@ -15,6 +16,13 @@ Java_io_envoyproxy_envoymobile_engine_AndroidJniLibrary_initialize(JNIEnv* env, jobject class_loader, jobject connectivity_manager) { set_class_loader(env->NewGlobalRef(class_loader)); + // At this point, we know Android APIs are available. Register cert chain validation JNI calls. + envoy_status_t result = + register_platform_api(cert_validator_name, get_android_cert_validator_api()); + if (result == ENVOY_FAILURE) { + return ENVOY_FAILURE; + } + // See note above about c-ares. // c-ares jvm init is necessary in order to let c-ares perform DNS resolution in Envoy. // More information can be found at: diff --git a/library/common/jni/android_network_utility.cc b/library/common/jni/android_network_utility.cc new file mode 100644 index 0000000000..8711728646 --- /dev/null +++ b/library/common/jni/android_network_utility.cc @@ -0,0 +1,149 @@ +#include "library/common/jni/android_network_utility.h" + +#include "library/common/data/utility.h" +#include "library/common/jni/jni_support.h" +#include "library/common/jni/jni_utility.h" +#include "openssl/ssl.h" + +// NOLINT(namespace-envoy) + +// Helper functions call into AndroidNetworkLibrary, but they are not platform dependent +// because AndroidNetworkLibray can be called in non-Android platform with mock interfaces. + +bool jvm_cert_is_issued_by_known_root(JNIEnv* env, jobject result) { + jclass jcls_AndroidCertVerifyResult = find_class("org.chromium.net.AndroidCertVerifyResult"); + jmethodID jmid_isIssuedByKnownRoot = + env->GetMethodID(jcls_AndroidCertVerifyResult, "isIssuedByKnownRoot", "()Z"); + ASSERT(jmid_isIssuedByKnownRoot); + bool is_issued_by_known_root = env->CallBooleanMethod(result, jmid_isIssuedByKnownRoot); + env->DeleteLocalRef(jcls_AndroidCertVerifyResult); + return is_issued_by_known_root; +} + +envoy_cert_verify_status_t jvm_cert_get_status(JNIEnv* env, jobject j_result) { + jclass jcls_AndroidCertVerifyResult = find_class("org.chromium.net.AndroidCertVerifyResult"); + jmethodID jmid_getStatus = env->GetMethodID(jcls_AndroidCertVerifyResult, "getStatus", "()I"); + ASSERT(jmid_getStatus); + envoy_cert_verify_status_t result = CERT_VERIFY_STATUS_FAILED; + result = static_cast(env->CallIntMethod(j_result, jmid_getStatus)); + + env->DeleteLocalRef(jcls_AndroidCertVerifyResult); + return result; +} + +jobjectArray jvm_cert_get_certificate_chain_encoded(JNIEnv* env, jobject result) { + jclass jcls_AndroidCertVerifyResult = find_class("org.chromium.net.AndroidCertVerifyResult"); + jmethodID jmid_getCertificateChainEncoded = + env->GetMethodID(jcls_AndroidCertVerifyResult, "getCertificateChainEncoded", "()[[B"); + jobjectArray certificate_chain = + static_cast(env->CallObjectMethod(result, jmid_getCertificateChainEncoded)); + env->DeleteLocalRef(jcls_AndroidCertVerifyResult); + return certificate_chain; +} + +static void ExtractCertVerifyResult(JNIEnv* env, jobject result, envoy_cert_verify_status_t* status, + bool* is_issued_by_known_root, + std::vector* verified_chain) { + *status = jvm_cert_get_status(env, result); + if (*status == CERT_VERIFY_STATUS_OK) { + *is_issued_by_known_root = jvm_cert_is_issued_by_known_root(env, result); + jobjectArray chain_byte_array = jvm_cert_get_certificate_chain_encoded(env, result); + if (chain_byte_array != nullptr) { + JavaArrayOfByteArrayToStringVector(env, chain_byte_array, verified_chain); + } + } +} + +// `auth_type` and `host` are expected to be UTF-8 encoded. +jobject call_jvm_verify_x509_cert_chain(JNIEnv* env, const std::vector& cert_chain, + std::string auth_type, std::string host) { + jni_log("[Envoy]", "jvm_verify_x509_cert_chain"); + jclass jcls_AndroidNetworkLibrary = find_class("org.chromium.net.AndroidNetworkLibrary"); + jmethodID jmid_verifyServerCertificates = + env->GetStaticMethodID(jcls_AndroidNetworkLibrary, "verifyServerCertificates", + "([[B[B[B)Lorg/chromium/net/AndroidCertVerifyResult;"); + jobjectArray chain_byte_array = ToJavaArrayOfByteArray(env, cert_chain); + jbyteArray auth_string = ToJavaByteArray(env, auth_type); + jbyteArray host_string = ToJavaByteArray(env, host); + jobject result = + env->CallStaticObjectMethod(jcls_AndroidNetworkLibrary, jmid_verifyServerCertificates, + chain_byte_array, auth_string, host_string); + env->DeleteLocalRef(chain_byte_array); + env->DeleteLocalRef(auth_string); + env->DeleteLocalRef(host_string); + env->DeleteLocalRef(jcls_AndroidNetworkLibrary); + return result; +} + +// `auth_type` and `host` are expected to be UTF-8 encoded. +static void jvm_verify_x509_cert_chain(const std::vector& cert_chain, + std::string auth_type, std::string host, + envoy_cert_verify_status_t* status, + bool* is_issued_by_known_root, + std::vector* verified_chain) { + JNIEnv* env = get_env(); + jobject result = call_jvm_verify_x509_cert_chain(env, cert_chain, auth_type, host); + if (env->ExceptionCheck() == JNI_TRUE) { + env->ExceptionDescribe(); + env->ExceptionClear(); + *status = CERT_VERIFY_STATUS_NOT_YET_VALID; + } else { + ExtractCertVerifyResult(get_env(), result, status, is_issued_by_known_root, verified_chain); + if (env->ExceptionCheck() == JNI_TRUE) { + env->ExceptionDescribe(); + env->ExceptionClear(); + *status = CERT_VERIFY_STATUS_FAILED; + }; + } + env->DeleteLocalRef(result); +} + +static envoy_cert_validation_result verify_x509_cert_chain(const envoy_data* certs, uint8_t size, + const char* host_name) { + jni_log("[Envoy]", "verify_x509_cert_chain"); + + envoy_cert_verify_status_t result; + bool is_issued_by_known_root; + std::vector verified_chain; + std::vector cert_chain; + for (uint8_t i = 0; i < size; ++i) { + cert_chain.push_back(Envoy::Data::Utility::copyToString(certs[i])); + release_envoy_data(certs[i]); + } + + // Android ignores the authType parameter to X509TrustManager.checkServerTrusted, so pass in "RSA" + // as dummy value. See https://crbug.com/627154. + jvm_verify_x509_cert_chain(cert_chain, "RSA", host_name, &result, &is_issued_by_known_root, + &verified_chain); + switch (result) { + case CERT_VERIFY_STATUS_OK: + return {ENVOY_SUCCESS}; + case CERT_VERIFY_STATUS_EXPIRED: { + return {ENVOY_FAILURE, SSL_AD_CERTIFICATE_EXPIRED, + "AndroidNetworkLibrary_verifyServerCertificates failed: expired cert."}; + } + case CERT_VERIFY_STATUS_NO_TRUSTED_ROOT: + return {ENVOY_FAILURE, SSL_AD_CERTIFICATE_UNKNOWN, + "AndroidNetworkLibrary_verifyServerCertificates failed: no trusted root."}; + case CERT_VERIFY_STATUS_UNABLE_TO_PARSE: + return {ENVOY_FAILURE, SSL_AD_BAD_CERTIFICATE, + "AndroidNetworkLibrary_verifyServerCertificates failed: unable to parse cert."}; + case CERT_VERIFY_STATUS_INCORRECT_KEY_USAGE: + return {ENVOY_FAILURE, SSL_AD_CERTIFICATE_UNKNOWN, + "AndroidNetworkLibrary_verifyServerCertificates failed: incorrect key usage."}; + case CERT_VERIFY_STATUS_FAILED: + return { + ENVOY_FAILURE, SSL_AD_CERTIFICATE_UNKNOWN, + "AndroidNetworkLibrary_verifyServerCertificates failed: validation couldn't be conducted."}; + case CERT_VERIFY_STATUS_NOT_YET_VALID: + return {ENVOY_FAILURE, SSL_AD_CERTIFICATE_UNKNOWN, + "AndroidNetworkLibrary_verifyServerCertificates failed: not yet valid."}; + } +} + +envoy_cert_validator* get_android_cert_validator_api() { + envoy_cert_validator* api = (envoy_cert_validator*)safe_malloc(sizeof(envoy_cert_validator)); + api->validate_cert = verify_x509_cert_chain; + api->release_validator = jvm_detach_thread; + return api; +} diff --git a/library/common/jni/android_network_utility.h b/library/common/jni/android_network_utility.h new file mode 100644 index 0000000000..52366113c1 --- /dev/null +++ b/library/common/jni/android_network_utility.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +#include "library/common/api/c_types.h" +#include "library/common/extensions/cert_validator/platform_bridge/c_types.h" +#include "library/common/jni/import/jni_import.h" + +// NOLINT(namespace-envoy) + +/* Calls up through JNI to validate given certificates. + */ +jobject call_jvm_verify_x509_cert_chain(JNIEnv* env, const std::vector& cert_chain, + std::string auth_type, std::string host); + +/* Returns a group of C functions to do certificates validation using AndroidNetworkLibrary. + */ +envoy_cert_validator* get_android_cert_validator_api(); + +static constexpr const char* cert_validator_name = "platform_cert_validator"; diff --git a/library/common/jni/android_test_jni_interface.cc b/library/common/jni/android_test_jni_interface.cc index 3597f3552c..a04ddae3b1 100644 --- a/library/common/jni/android_test_jni_interface.cc +++ b/library/common/jni/android_test_jni_interface.cc @@ -1,3 +1,4 @@ +#include "library/common/jni/android_network_utility.h" #include "library/common/jni/import/jni_import.h" #include "library/common/jni/jni_support.h" #include "library/common/jni/jni_utility.h" @@ -13,6 +14,6 @@ Java_io_envoyproxy_envoymobile_engine_AndroidJniLibrary_initialize(JNIEnv* env, jobject class_loader, jobject connectivity_manager) { set_class_loader(env->NewGlobalRef(class_loader)); - - return 0; + // At this point, we know Android APIs are available. Register cert chain validation JNI calls. + return register_platform_api(cert_validator_name, get_android_cert_validator_api()); } diff --git a/library/common/jni/jni_interface.cc b/library/common/jni/jni_interface.cc index 38aa58282b..fe503d35de 100644 --- a/library/common/jni/jni_interface.cc +++ b/library/common/jni/jni_interface.cc @@ -1,11 +1,10 @@ #include -#include -#include - #include "library/common/api/c_types.h" +#include "library/common/data/utility.h" #include "library/common/extensions/filters/http/platform_bridge/c_types.h" #include "library/common/extensions/key_value/platform/c_types.h" +#include "library/common/jni/android_network_utility.h" #include "library/common/jni/import/jni_import.h" #include "library/common/jni/jni_support.h" #include "library/common/jni/jni_utility.h" @@ -175,6 +174,17 @@ Java_io_envoyproxy_envoymobile_engine_JniLibrary_socketTagConfigInsert(JNIEnv* e return result; } +extern "C" JNIEXPORT jstring JNICALL +Java_io_envoyproxy_envoymobile_engine_JniLibrary_certValidationTemplate(JNIEnv* env, jclass, + jboolean use_platform) { + if (use_platform == JNI_TRUE) { + jstring result = env->NewStringUTF(platform_cert_validation_context_template); + return result; + } + jstring result = env->NewStringUTF(default_cert_validation_context_template); + return result; +} + extern "C" JNIEXPORT jint JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibrary_recordCounterInc( JNIEnv* env, jclass, // class @@ -1148,91 +1158,6 @@ extern "C" JNIEXPORT jint JNICALL Java_io_envoyproxy_envoymobile_engine_JniLibra return result; } -bool jvm_cert_is_issued_by_known_root(JNIEnv* env, jobject result) { - jclass jcls_AndroidCertVerifyResult = find_class("org.chromium.net.AndroidCertVerifyResult"); - jmethodID jmid_isIssuedByKnownRoot = - env->GetMethodID(jcls_AndroidCertVerifyResult, "isIssuedByKnownRoot", "()Z"); - bool is_issued_by_known_root = - env->CallBooleanMethod(jcls_AndroidCertVerifyResult, jmid_isIssuedByKnownRoot, result); - env->DeleteLocalRef(jcls_AndroidCertVerifyResult); - return is_issued_by_known_root; -} - -envoy_cert_verify_status_t jvm_cert_get_status(JNIEnv* env, jobject j_result) { - jclass jcls_AndroidCertVerifyResult = find_class("org.chromium.net.AndroidCertVerifyResult"); - jmethodID jmid_getStatus = env->GetMethodID(jcls_AndroidCertVerifyResult, "getStatus", "()I"); - envoy_cert_verify_status_t result = static_cast( - env->CallIntMethod(jcls_AndroidCertVerifyResult, jmid_getStatus, j_result)); - env->DeleteLocalRef(jcls_AndroidCertVerifyResult); - return result; -} - -jobjectArray jvm_cert_get_certificate_chain_encoded(JNIEnv* env, jobject result) { - jclass jcls_AndroidCertVerifyResult = find_class("org.chromium.net.AndroidCertVerifyResult"); - jmethodID jmid_getCertificateChainEncoded = - env->GetMethodID(jcls_AndroidCertVerifyResult, "getCertificateChainEncoded", "()[[B"); - jobjectArray certificate_chain = static_cast( - env->CallObjectMethod(jcls_AndroidCertVerifyResult, jmid_getCertificateChainEncoded, result)); - env->DeleteLocalRef(jcls_AndroidCertVerifyResult); - return certificate_chain; -} - -// Once we have a better picture of how Android's certificate verification will -// be plugged into EM, we should decide where this function should really live. -// Context: as of now JNI functions declared in this file are not exported through any -// header files, instead they are stored as callbacks into plain function -// tables. For this reason, this function, which would ideally be defined in -// jni_utility.cc, is currently defined here. -static void ExtractCertVerifyResult(JNIEnv* env, jobject result, envoy_cert_verify_status_t* status, - bool* is_issued_by_known_root, - std::vector* verified_chain) { - *status = jvm_cert_get_status(env, result); - - *is_issued_by_known_root = jvm_cert_is_issued_by_known_root(env, result); - - jobjectArray chain_byte_array = jvm_cert_get_certificate_chain_encoded(env, result); - JavaArrayOfByteArrayToStringVector(env, chain_byte_array, verified_chain); -} - -// `auth_type` and `host` are expected to be UTF-8 encoded. -static jobject call_jvm_verify_x509_cert_chain(JNIEnv* env, - const std::vector& cert_chain, - std::string auth_type, std::string host) { - jni_log("[Envoy]", "jvm_verify_x509_cert_chain"); - jclass jcls_AndroidNetworkLibrary = find_class("org.chromium.net.AndroidNetworkLibrary"); - jmethodID jmid_verifyServerCertificates = - env->GetStaticMethodID(jcls_AndroidNetworkLibrary, "verifyServerCertificates", - "([[B[B[B)Lorg/chromium/net/AndroidCertVerifyResult;"); - - jobjectArray chain_byte_array = ToJavaArrayOfByteArray(env, cert_chain); - jbyteArray auth_string = ToJavaByteArray(env, auth_type); - jbyteArray host_string = ToJavaByteArray(env, host); - jobject result = - env->CallStaticObjectMethod(jcls_AndroidNetworkLibrary, jmid_verifyServerCertificates, - chain_byte_array, auth_string, host_string); - - env->DeleteLocalRef(chain_byte_array); - env->DeleteLocalRef(auth_string); - env->DeleteLocalRef(host_string); - env->DeleteLocalRef(jcls_AndroidNetworkLibrary); - return result; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-function" -// `auth_type` and `host` are expected to be UTF-8 encoded. -static void jvm_verify_x509_cert_chain(const std::vector& cert_chain, - std::string auth_type, std::string host, - envoy_cert_verify_status_t* status, - bool* is_issued_by_known_root, - std::vector* verified_chain) { - JNIEnv* env = get_env(); - jobject result = call_jvm_verify_x509_cert_chain(env, cert_chain, auth_type, host); - ExtractCertVerifyResult(get_env(), result, status, is_issued_by_known_root, verified_chain); - env->DeleteLocalRef(result); -} -#pragma clang diagnostic pop - static void jvm_add_test_root_certificate(const uint8_t* cert, size_t len) { jni_log("[Envoy]", "jvm_add_test_root_certificate"); JNIEnv* env = get_env(); diff --git a/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java b/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java index e19a11d634..4fc1f84027 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java +++ b/library/java/io/envoyproxy/envoymobile/engine/EnvoyConfiguration.java @@ -60,6 +60,7 @@ public enum TrustChainVerification { public final Map stringAccessors; public final Map keyValueStores; public final List statSinks; + public final Boolean enablePlatformCertificatesValidation; public final Boolean enableSkipDNSLookupForProxiedRequests; private static final Pattern UNRESOLVED_KEY_PATTERN = Pattern.compile("\\{\\{ (.+) \\}\\}"); @@ -144,7 +145,7 @@ public EnvoyConfiguration( List httpPlatformFilterFactories, Map stringAccessors, Map keyValueStores, List statSinks, - Boolean enableSkipDNSLookupForProxiedRequests) { + Boolean enableSkipDNSLookupForProxiedRequests, boolean enablePlatformCertificatesValidation) { this.adminInterfaceEnabled = adminInterfaceEnabled; this.grpcStatsDomain = grpcStatsDomain; this.connectTimeoutSeconds = connectTimeoutSeconds; @@ -181,6 +182,7 @@ public EnvoyConfiguration( this.stringAccessors = stringAccessors; this.keyValueStores = keyValueStores; this.statSinks = statSinks; + this.enablePlatformCertificatesValidation = enablePlatformCertificatesValidation; this.enableSkipDNSLookupForProxiedRequests = enableSkipDNSLookupForProxiedRequests; } @@ -199,7 +201,8 @@ public EnvoyConfiguration( String resolveTemplate(final String configTemplate, final String platformFilterTemplate, final String nativeFilterTemplate, final String altProtocolCacheFilterInsert, final String gzipFilterInsert, - final String brotliFilterInsert, final String socketTagFilterInsert) { + final String brotliFilterInsert, final String socketTagFilterInsert, + final String certValidationTemplate) { final StringBuilder customFiltersBuilder = new StringBuilder(); for (EnvoyHTTPFilterFactory filterFactory : httpPlatformFilterFactories) { @@ -225,7 +228,6 @@ String resolveTemplate(final String configTemplate, final String platformFilterT if (enableBrotli) { customFiltersBuilder.append(brotliFilterInsert); } - if (enableSocketTagging) { customFiltersBuilder.append(socketTagFilterInsert); } @@ -311,6 +313,9 @@ String resolveTemplate(final String configTemplate, final String platformFilterT configBuilder.append("] \n"); } + // Add a new anchor to override the default anchors in config header. + configBuilder.append(certValidationTemplate).append("\n"); + if (adminInterfaceEnabled) { configBuilder.append("admin: *admin_interface\n"); } diff --git a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java index 1d180d5800..ce46c1da18 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java +++ b/library/java/io/envoyproxy/envoymobile/engine/EnvoyEngineImpl.java @@ -99,7 +99,9 @@ public int runWithTemplate(String configurationYAML, EnvoyConfiguration envoyCon JniLibrary.nativeFilterTemplate(), JniLibrary.altProtocolCacheFilterInsert(), JniLibrary.gzipConfigInsert(), JniLibrary.brotliConfigInsert(), - JniLibrary.socketTagConfigInsert()), + JniLibrary.socketTagConfigInsert(), + JniLibrary.certValidationTemplate( + envoyConfiguration.enablePlatformCertificatesValidation)), logLevel); } diff --git a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java index 54d6add8b1..44b89a6893 100644 --- a/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java +++ b/library/java/io/envoyproxy/envoymobile/engine/JniLibrary.java @@ -315,6 +315,14 @@ protected static native int recordHistogramValue(long engine, String elements, b */ public static native String brotliConfigInsert(); + /** + * Provides a template to config the certification validator to be used. + * + * @param usePlatform true if the usage of platform validation APIs is desired. + * @return string, the config template string. + */ + public static native String certValidationTemplate(boolean usePlatform); + /** * Provides a configuration insert that may be used to enable socket tagging. */ diff --git a/library/java/org/chromium/net/AndroidNetworkLibrary.java b/library/java/org/chromium/net/AndroidNetworkLibrary.java index 7ea3c22a89..591c46b91e 100644 --- a/library/java/org/chromium/net/AndroidNetworkLibrary.java +++ b/library/java/org/chromium/net/AndroidNetworkLibrary.java @@ -31,6 +31,7 @@ * This class implements net utilities required by the net component. */ public final class AndroidNetworkLibrary { + private static final String TAG = "AndroidNetworkLibrary"; private static boolean mUseFakeCertificateVerification; @@ -41,11 +42,15 @@ public final class AndroidNetworkLibrary { * * @param useFakeCertificateVerification Whether FakeX509Util should be used or not. */ - public static void + public static synchronized void setFakeCertificateVerificationForTesting(boolean useFakeCertificateVerification) { mUseFakeCertificateVerification = useFakeCertificateVerification; } + public static synchronized boolean getFakeCertificateVerificationForTesting() { + return mUseFakeCertificateVerification; + } + /** * Validate the server's certificate chain is trusted. Note that the caller * must still verify the name matches that of the leaf certificate. @@ -57,12 +62,15 @@ public final class AndroidNetworkLibrary { * @param host Bytes representing the UTF-8 encoding of the hostname of the server. * @return Android certificate verification result code. */ - public static AndroidCertVerifyResult - verifyServerCertificates(byte[][] certChain, byte[] authTypeBytes, byte[] hostBytes) { + public static synchronized AndroidCertVerifyResult verifyServerCertificates(byte[][] certChain, + byte[] authTypeBytes, + byte[] hostBytes) { String authType = new String(authTypeBytes, StandardCharsets.UTF_8); String host = new String(hostBytes, StandardCharsets.UTF_8); if (mUseFakeCertificateVerification) { - return FakeX509Util.verifyServerCertificates(certChain, authType, host); + AndroidCertVerifyResult result = + FakeX509Util.verifyServerCertificates(certChain, authType, host); + return result; } try { diff --git a/library/java/org/chromium/net/FakeX509Util.java b/library/java/org/chromium/net/FakeX509Util.java index efdfa36d6f..8327e34d05 100644 --- a/library/java/org/chromium/net/FakeX509Util.java +++ b/library/java/org/chromium/net/FakeX509Util.java @@ -19,7 +19,10 @@ public final class FakeX509Util { private static final Set validFakeCerts = new HashSet(); public static final String expectedAuthType = "RSA"; - public static final String expectedHost = "www.example.com"; + private static String expectedHost = "www.example.com"; + + public static String getExpectedHost() { return expectedHost; } + public static void setExpectedHost(String host) { expectedHost = host; } public static void addTestRootCertificate(byte[] rootCertBytes) { String fakeCertificate = new String(rootCertBytes); diff --git a/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java b/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java index 618f971d0b..f017ed2528 100644 --- a/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java +++ b/library/java/org/chromium/net/impl/NativeCronetEngineBuilderImpl.java @@ -61,6 +61,7 @@ public class NativeCronetEngineBuilderImpl extends CronetEngineBuilderImpl { private String mAppId = "unspecified"; private TrustChainVerification mTrustChainVerification = VERIFY_TRUST_CHAIN; private String mVirtualClusters = "[]"; + private boolean mEnablePlatformCertificatesValidation = true; /** * Builder for Native Cronet Engine. Default config enables SPDY, disables QUIC and HTTP cache. @@ -114,6 +115,7 @@ private EnvoyConfiguration createEnvoyConfiguration() { mH2ConnectionKeepaliveTimeoutSeconds, mH2ExtendKeepaliveTimeout, mMaxConnectionsPerHost, mStatsFlushSeconds, mStreamIdleTimeoutSeconds, mPerTryIdleTimeoutSeconds, mAppVersion, mAppId, mTrustChainVerification, mVirtualClusters, nativeFilterChain, platformFilterChain, - stringAccessors, keyValueStores, statSinks, mEnableSkipDNSLookupForProxiedRequests); + stringAccessors, keyValueStores, statSinks, mEnableSkipDNSLookupForProxiedRequests, + mEnablePlatformCertificatesValidation); } } diff --git a/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt b/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt index bf5efc104f..15784a18d8 100644 --- a/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt +++ b/library/kotlin/io/envoyproxy/envoymobile/EngineBuilder.kt @@ -77,6 +77,7 @@ open class EngineBuilder( private var stringAccessors = mutableMapOf() private var keyValueStores = mutableMapOf() private var statsSinks = listOf() + private var enablePlatformCertificatesValidation = false /** * Add a log level to use with Envoy. @@ -653,7 +654,8 @@ open class EngineBuilder( stringAccessors, keyValueStores, statsSinks, - enableSkipDNSLookupForProxiedRequests + enableSkipDNSLookupForProxiedRequests, + enablePlatformCertificatesValidation ) return when (configuration) { @@ -684,4 +686,19 @@ open class EngineBuilder( this.engineType = engineType return this } + + /** + * Specify whether to use platform provided certificate validation APIs or Envoy built-in + * validation logic. Defaults to false. + * + * @param enablePlatformCertificatesValidation true if using platform APIs is desired. + * + * @return This builder. + */ + fun enablePlatformCertificatesValidation(enablePlatformCertificatesValidation: Boolean): + EngineBuilder { + this.enablePlatformCertificatesValidation = enablePlatformCertificatesValidation + return this + } + } diff --git a/library/objective-c/EnvoyConfiguration.m b/library/objective-c/EnvoyConfiguration.m index 9cde6730dc..27014e15db 100644 --- a/library/objective-c/EnvoyConfiguration.m +++ b/library/objective-c/EnvoyConfiguration.m @@ -192,6 +192,10 @@ - (nullable NSString *)resolveTemplate:(NSString *)templateYAML { [definitions appendFormat:@"- &stats_flush_interval %lus\n", (unsigned long)self.statsFlushSeconds]; + NSString *cert_validator_template = + [[NSString alloc] initWithUTF8String:default_cert_validation_context_template]; + [definitions appendFormat:@"%@\n", cert_validator_template]; + NSMutableArray *stat_sinks_config = [self.statsSinks mutableCopy]; if (self.grpcStatsDomain != nil) { diff --git a/test/cc/unit/envoy_config_test.cc b/test/cc/unit/envoy_config_test.cc index 0a8c6a06b7..aef8fb7de7 100644 --- a/test/cc/unit/envoy_config_test.cc +++ b/test/cc/unit/envoy_config_test.cc @@ -40,21 +40,21 @@ TEST(TestConfig, ConfigIsApplied) { .setDeviceOs("probably-ubuntu-on-CI"); std::string config_str = engine_builder.generateConfigStr(); - std::vector must_contain = { - "- &stats_domain asdf.fake.website", - "- &connect_timeout 123s", - "- &dns_refresh_rate 456s", - "- &dns_fail_base_interval 789s", - "- &dns_fail_max_interval 987s", - "- &dns_query_timeout 321s", - "- &dns_preresolve_hostnames [hostname]", - "- &h2_connection_keepalive_idle_interval 0.222s", - "- &h2_connection_keepalive_timeout 333s", - "- &stats_flush_interval 654s", - "- &virtual_clusters [virtual-clusters]", - ("- &metadata { device_os: probably-ubuntu-on-CI, " - "app_version: 1.2.3, app_id: 1234-1234-1234 }"), - }; + std::vector must_contain = {"- &stats_domain asdf.fake.website", + "- &connect_timeout 123s", + "- &dns_refresh_rate 456s", + "- &dns_fail_base_interval 789s", + "- &dns_fail_max_interval 987s", + "- &dns_query_timeout 321s", + "- &dns_preresolve_hostnames [hostname]", + "- &h2_connection_keepalive_idle_interval 0.222s", + "- &h2_connection_keepalive_timeout 333s", + "- &stats_flush_interval 654s", + "- &virtual_clusters [virtual-clusters]", + ("- &metadata { device_os: probably-ubuntu-on-CI, " + "app_version: 1.2.3, app_id: 1234-1234-1234 }"), + R"(- &validation_context + trusted_ca:)"}; for (const auto& string : must_contain) { ASSERT_NE(config_str.find(string), std::string::npos) << "'" << string << "' not found"; } @@ -184,5 +184,28 @@ TEST(TestConfig, RemainingTemplatesThrows) { } } +TEST(TestConfig, EnablePlatformCertificatesValidation) { + auto engine_builder = EngineBuilder(); + envoy::config::bootstrap::v3::Bootstrap bootstrap; + engine_builder.enablePlatformCertificatesValidation(false); + auto config_str1 = engine_builder.generateConfigStr(); + TestUtility::loadFromYaml(absl::StrCat(config_header, config_str1), bootstrap); + ASSERT_THAT(bootstrap.DebugString(), + Not(HasSubstr("envoy_mobile.cert_validator.platform_bridge_cert_validator"))); + ASSERT_THAT(bootstrap.DebugString(), HasSubstr("trusted_ca")); + +#if not defined(__APPLE__) + engine_builder.enablePlatformCertificatesValidation(true); + auto config_str2 = engine_builder.generateConfigStr(); + TestUtility::loadFromYaml(absl::StrCat(config_header, config_str2), bootstrap); + ASSERT_THAT(bootstrap.DebugString(), + HasSubstr("envoy_mobile.cert_validator.platform_bridge_cert_validator")); + ASSERT_THAT(bootstrap.DebugString(), Not(HasSubstr("trusted_ca"))); +#else + EXPECT_DEATH(engine_builder.enablePlatformCertificatesValidation(true), + "Certificates validation using platform provided APIs is not supported in IOS"); +#endif +} + } // namespace } // namespace Envoy diff --git a/test/common/jni/BUILD b/test/common/jni/BUILD index 8de058294d..9aa969eeb5 100644 --- a/test/common/jni/BUILD +++ b/test/common/jni/BUILD @@ -25,6 +25,7 @@ cc_binary( copts = ["-std=c++17"], linkshared = True, deps = [ + "//library/common/jni:android_network_utility_lib", "//library/common/jni:base_java_jni_lib", "//test/common/integration:quic_test_server_interface_lib", ], diff --git a/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt b/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt index 93c4be82b3..a55898f851 100644 --- a/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt +++ b/test/java/io/envoyproxy/envoymobile/engine/EnvoyConfigurationTest.kt @@ -44,6 +44,12 @@ private const val SOCKET_TAG_INSERT = - name: SocketTag """ +private const val CERT_VALIDATION_TEMPLATE = +""" + custom_validator_config: + name: "dumb_validator" +""" + class EnvoyConfigurationTest { fun buildTestEnvoyConfiguration( @@ -77,7 +83,8 @@ class EnvoyConfigurationTest { appId: String = "com.example.myapp", trustChainVerification: TrustChainVerification = TrustChainVerification.VERIFY_TRUST_CHAIN, virtualClusters: String = "[test]", - enableSkipDNSLookupForProxiedRequests: Boolean = false + enableSkipDNSLookupForProxiedRequests: Boolean = false, + enablePlatformCertificatesValidation: Boolean = false ): EnvoyConfiguration { return EnvoyConfiguration( adminInterfaceEnabled, @@ -115,7 +122,8 @@ class EnvoyConfigurationTest { emptyMap(), emptyMap(), emptyList(), - enableSkipDNSLookupForProxiedRequests + enableSkipDNSLookupForProxiedRequests, + enablePlatformCertificatesValidation ) } @@ -124,7 +132,8 @@ class EnvoyConfigurationTest { val envoyConfiguration = buildTestEnvoyConfiguration() val resolvedTemplate = envoyConfiguration.resolveTemplate( - TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT + TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT, + CERT_VALIDATION_TEMPLATE ) assertThat(resolvedTemplate).contains("&connect_timeout 123s") @@ -187,6 +196,9 @@ class EnvoyConfigurationTest { assertThat(resolvedTemplate).contains("filter_name") assertThat(resolvedTemplate).contains("test_config") + // Cert Validation + assertThat(resolvedTemplate).contains("custom_validator_config") + // Proxying assertThat(resolvedTemplate).contains("&skip_dns_lookup_for_proxied_requests false") } @@ -204,11 +216,13 @@ class EnvoyConfigurationTest { enableBrotli = true, enableInterfaceBinding = true, h2ExtendKeepaliveTimeout = true, - enableSkipDNSLookupForProxiedRequests = true + enableSkipDNSLookupForProxiedRequests = true, + enablePlatformCertificatesValidation = true ) val resolvedTemplate = envoyConfiguration.resolveTemplate( - TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT + TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT, +CERT_VALIDATION_TEMPLATE ) // DNS @@ -232,6 +246,9 @@ class EnvoyConfigurationTest { // Interface Binding assertThat(resolvedTemplate).contains("&enable_interface_binding true") + // Cert Validation + assertThat(resolvedTemplate).contains("custom_validator_config") + // Proxying assertThat(resolvedTemplate).contains("&skip_dns_lookup_for_proxied_requests true") } @@ -243,7 +260,8 @@ class EnvoyConfigurationTest { ) val resolvedTemplate = envoyConfiguration.resolveTemplate( - TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT + TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT, + CERT_VALIDATION_TEMPLATE ) assertThat(resolvedTemplate).contains("&dns_resolver_name envoy.network.dns_resolver.cares") @@ -255,7 +273,7 @@ class EnvoyConfigurationTest { val envoyConfiguration = buildTestEnvoyConfiguration() try { - envoyConfiguration.resolveTemplate("{{ missing }}", "", "", "", "", "", "") + envoyConfiguration.resolveTemplate("{{ missing }}", "", "", "", "", "", "", "") fail("Unresolved configuration keys should trigger exception.") } catch (e: EnvoyConfiguration.ConfigurationException) { assertThat(e.message).contains("missing") @@ -270,7 +288,8 @@ class EnvoyConfigurationTest { ) val resolvedTemplate = envoyConfiguration.resolveTemplate( - TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT + TEST_CONFIG, PLATFORM_FILTER_CONFIG, NATIVE_FILTER_CONFIG, APCF_INSERT, GZIP_INSERT, BROTLI_INSERT, SOCKET_TAG_INSERT, +CERT_VALIDATION_TEMPLATE ) assertThat(resolvedTemplate).contains("&dns_resolver_config {\"@type\":\"type.googleapis.com/envoy.extensions.network.dns_resolver.cares.v3.CaresDnsResolverConfig\",\"resolvers\":[{\"socket_address\":{\"address\":\"8.8.8.8\"}},{\"socket_address\":{\"address\":\"1.1.1.1\"}}],\"use_resolvers_as_fallback\": true, \"filter_unroutable_families\": true}") diff --git a/test/java/org/chromium/net/CertificateVerificationTest.java b/test/java/org/chromium/net/CertificateVerificationTest.java index 2ac4af7626..ff8e0083d8 100644 --- a/test/java/org/chromium/net/CertificateVerificationTest.java +++ b/test/java/org/chromium/net/CertificateVerificationTest.java @@ -28,7 +28,8 @@ public final class CertificateVerificationTest { AndroidJniLibrary.load(context.getApplicationContext()); } - private static final byte[] host = FakeX509Util.expectedHost.getBytes(StandardCharsets.UTF_8); + private static final byte[] host = + FakeX509Util.getExpectedHost().getBytes(StandardCharsets.UTF_8); private static final byte[] authType = FakeX509Util.expectedAuthType.getBytes(StandardCharsets.UTF_8); diff --git a/test/java/org/chromium/net/testing/CronetTestRule.java b/test/java/org/chromium/net/testing/CronetTestRule.java index 993263c128..533e7c7638 100644 --- a/test/java/org/chromium/net/testing/CronetTestRule.java +++ b/test/java/org/chromium/net/testing/CronetTestRule.java @@ -33,6 +33,7 @@ import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import org.chromium.net.AndroidNetworkLibrary; /** * Custom TestRule for Cronet instrumentation tests. @@ -258,6 +259,7 @@ private void setUp() { .penaltyDeath() .build()); } + AndroidNetworkLibrary.setFakeCertificateVerificationForTesting(true); } private void tearDown() { @@ -272,6 +274,7 @@ private void tearDown() { } resetURLStreamHandlerFactory(); + AndroidNetworkLibrary.setFakeCertificateVerificationForTesting(false); try { // Run GC and finalizers a few times to pick up leaked closeables diff --git a/test/java/org/chromium/net/testing/Http2TestServerTest.java b/test/java/org/chromium/net/testing/Http2TestServerTest.java index 2ed95a0773..626fe862c7 100644 --- a/test/java/org/chromium/net/testing/Http2TestServerTest.java +++ b/test/java/org/chromium/net/testing/Http2TestServerTest.java @@ -1,13 +1,15 @@ package org.chromium.net.testing; -import static io.envoyproxy.envoymobile.engine.EnvoyConfiguration.TrustChainVerification.ACCEPT_UNTRUSTED; +import static io.envoyproxy.envoymobile.engine.EnvoyConfiguration.TrustChainVerification; import static org.assertj.core.api.Assertions.assertThat; import static org.chromium.net.testing.CronetTestRule.SERVER_CERT_PEM; import static org.chromium.net.testing.CronetTestRule.SERVER_KEY_PKCS8_PEM; +import static org.junit.Assert.assertNotNull; import android.content.Context; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.chromium.net.AndroidNetworkLibrary; import io.envoyproxy.envoymobile.AndroidEngineBuilder; import io.envoyproxy.envoymobile.Engine; import io.envoyproxy.envoymobile.EnvoyError; @@ -18,6 +20,7 @@ import io.envoyproxy.envoymobile.ResponseTrailers; import io.envoyproxy.envoymobile.UpstreamHttpProtocol; import io.envoyproxy.envoymobile.engine.AndroidJniLibrary; +import io.envoyproxy.envoymobile.engine.JniLibrary; import java.net.MalformedURLException; import java.net.URL; import java.nio.ByteBuffer; @@ -27,34 +30,52 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import java.nio.charset.StandardCharsets; +import org.chromium.net.testing.CertTestUtil; +import org.chromium.net.FakeX509Util; -@RunWith(AndroidJUnit4.class) +@RunWith(RobolectricTestRunner.class) public class Http2TestServerTest { private Engine engine; + private String serverCertPath = SERVER_CERT_PEM; + private String serverKeyPath = SERVER_KEY_PKCS8_PEM; @BeforeClass public static void loadJniLibrary() { AndroidJniLibrary.loadTestLibrary(); + JniLibrary.load(); } @Before - public void setUpEngine() throws Exception { + public void setUp() throws Exception { + AndroidNetworkLibrary.setFakeCertificateVerificationForTesting(true); + FakeX509Util.setExpectedHost(Http2TestServer.getServerHost()); + AndroidNetworkLibrary.addTestRootCertificate(CertTestUtil.pemToDer(serverCertPath)); + } + + public void setUpEngine(boolean enablePlatformCertificatesValidation, + TrustChainVerification trustChainVerification) throws Exception { CountDownLatch latch = new CountDownLatch(1); Context appContext = ApplicationProvider.getApplicationContext(); engine = new AndroidEngineBuilder(appContext) - .setTrustChainVerification(ACCEPT_UNTRUSTED) + .enablePlatformCertificatesValidation(enablePlatformCertificatesValidation) + .setTrustChainVerification(trustChainVerification) .setOnEngineRunning(() -> { latch.countDown(); return null; }) .build(); - Http2TestServer.startHttp2TestServer(appContext, SERVER_CERT_PEM, SERVER_KEY_PKCS8_PEM); + Http2TestServer.startHttp2TestServer(appContext, serverCertPath, serverKeyPath); latch.await(); // Don't launch a request before initialization has completed. } @@ -62,10 +83,14 @@ public void setUpEngine() throws Exception { public void shutdown() throws Exception { engine.terminate(); Http2TestServer.shutdownHttp2TestServer(); + AndroidNetworkLibrary.clearTestRootCertificates(); + AndroidNetworkLibrary.setFakeCertificateVerificationForTesting(false); } - @Test - public void getSchemeIsHttps() throws Exception { + private void getSchemeIsHttps(boolean enablePlatformCertificatesValidation, + TrustChainVerification trustChainVerification) throws Exception { + setUpEngine(enablePlatformCertificatesValidation, trustChainVerification); + RequestScenario requestScenario = new RequestScenario() .setHttpMethod(RequestMethod.GET) .setUrl(Http2TestServer.getEchoAllHeadersUrl()); @@ -78,6 +103,90 @@ public void getSchemeIsHttps() throws Exception { assertThat(response.getEnvoyError()).isNull(); } + @Test + public void testGetRequest() throws Exception { + getSchemeIsHttps(false, TrustChainVerification.ACCEPT_UNTRUSTED); + } + + @Test + public void testGetRequestWithPlatformCertValidatorSuccess() throws Exception { + getSchemeIsHttps(true, TrustChainVerification.VERIFY_TRUST_CHAIN); + } + + @Test + public void testGetRequestWithPlatformCertValidatorFail() throws Exception { + // Remove any pre-installed test certs, so that following verifications will fail. + AndroidNetworkLibrary.clearTestRootCertificates(); + setUpEngine(true, TrustChainVerification.VERIFY_TRUST_CHAIN); + RequestScenario requestScenario = new RequestScenario() + .setHttpMethod(RequestMethod.GET) + .setUrl(Http2TestServer.getEchoAllHeadersUrl()); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference response = new AtomicReference<>(new Response()); + engine.streamClient() + .newStreamPrototype() + .setOnError((error, ignored) -> { + response.get().setEnvoyError(error); + latch.countDown(); + return null; + }) + .setOnCancel((ignored) -> { throw new AssertionError("Unexpected OnCancel called."); }) + .start(Executors.newSingleThreadExecutor()) + .sendHeaders(requestScenario.getHeaders(), false); + + latch.await(); + assertNotNull(response.get().getEnvoyError()); + assertThat(response.get().getEnvoyError().getErrorCode()).isEqualTo(2); + assertThat(response.get().getEnvoyError().getMessage()).contains("CERTIFICATE_VERIFY_FAILED"); + response.get().throwAssertionErrorIfAny(); + } + + @Test + public void testAcceptUntrustedWithPlatformCertValidator() throws Exception { + // Remove any pre-installed test certs, so that following verifications will fail. + AndroidNetworkLibrary.clearTestRootCertificates(); + getSchemeIsHttps(true, TrustChainVerification.ACCEPT_UNTRUSTED); + } + + @Test + public void testSubjectAltNameErrorWithPlatformCertValidator() throws Exception { + // Switch to a cert which doesn't have 127.0.0.1 in the SAN list. + serverCertPath = "../envoy/test/config/integration/certs/servercert.pem"; + serverKeyPath = "../envoy/test/config/integration/certs/serverkey.pem"; + AndroidNetworkLibrary.addTestRootCertificate(CertTestUtil.pemToDer(serverCertPath)); + setUpEngine(true, TrustChainVerification.VERIFY_TRUST_CHAIN); + RequestScenario requestScenario = new RequestScenario() + .setHttpMethod(RequestMethod.GET) + .setUrl(Http2TestServer.getEchoAllHeadersUrl()); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference response = new AtomicReference<>(new Response()); + engine.streamClient() + .newStreamPrototype() + .setOnError((error, ignored) -> { + response.get().setEnvoyError(error); + latch.countDown(); + return null; + }) + .setOnCancel((ignored) -> { throw new AssertionError("Unexpected OnCancel called."); }) + .start(Executors.newSingleThreadExecutor()) + .sendHeaders(requestScenario.getHeaders(), false); + + latch.await(); + assertNotNull(response.get().getEnvoyError()); + assertThat(response.get().getEnvoyError().getErrorCode()).isEqualTo(2); + assertThat(response.get().getEnvoyError().getMessage()).contains("CERTIFICATE_VERIFY_FAILED"); + response.get().throwAssertionErrorIfAny(); + } + + @Test + public void testSubjectAltNameErrorAllowedWithPlatformCertValidator() throws Exception { + // Switch to a cert which doesn't have 127.0.0.1 in the SAN list. + serverCertPath = "../envoy/test/config/integration/certs/servercert.pem"; + serverKeyPath = "../envoy/test/config/integration/certs/serverkey.pem"; + AndroidNetworkLibrary.addTestRootCertificate(CertTestUtil.pemToDer(serverCertPath)); + getSchemeIsHttps(true, TrustChainVerification.ACCEPT_UNTRUSTED); + } + private Response sendRequest(RequestScenario requestScenario) throws Exception { final CountDownLatch latch = new CountDownLatch(1); final AtomicReference response = new AtomicReference<>(new Response());