CVE-2026-79192
Overview
Background
- `CertVerifierServiceFactoryImpl::UpdateNetworkTime`
- the cert-verifier-service entry point (
services/cert_verifier/cert_verifier_service_factory.cc) that receives a network-time measurement and installs it as the time source used for certificate validity checks. - `TimeTracker` (`proc_params_.time_tracker`)
- the custom network-time state the cert verifier uses in place of the system clock when validating certificate dates.
- `uncertainty`
- the error bound reported with a network-time measurement, expressing how imprecise that time estimate is.
- `kMaxUncertaintyForNetworkTime`
- the new 45-second ceiling above which a network-time update is rejected.
Root Cause Analysis
3b4225c11d5b. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (823df872a358).CertVerifierServiceFactoryImpl::UpdateNetworkTime receives system_time, system_ticks, current_time, and uncertainty (cert_verifier_service_factory.cc:567-571). Pre-fix, it ignored uncertainty for any decision: a TODO: crbug.com/502082953 - Consider what to do if the uncertainty is high. (cert_verifier_service_factory.cc:572) was followed unconditionally by proc_params_.time_tracker.emplace(system_time, system_ticks, current_time, uncertainty) and UpdateVerifierServices() (cert_verifier_service_factory.cc:573-575). The violated invariant is that certificate validation must not rely on a network time whose error bound is large enough to move “now” across a certificate’s validity window; the pre-fix code installed every measurement regardless of its uncertainty, so an attacker-skewed, high-uncertainty time estimate would still be trusted for date checks.
The fix (commit 3b4225c11d5b5b940f0acbaad1538ee3893e8d72) adds constexpr base::TimeDelta kMaxUncertaintyForNetworkTime = base::Seconds(45) and, in place of the TODO, if (uncertainty > kMaxUncertaintyForNetworkTime) { return; }, so an update whose uncertainty exceeds the threshold is dropped and the prior (e.g. system-clock-based) time is retained. The added unit tests confirm an update at 45s is applied while one at 46s is ignored (falling back to system time, yielding net::ERR_CERT_DATE_INVALID). Note the material shows uncertainty was already a parameter here and was already forwarded into the tracker, which contradicts the report’s claim that uncertainty is “discarded” / hardcoded to zero on this path; the code wins.
uncertainty exceeds a 45-second ceiling, so a wide-margin (attacker-stretched) time estimate can no longer drive certificate-date validation.Attack Path
- Delay a timed network fetch
An on-path attacker lets a time-bearing response’s headers arrive but trickles its body, inflating the measured round-trip and thus the derived network-time
uncertainty(reporter’s trigger via a Variations seed fetch; the Variations/latency path is not shown in this commit’s material, (inferred)). - Skew network time into the past
The large measured latency shifts the estimated “now” earlier while producing a correspondingly large
uncertainty. - Trusted by the cert verifier (pre-fix)
UpdateNetworkTimeinstalls that skewed time intoproc_params_.time_trackerunconditionally (cert_verifier_service_factory.cc:572-575). - Accept an expired certificate Certificate date checks run against the past-skewed time, so a recently expired certificate for a target origin validates as still-valid, enabling origin impersonation (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifservices/cert_verifier/cert_verifier_service_factory.cc |
modified |
Files Changed
services/cert_verifier/cert_verifier_service_factory.ccservices/cert_verifier/cert_verifier_service_factory_unittest.cc
Audit Directions
- Unused precision metadataFlag code that receives an
uncertainty/confidence value but makes no decision based on it (especially a lingeringTODObeside a security-relevantemplace/SetHeader/Set*). - Network time feeding security checksAudit every consumer of network-time state used for certificate or validity-window decisions to confirm it bounds or rejects low-confidence measurements rather than trusting them implicitly.
- Cross-service invariant driftWhen one service (network time) caps a value and another (cert verifier) consumes it, verify the consumer re-validates independently instead of assuming the producer’s bound.
Patch
diff --git a/services/cert_verifier/cert_verifier_service_factory.cc b/services/cert_verifier/cert_verifier_service_factory.cc
index 2b9e4cd..5a58892 100644
--- a/services/cert_verifier/cert_verifier_service_factory.cc
+++ b/services/cert_verifier/cert_verifier_service_factory.cc
@@ -66,6 +66,15 @@
namespace cert_verifier {
namespace {
+// The maximum uncertainty for network time that is considered acceptable. This
+// value is used to determine whether the network time update should be used to
+// update the CertVerifierService's time tracker.
+//
+// This is derived from the 99th percentile of observed network time tracker
+// uncertainty in June 2026, plus a comfortable margin. It is still well below
+// the maximum uncertainty allowed by the network time service's timeout.
+constexpr base::TimeDelta kMaxUncertaintyForNetworkTime = base::Seconds(45);
+
internal::CertVerifierServiceImpl* GetNewCertVerifierImpl(
mojo::PendingReceiver<mojom::CertVerifierService> service_receiver,
mojo::PendingReceiver<mojom::CertVerifierServiceUpdater> updater_receiver,
@@ -569,7 +578,10 @@
base::TimeTicks system_ticks,
base::Time current_time,
base::TimeDelta uncertainty) {
- // TODO: crbug.com/502082953 - Consider what to do if the uncertainty is high.
+ // Ignore network time updates having excessive uncertainty.
+ if (uncertainty > kMaxUncertaintyForNetworkTime) {
+ return;
+ }
proc_params_.time_tracker.emplace(system_time, system_ticks, current_time,
uncertainty);
UpdateVerifierServices();
diff --git a/services/cert_verifier/cert_verifier_service_factory_unittest.cc b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
index d0d6a71..cba40f3 100644
--- a/services/cert_verifier/cert_verifier_service_factory_unittest.cc
+++ b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
@@ -1891,4 +1891,82 @@
}
#endif // BUILDFLAG(CHROME_ROOT_STORE_SUPPORTED)
+TEST_F(CertVerifierServiceFactoryBuiltinVerifierTest,
+ UpdateNetworkTime_Applied) {
+ auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
+ base::Time now = base::Time::Now();
+ base::TimeTicks ticks_now = base::TimeTicks::Now();
+ // Configure the leaf certificate so it is no longer valid according to the
+ // system time.
+ leaf->SetValidity(now - base::Days(3), now - base::Days(1));
+ leaf->SetSubjectAltName("host.test");
+ net::ScopedTestRoot scoped_test_root(root->GetX509Certificate());
+
+ mojo::Remote<mojom::CertVerifierServiceFactory> cv_service_factory_remote;
+ CertVerifierServiceFactoryImpl cv_service_factory_impl(
+ cv_service_factory_remote.BindNewPipeAndPassReceiver());
+ EnableChromeRootStoreIfOptional(&cv_service_factory_impl);
+
+ // Update the time tracker with uncertainty exactly at the threshold (45s).
+ // This should be applied.
+ cv_service_factory_impl.UpdateNetworkTime(now, ticks_now, now - base::Days(2),
+ base::Seconds(45));
+
+ mojo::Remote<mojom::CertVerifierService> cv_service_remote;
+ DummyCVServiceClient cv_service_client;
+ mojom::CertVerifierCreationParamsPtr cv_creation_params =
+ mojom::CertVerifierCreationParams::New();
+
+ cv_service_factory_remote->GetNewCertVerifier(
+ cv_service_remote.BindNewPipeAndPassReceiver(),
+ /*updater=*/mojo::NullReceiver(),
+ cv_service_client.client_.BindNewPipeAndPassRemote(),
+ std::move(cv_creation_params));
+
+ auto [net_error, result] =
+ Verify(cv_service_remote, leaf->GetX509Certificate(), "host.test");
+ EXPECT_THAT(net_error, IsError(net::OK));
+ EXPECT_FALSE(net::IsCertStatusError(result.cert_status));
+}
+
+TEST_F(CertVerifierServiceFactoryBuiltinVerifierTest,
+ UpdateNetworkTime_Ignored) {
+ auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
+ base::Time now = base::Time::Now();
+ base::TimeTicks ticks_now = base::TimeTicks::Now();
+ // Configure the leaf certificate so it is no longer valid according to the
+ // system time.
+ leaf->SetValidity(now - base::Days(3), now - base::Days(1));
+ leaf->SetSubjectAltName("host.test");
+ net::ScopedTestRoot scoped_test_root(root->GetX509Certificate());
+
+ mojo::Remote<mojom::CertVerifierServiceFactory> cv_service_factory_remote;
+ CertVerifierServiceFactoryImpl cv_service_factory_impl(
+ cv_service_factory_remote.BindNewPipeAndPassReceiver());
+ EnableChromeRootStoreIfOptional(&cv_service_factory_impl);
+
+ // Update the time tracker with uncertainty just above the threshold (46s).
+ // This should be ignored.
+ cv_service_factory_impl.UpdateNetworkTime(now, ticks_now, now - base::Days(2),
+ base::Seconds(46));
+
+ mojo::Remote<mojom::CertVerifierService> cv_service_remote;
+ DummyCVServiceClient cv_service_client;
+ mojom::CertVerifierCreationParamsPtr cv_creation_params =
+ mojom::CertVerifierCreationParams::New();
+
+ cv_service_factory_remote->GetNewCertVerifier(
+ cv_service_remote.BindNewPipeAndPassReceiver(),
+ /*updater=*/mojo::NullReceiver(),
+ cv_service_client.client_.BindNewPipeAndPassRemote(),
+ std::move(cv_creation_params));
+
+ auto [net_error, result] =
+ Verify(cv_service_remote, leaf->GetX509Certificate(), "host.test");
+ // Should fail because the network time update was ignored, so it used
+ // system time (which is expired).
+ EXPECT_THAT(net_error, IsError(net::ERR_CERT_DATE_INVALID));
+ EXPECT_TRUE(net::IsCertStatusError(result.cert_status));
+}
+
} // namespace cert_verifier
Regression Test / PoC
diff --git a/services/cert_verifier/cert_verifier_service_factory_unittest.cc b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
index d0d6a71..cba40f3 100644
--- a/services/cert_verifier/cert_verifier_service_factory_unittest.cc
+++ b/services/cert_verifier/cert_verifier_service_factory_unittest.cc
@@ -1891,4 +1891,82 @@
}
#endif // BUILDFLAG(CHROME_ROOT_STORE_SUPPORTED)
+TEST_F(CertVerifierServiceFactoryBuiltinVerifierTest,
+ UpdateNetworkTime_Applied) {
+ auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
+ base::Time now = base::Time::Now();
+ base::TimeTicks ticks_now = base::TimeTicks::Now();
+ // Configure the leaf certificate so it is no longer valid according to the
+ // system time.
+ leaf->SetValidity(now - base::Days(3), now - base::Days(1));
+ leaf->SetSubjectAltName("host.test");
+ net::ScopedTestRoot scoped_test_root(root->GetX509Certificate());
+
+ mojo::Remote<mojom::CertVerifierServiceFactory> cv_service_factory_remote;
+ CertVerifierServiceFactoryImpl cv_service_factory_impl(
+ cv_service_factory_remote.BindNewPipeAndPassReceiver());
+ EnableChromeRootStoreIfOptional(&cv_service_factory_impl);
+
+ // Update the time tracker with uncertainty exactly at the threshold (45s).
+ // This should be applied.
+ cv_service_factory_impl.UpdateNetworkTime(now, ticks_now, now - base::Days(2),
+ base::Seconds(45));
+
+ mojo::Remote<mojom::CertVerifierService> cv_service_remote;
+ DummyCVServiceClient cv_service_client;
+ mojom::CertVerifierCreationParamsPtr cv_creation_params =
+ mojom::CertVerifierCreationParams::New();
+
+ cv_service_factory_remote->GetNewCertVerifier(
+ cv_service_remote.BindNewPipeAndPassReceiver(),
+ /*updater=*/mojo::NullReceiver(),
+ cv_service_client.client_.BindNewPipeAndPassRemote(),
+ std::move(cv_creation_params));
+
+ auto [net_error, result] =
+ Verify(cv_service_remote, leaf->GetX509Certificate(), "host.test");
+ EXPECT_THAT(net_error, IsError(net::OK));
+ EXPECT_FALSE(net::IsCertStatusError(result.cert_status));
+}
+
+TEST_F(CertVerifierServiceFactoryBuiltinVerifierTest,
+ UpdateNetworkTime_Ignored) {
+ auto [leaf, root] = net::CertBuilder::CreateSimpleChain2();
+ base::Time now = base::Time::Now();
+ base::TimeTicks ticks_now = base::TimeTicks::Now();
+ // Configure the leaf certificate so it is no longer valid according to the
+ // system time.
+ leaf->SetValidity(now - base::Days(3), now - base::Days(1));
+ leaf->SetSubjectAltName("host.test");
+ net::ScopedTestRoot scoped_test_root(root->GetX509Certificate());
+
+ mojo::Remote<mojom::CertVerifierServiceFactory> cv_service_factory_remote;
+ CertVerifierServiceFactoryImpl cv_service_factory_impl(
+ cv_service_factory_remote.BindNewPipeAndPassReceiver());
+ EnableChromeRootStoreIfOptional(&cv_service_factory_impl);
+
+ // Update the time tracker with uncertainty just above the threshold (46s).
+ // This should be ignored.
+ cv_service_factory_impl.UpdateNetworkTime(now, ticks_now, now - base::Days(2),
+ base::Seconds(46));
+
+ mojo::Remote<mojom::CertVerifierService> cv_service_remote;
+ DummyCVServiceClient cv_service_client;
+ mojom::CertVerifierCreationParamsPtr cv_creation_params =
+ mojom::CertVerifierCreationParams::New();
+
+ cv_service_factory_remote->GetNewCertVerifier(
+ cv_service_remote.BindNewPipeAndPassReceiver(),
+ /*updater=*/mojo::NullReceiver(),
+ cv_service_client.client_.BindNewPipeAndPassRemote(),
+ std::move(cv_creation_params));
+
+ auto [net_error, result] =
+ Verify(cv_service_remote, leaf->GetX509Certificate(), "host.test");
+ // Should fail because the network time update was ignored, so it used
+ // system time (which is expired).
+ EXPECT_THAT(net_error, IsError(net::ERR_CERT_DATE_INVALID));
+ EXPECT_TRUE(net::IsCertStatusError(result.cert_status));
+}
+
} // namespace cert_verifier
Original Bug Report
Cert validation bypass via NetworkTimeTracker past-skew from delayed Variations Seed body
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.
Overview: An on-path attacker can skew Chrome’s NetworkTimeTracker into the past by intentionally delaying the HTTP response body of a Variations Seed fetch. The resulting uncertainty value is incorrectly discarded before reaching the CertVerifierService, causing it to fully trust the skewed network time. This potentially allows the attacker to bypass certificate expiration checks and impersonate arbitrary origins using expired certificates.
Affected files:
components/variations/service/variations_service.ccchrome/browser/net/cert_verifier_service_time_updater.ccservices/cert_verifier/cert_verifier_service_factory.ccnet/cert/cert_verify_proc_builtin.cccomponents/network_time/network_time_tracker.cc
Estimated timestamp from git blame: 2026-03-12
Vulnerability Details
This vulnerability arises from a chain of logic errors spanning the Variations Service, Network Time Tracker, and the Certificate Verifier:
- Unbounded Fetch & Inflated Latency: In
components/variations/service/variations_service.cc,DoFetchFromURLcreates aSimpleURLLoaderto download the variations seed usingDownloadToStringOfUnboundedSizeUntilCrashAndDie, but fails to set a timeout duration.OnSimpleLoaderCompletecalculates request latency as the entire duration from request start to full body completion (now - last_request_started_time_). An on-path attacker can trickle the response body over a prolonged duration $\Delta$, arbitrarily inflating this latency. - Time Skew:
VariationsServicepasses this inflated latency toNetworkTimeTracker::UpdateNetworkTime. The tracker estimates that the server’s time (from the HTTPDateheader) corresponds to the midpoint of the latency (latency / 2). This effectively skews the tracked network time into the past by $\Delta / 2$. - Discarded Uncertainty:
NetworkTimeTrackercorrectly calculates a largeuncertaintyvalue for this measurement. However,CertVerifierServiceTimeUpdater::OnNetworkTimeChanged(inchrome/browser/net/cert_verifier_service_time_updater.cc) drops theuncertaintyparameter when forwarding the time state over Mojo.CertVerifierServiceFactoryImpl::UpdateNetworkTimethen initializes its internalTimeTrackerwith a hardcoded zero uncertainty (base::TimeDelta()). - Validation Bypass:
CertVerifyProcBuiltin::VerifyInternaluses this custom, zero-uncertainty network time for certificate validation. Because the time is trusted implicitly, an expired certificate that was valid $\Delta / 2$ ago will be considered currently valid. WhenTryBuildPathsucceeds with the custom time, the verification loop breaks immediately, completely skipping any fallback checks against the true system clock.
Potential Attacker Steps
Note: These steps are based on static code analysis; an automated agent has not executed a live proof-of-concept.
- Intercept Seed Fetch: An on-path attacker monitors for Chrome’s Variations Seed fetch (e.g., to
clients4.google.com). - Trickle Body: The attacker permits the TLS-encrypted headers to reach Chrome immediately but stalls the delivery of the TLS-opaque response body over an extended duration $\Delta$ (e.g., 2 hours) using TCP window manipulation or simple packet delays.
- Skew Time: Once the body transfer completes, Chrome’s internal network time is skewed $\Delta/2$ (1 hour) into the past, with its uncertainty wiped out during Mojo IPC.
- Impersonate Target: Within the next several hours (before the next randomized background time fetch), the attacker MITMs a connection to a target origin (e.g.,
https://target.com). - Bypass Expiration: The attacker presents a certificate for the target origin that expired less than $\Delta/2$ ago. Chrome’s
CertVerifieruses the skewed past-time, finds the certificate valid, and allows the attacker to spoof the origin.
Proposed Fix
- Enforce Timeouts: Call
SetTimeoutDuration()on theSimpleURLLoaderused inVariationsService::DoFetchFromURLto prevent arbitrary latency inflation. - Plumb Uncertainty: Update the
CertVerifierServiceFactory::UpdateNetworkTimeMojo interface to accept theuncertaintyparameter, and ensureCertVerifierServiceTimeUpdaterforwards it. - Enforce Limits: Update
CertVerifyProcBuiltinor the internalTimeTrackerto reject or deprioritize network time if theuncertaintyis excessively large or if the deviation from the system clock is unreasonably high.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.