0

Add net error code for self-signed certs on local network URLs

This adds (behind a new, default disabled feature flag) a new net error
code (ERR_CERT_SELF_SIGNED_LOCAL_NETWORK), which is returned for
certificate errors when the certificate is self-signed, and is being
used on a local network URL (either an RFC 1918 IP, or a URL that ends
in .local).

This is step one towards implementing go/betterselfsigned, for now this
only results in the regular SSL interstitial being triggered.

Bug: 394119724
Change-Id: I62850128c250358071cc0693526b23c2677647cf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6226147
Commit-Queue: Carlos IL <carlosil@chromium.org>
Reviewed-by: Matt Mueller <mattm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1424804}
This commit is contained in:
Carlos IL
2025-02-25 14:23:48 -08:00
committed by Chromium LUCI CQ
parent 859865d196
commit 9f2a0256c7
14 changed files with 295 additions and 11 deletions

@ -26,6 +26,7 @@ int IsCertErrorFatal(int cert_error) {
case net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED:
case net::ERR_CERT_SYMANTEC_LEGACY:
case net::ERR_CERT_KNOWN_INTERCEPTION_BLOCKED:
case net::ERR_CERT_SELF_SIGNED_LOCAL_NETWORK:
return false;
case net::ERR_CERT_CONTAINS_ERRORS:
case net::ERR_CERT_REVOKED:

@ -100,6 +100,7 @@ ErrorInfo ErrorInfo::CreateError(ErrorType error_type,
case CERT_KNOWN_INTERCEPTION_BLOCKED:
case CERT_AUTHORITY_INVALID:
case CERT_SYMANTEC_LEGACY:
case CERT_SELF_SIGNED_LOCAL_NETWORK:
details =
l10n_util::GetStringFUTF16(IDS_CERT_ERROR_AUTHORITY_INVALID_DETAILS,
UTF8ToUTF16(request_url.host()));
@ -235,6 +236,8 @@ ErrorInfo::ErrorType ErrorInfo::NetErrorToErrorType(int net_error) {
return CERT_SYMANTEC_LEGACY;
case net::ERR_CERT_KNOWN_INTERCEPTION_BLOCKED:
return CERT_KNOWN_INTERCEPTION_BLOCKED;
case net::ERR_CERT_SELF_SIGNED_LOCAL_NETWORK:
return CERT_SELF_SIGNED_LOCAL_NETWORK;
default:
NOTREACHED();
}

@ -43,6 +43,7 @@ class ErrorInfo {
CERT_KNOWN_INTERCEPTION_BLOCKED = 17,
LEGACY_TLS = 18,
CERT_NON_UNIQUE_NAME = 19,
CERT_SELF_SIGNED_LOCAL_NETWORK = 20,
END_OF_ENUM
};

@ -708,4 +708,8 @@ BASE_FEATURE(kUseCertTransparencyAwareApiForOsCertVerify,
base::FEATURE_ENABLED_BY_DEFAULT);
#endif // BUILDFLAG(IS_ANDROID)
BASE_FEATURE(kSelfSignedLocalNetworkInterstitial,
"SelfSignedLocalNetworkInterstitial",
base::FEATURE_DISABLED_BY_DEFAULT);
} // namespace net::features

@ -725,6 +725,10 @@ NET_EXPORT BASE_DECLARE_FEATURE(kExcludeLargeBodyReports);
NET_EXPORT BASE_DECLARE_FEATURE(kUseCertTransparencyAwareApiForOsCertVerify);
#endif // BUILDFLAG(IS_ANDROID)
// Enables a special interstitial for self signed cert errors in local network
// URLs.
NET_EXPORT BASE_DECLARE_FEATURE(kSelfSignedLocalNetworkInterstitial);
} // namespace net::features
#endif // NET_BASE_FEATURES_H_

@ -564,13 +564,17 @@ NET_ERROR(CERT_KNOWN_INTERCEPTION_BLOCKED, -217)
// -218 was SSL_OBSOLETE_VERSION which is not longer used. TLS 1.0/1.1 instead
// cause SSL_VERSION_OR_CIPHER_MISMATCH now.
// The certificate is self signed and it's being used for either an RFC1918 IP
// literal URL, or a url ending in .local.
NET_ERROR(CERT_SELF_SIGNED_LOCAL_NETWORK, -219)
// Add new certificate error codes here.
//
// Update the value of CERT_END whenever you add a new certificate error
// code.
// The value immediately past the last certificate error code.
NET_ERROR(CERT_END, -219)
NET_ERROR(CERT_END, -220)
// The URL is invalid.
NET_ERROR(INVALID_URL, -300)

@ -29,6 +29,7 @@ TEST(NetErrorsTest, IsCertificateError) {
EXPECT_TRUE(IsCertificateError(ERR_CERT_WEAK_SIGNATURE_ALGORITHM));
EXPECT_TRUE(IsCertificateError(ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN));
EXPECT_TRUE(IsCertificateError(ERR_CERT_KNOWN_INTERCEPTION_BLOCKED));
EXPECT_TRUE(IsCertificateError(ERR_CERT_SELF_SIGNED_LOCAL_NETWORK));
// Negative tests.
EXPECT_FALSE(IsCertificateError(ERR_SSL_PROTOCOL_ERROR));
@ -42,7 +43,7 @@ TEST(NetErrorsTest, IsCertificateError) {
// Trigger a failure whenever ERR_CERT_END is changed, forcing developers to
// update this test.
EXPECT_EQ(ERR_CERT_END, -219)
EXPECT_EQ(ERR_CERT_END, -220)
<< "It looks like you added a new certificate error code ("
<< ErrorToString(ERR_CERT_END + 1)
<< ").\n"

@ -353,6 +353,14 @@ std::string CanonicalizeHost(std::string_view host,
}
} // namespace
std::string CanonicalizeHostSupportsBareIPV6(std::string_view host,
url::CanonHostInfo* host_info) {
const std::string host_or_ip = host.find(':') != std::string::npos
? base::StrCat({"[", host, "]"})
: std::string(host);
return CanonicalizeHost(host_or_ip, host_info);
}
std::string CanonicalizeHost(std::string_view host,
url::CanonHostInfo* host_info) {
return CanonicalizeHost(host, /*is_file_scheme=*/false, host_info);

@ -167,6 +167,12 @@ NET_EXPORT std::string GetSuperdomain(std::string_view domain);
NET_EXPORT bool IsSubdomainOf(std::string_view subdomain,
std::string_view superdomain);
// Wrapper for CanonicalizeHost that allows using a bare IPV6. If |host| is
// not IPV6, this is equivalent to CanonicalizeHost.
NET_EXPORT std::string CanonicalizeHostSupportsBareIPV6(
std::string_view host,
url::CanonHostInfo* host_info);
// Canonicalizes |host| and returns it. Also fills |host_info| with
// IP address information. |host_info| must not be NULL.
// Canonicalization will follow the host parsing rules for a non-file

@ -25,10 +25,15 @@ int MapCertStatusToNetError(CertStatus cert_status) {
return ERR_CERT_KNOWN_INTERCEPTION_BLOCKED;
if (cert_status & CERT_STATUS_REVOKED)
return ERR_CERT_REVOKED;
if (cert_status & CERT_STATUS_AUTHORITY_INVALID)
if (cert_status & CERT_STATUS_AUTHORITY_INVALID &&
!(cert_status & CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK)) {
return ERR_CERT_AUTHORITY_INVALID;
}
if (cert_status & CERT_STATUS_COMMON_NAME_INVALID)
return ERR_CERT_COMMON_NAME_INVALID;
if (cert_status & CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK) {
return ERR_CERT_SELF_SIGNED_LOCAL_NETWORK;
}
if (cert_status & CERT_STATUS_CERTIFICATE_TRANSPARENCY_REQUIRED)
return ERR_CERTIFICATE_TRANSPARENCY_REQUIRED;
if (cert_status & CERT_STATUS_SYMANTEC_LEGACY)

@ -45,3 +45,4 @@ CERT_STATUS_FLAG(CERTIFICATE_TRANSPARENCY_REQUIRED, 1 << 24)
CERT_STATUS_FLAG(SYMANTEC_LEGACY, 1 << 25)
CERT_STATUS_FLAG(KNOWN_INTERCEPTION_BLOCKED, 1 << 26)
// Bit 27 was CERT_STATUS_LEGACY_TLS.
CERT_STATUS_FLAG(SELF_SIGNED_LOCAL_NETWORK, 1 << 28)

@ -13,6 +13,8 @@
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/network_time/time_tracker/time_tracker.h"
@ -20,6 +22,7 @@
#include "net/base/features.h"
#include "net/base/ip_address.h"
#include "net/base/net_errors.h"
#include "net/base/url_util.h"
#include "net/cert/cert_net_fetcher.h"
#include "net/cert/cert_status_flags.h"
#include "net/cert/cert_verifier.h"
@ -49,6 +52,7 @@
#include "third_party/boringssl/src/pki/trust_store.h"
#include "third_party/boringssl/src/pki/trust_store_collection.h"
#include "third_party/boringssl/src/pki/trust_store_in_memory.h"
#include "url/url_canon.h"
#if BUILDFLAG(CHROME_ROOT_STORE_SUPPORTED)
#include "base/version_info/version_info.h" // nogncheck
@ -188,6 +192,37 @@ bool IsEVCandidate(const EVRootCAMetadata* ev_metadata,
return !oids.empty();
}
bool IsSelfSignedCertOnLocalNetwork(const X509Certificate* cert,
const std::string& hostname) {
if (!base::FeatureList::IsEnabled(
features::kSelfSignedLocalNetworkInterstitial)) {
return false;
}
url::CanonHostInfo host_info;
std::string canonicalized_hostname =
CanonicalizeHostSupportsBareIPV6(hostname, &host_info);
if (canonicalized_hostname.empty()) {
return false;
}
if (host_info.IsIPAddress()) {
base::span<uint8_t> ip_span(host_info.address);
// AddressLength() is always 0, 4, or 16, so it's safe to cast to unsigned.
IPAddress ip(
ip_span.first(static_cast<unsigned>(host_info.AddressLength())));
if (ip.IsPubliclyRoutable()) {
return false;
}
} else {
if (!base::EndsWith(canonicalized_hostname, ".local",
base::CompareCase::INSENSITIVE_ASCII) &&
!base::EndsWith(canonicalized_hostname, ".local.",
base::CompareCase::INSENSITIVE_ASCII)) {
return false;
}
}
return X509Certificate::IsSelfSigned(cert->cert_buffer());
}
// CertVerifyProcTrustStore wraps a SystemTrustStore with additional trust
// anchors and TestRootCerts.
class CertVerifyProcTrustStore {
@ -1158,9 +1193,13 @@ int AssignVerifyResult(X509Certificate* input_cert,
verify_result->policy_compliance = delegate_data->ct_policy_compliance;
}
return IsCertStatusError(verify_result->cert_status)
? MapCertStatusToNetError(verify_result->cert_status)
: OK;
if (IsCertStatusError(verify_result->cert_status)) {
if (IsSelfSignedCertOnLocalNetwork(input_cert, hostname)) {
verify_result->cert_status |= CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK;
}
return MapCertStatusToNetError(verify_result->cert_status);
}
return OK;
}
// Returns true if retrying path building with a less stringent signature

@ -20,8 +20,10 @@
#include "base/time/time.h"
#include "components/network_time/time_tracker/time_tracker.h"
#include "net/base/features.h"
#include "net/base/ip_address.h"
#include "net/base/net_errors.h"
#include "net/base/test_completion_callback.h"
#include "net/cert/cert_status_flags.h"
#include "net/cert/cert_verify_proc.h"
#include "net/cert/crl_set.h"
#include "net/cert/do_nothing_ct_verifier.h"
@ -29,6 +31,7 @@
#include "net/cert/internal/system_trust_store.h"
#include "net/cert/sct_status_flags.h"
#include "net/cert/time_conversions.h"
#include "net/cert/x509_certificate.h"
#include "net/cert/x509_util.h"
#include "net/cert_net/cert_net_fetcher_url_request.h"
#include "net/http/transport_security_state.h"
@ -2111,4 +2114,211 @@ TEST_F(CertVerifyProcBuiltinTest, IterationLimit) {
EXPECT_EQ(true, event->params.FindBool("exceeded_iteration_limit"));
}
class CertVerifyProcBuiltinSelfSignedTest
: public CertVerifyProcBuiltinTest,
public testing::WithParamInterface<bool> {
public:
CertVerifyProcBuiltinSelfSignedTest() {
if (GetParam()) {
feature_list_.InitAndEnableFeature(
features::kSelfSignedLocalNetworkInterstitial);
} else {
feature_list_.InitAndDisableFeature(
features::kSelfSignedLocalNetworkInterstitial);
}
}
scoped_refptr<X509Certificate> CreateSelfSigned(
std::string_view subject_dns_name) {
// Create a chain of size 1, which will result in a self-signed certificate
std::vector<std::unique_ptr<CertBuilder>> builders =
CertBuilder::CreateSimpleChain(1);
base::Time not_before = base::Time::Now() - base::Days(1);
base::Time not_after = base::Time::Now() + base::Days(1);
builders[0]->SetValidity(not_before, not_after);
builders[0]->SetSubjectAltName(subject_dns_name);
return builders[0]->GetX509Certificate();
}
scoped_refptr<X509Certificate> CreateSelfSignedIPSubject(
std::string_view ip_address) {
// Create a chain of size 1, which will result in a self-signed certificate
std::vector<std::unique_ptr<CertBuilder>> builders =
CertBuilder::CreateSimpleChain(1);
base::Time not_before = base::Time::Now() - base::Days(1);
base::Time not_after = base::Time::Now() + base::Days(1);
builders[0]->SetValidity(not_before, not_after);
IPAddress ip;
if (!ParseURLHostnameToAddress(ip_address, &ip)) {
ADD_FAILURE() << "Failed to parse IP address";
}
builders[0]->SetSubjectAltNames({}, {ip});
return builders[0]->GetX509Certificate();
}
private:
base::test::ScopedFeatureList feature_list_;
};
TEST_P(CertVerifyProcBuiltinSelfSignedTest,
SelfSignedCertOnLocalNetworkHostname) {
scoped_refptr<X509Certificate> cert = CreateSelfSigned("testurl.local");
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(cert, "testurl.local", 0, &verify_result, &verify_net_log_source,
callback.callback());
int error = callback.WaitForResult();
if (GetParam()) {
EXPECT_TRUE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_SELF_SIGNED_LOCAL_NETWORK));
} else {
EXPECT_FALSE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_AUTHORITY_INVALID));
}
}
TEST_P(CertVerifyProcBuiltinSelfSignedTest, SelfSignedCertOnLocalNetworkIP) {
scoped_refptr<X509Certificate> cert =
CreateSelfSignedIPSubject("192.168.0.1");
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(cert, "192.168.0.1", 0, &verify_result, &verify_net_log_source,
callback.callback());
int error = callback.WaitForResult();
if (GetParam()) {
EXPECT_TRUE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_SELF_SIGNED_LOCAL_NETWORK));
} else {
EXPECT_FALSE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_AUTHORITY_INVALID));
}
}
TEST_P(CertVerifyProcBuiltinSelfSignedTest, SelfSignedCertOnLocalNetworkIPv6) {
scoped_refptr<X509Certificate> cert =
CreateSelfSignedIPSubject("[fc00:0:0:0:0:0:0:0]");
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(cert, "fc00:0:0:0:0:0:0:0", 0, &verify_result, &verify_net_log_source,
callback.callback());
int error = callback.WaitForResult();
if (GetParam()) {
EXPECT_TRUE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_SELF_SIGNED_LOCAL_NETWORK));
} else {
EXPECT_FALSE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_AUTHORITY_INVALID));
}
}
TEST_P(CertVerifyProcBuiltinSelfSignedTest, NonSelfSignedCertOnLocalNetwork) {
std::vector<std::unique_ptr<CertBuilder>> builders =
CertBuilder::CreateSimpleChain(2);
base::Time not_before = base::Time::Now() - base::Days(2);
base::Time not_after = base::Time::Now() - base::Days(2);
builders[0]->SetValidity(not_before, not_after);
builders[0]->SetSubjectAltName("testurl.local");
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(builders[0]->GetX509CertificateChain(), "testurl.local", 0,
&verify_result, &verify_net_log_source, callback.callback());
int error = callback.WaitForResult();
EXPECT_FALSE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_AUTHORITY_INVALID));
}
TEST_P(CertVerifyProcBuiltinSelfSignedTest,
SelfSignedCertNotLocalNetworkHostname) {
scoped_refptr<X509Certificate> cert = CreateSelfSigned("www.example.com");
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(cert, "www.example.com", 0, &verify_result, &verify_net_log_source,
callback.callback());
int error = callback.WaitForResult();
EXPECT_FALSE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_AUTHORITY_INVALID));
}
TEST_P(CertVerifyProcBuiltinSelfSignedTest, SelfSignedCertNotLocalNetworkIP) {
scoped_refptr<X509Certificate> cert = CreateSelfSignedIPSubject("8.8.8.8");
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(cert, "8.8.8.8", 0, &verify_result, &verify_net_log_source,
callback.callback());
int error = callback.WaitForResult();
EXPECT_FALSE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_AUTHORITY_INVALID));
}
TEST_P(CertVerifyProcBuiltinSelfSignedTest, SelfSignedCertNotLocalNetworkIPv6) {
scoped_refptr<X509Certificate> cert =
CreateSelfSignedIPSubject("[2001:4860:4860::8888]");
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(cert, "2001:4860:4860::8888", 0, &verify_result,
&verify_net_log_source, callback.callback());
int error = callback.WaitForResult();
EXPECT_FALSE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_AUTHORITY_INVALID));
}
TEST_P(CertVerifyProcBuiltinSelfSignedTest,
SelfSignedCertOnLocalNetworkHostnameNameMismatchTakesPrecedence) {
scoped_refptr<X509Certificate> cert = CreateSelfSigned("nottesturl.local");
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(cert, "testurl.local", 0, &verify_result, &verify_net_log_source,
callback.callback());
int error = callback.WaitForResult();
if (GetParam()) {
EXPECT_TRUE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_COMMON_NAME_INVALID));
} else {
EXPECT_FALSE(verify_result.cert_status &
CERT_STATUS_SELF_SIGNED_LOCAL_NETWORK);
EXPECT_THAT(error, IsError(ERR_CERT_AUTHORITY_INVALID));
}
}
INSTANTIATE_TEST_SUITE_P(SelfSignedInterstitial,
CertVerifyProcBuiltinSelfSignedTest,
testing::Bool());
} // namespace net

@ -452,12 +452,9 @@ bool X509Certificate::VerifyHostname(
// access, i.e. the thing displayed in the URL bar.
// Presented identifier(s) == name(s) the server knows itself as, in its cert.
// CanonicalizeHost requires surrounding brackets to parse an IPv6 address.
const std::string host_or_ip = hostname.find(':') != std::string::npos
? base::StrCat({"[", hostname, "]"})
: std::string(hostname);
url::CanonHostInfo host_info;
std::string reference_name = CanonicalizeHost(host_or_ip, &host_info);
std::string reference_name =
CanonicalizeHostSupportsBareIPV6(hostname, &host_info);
// If the host cannot be canonicalized, fail fast.
if (reference_name.empty())