Repository navigation
crypto/tls: certificate chains aren't re-checked on resumption #77217
Description
As part of the report which led to #77113, @rbqvq, also noted that we don't reverify certificate chains during resumption. This has been the default behavior of Go since 1.21 (see #31641 for the discussion of this at the time), when we resolved our previous inconsistent behavior and landed on not reverifying certificates during resumption (and as such documented VerifyPeerCertificates was not called for resumed connections).
The issue as reported was that if the underlying trust configuration changed after an initial connection, this could cause session tickets to be used to resume a connection based on previous information that would otherwise be rejected if used during a fresh handshake. It is our belief that this does not represent a significant security issue, and that this behavior matches the behavior of a number of other implementations (namely BoringSSL and OpenSSL).
Firstly, it is documented to be a API violation to mutate a tls.Config once it has been passed to a TLS function ("After [a tls.Config] has been passed to a TLS function it must not be modified"), so mutating any of the Config fields which define trust (Config.Roots, Config.ClientCAs, Config.ClientAuth, Config.VerifyConnection, etc) while a Config is in-use is already a misuse of crypto/tls.
There was a related security issue with respect to using Config.Clone to clone a Config for mutation that retained the automatically generated session ticket keys for the cloned config (see #77113) that has since been resolved.
We also provide an advanced API, Config.SetSessionTicketKeys, which allows explicit user control over the session ticket keys, disabling our automatic generation and rotation of said keys. We believe making changes to the trust configuration of a Config but retaining the same keys is a misuse of this API (i.e. by not calling Config.SetSessionTicketKeys with a new set of keys after mutating the relevant trust configurations fields). Additionally, using the same set of session ticket keys across backends that are explicitly configured to have different trust configurations is a misuse of the API. I believe our documentation here is somewhat lacking and should be improved to detail that this is a rather sharp edged API, and should be used carefully with respect to these properties.
Skipping certificate chain verification is one of the main wins we get from session resumption, but it is worth considering if there is anything we can do to improve the security of resumption without reducing the performance. @rbqvq had some ideas here which are worth investigating.
cc @golang/security @FiloSottile
Activity
gabyhelp commentedon Jan 17, 2026
Related Issues
Related Code Changes
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
rbqvq commentedon Jan 17, 2026
Update at May 10 (after most application get patched):
Risk details
Cloneunexpected resumption for revoked bypassGetConfigForClientunexpected resumption for cross server name authenticate bypass (cross virtual host privilege bypass/escalation attack)Case: Traefik GHSA-gv8r-9rw9-9697
Reference: Nginx CVE-2025-23419
Reference: Cloudflare Blog for Nginx CVE-2025-23419
Analysis: cisagov/vulnrichment#291
Origin in Jan:
First, I need to point out the issues in #77113:
The vulnerability description doesn't explicitly state that sharing SessionTicketKeys can lead to session resumption between different trust configurations (whether it's config.Clone or SetSessionTicketKeys).
Most downstream applications are unaware that this could lead to authentication bypass across ClientCAs.
Furthermore, the related patch content and description are incorrect;
peerCertificatesdoes not include the RootCA, and it doesn't correctly check the RootCA's expiration.The correct approach is to use
verifiedChains.Additionally, the related checks don't consider clock rollback; it lacks a NotBefore check.
Breaking config.Clone causes QUIC-Go's TLS Resumption (including 0-RTT) to fail, resulting in massive performance regressions.
In addition, there are additional risk factors:
Most people are unaware of the differences in session recovery (including Teleport) between TLS 1.0-1.2 and TLS 1.3:
No master key is renegotiated, therefore the ticket validity period is fixed at 7 days.
The master key is renegotiated, and a new ticket with a 7-day validity period is issued (forward security).
The ticket validity period is not linked to identity freshness, but rather to key freshness.
With public CAs no longer issuing new client certificates, private PKIs typically have validity periods of several years or even longer (they are not subject to CA/B restrictions).
But Roland said:
Downstream applications, such as Teleport #62896, bypass the restrictions of config.Clone by using a dangerous workaround involving manually shared keys.
Teleport said:
I do not believe that TLS resumption and persistent connections are equivalent in nature.
The migration of TCP connections to QUIC in modern network environments demonstrates the fragility of TCP.
Furthermore, cluster maintenance and traffic redirection are common, which can interrupt existing connections and cause persistent connections to fail.
Regarding #31641, normally Go teams need to go through a proposal process and leave a GODEBUG when making related changes.
I don't know why the team suddenly picked up this issue and merged it after it had been dormant for almost a year.
There are fewer than 30 discussions under #31641, and even fewer people know about the changes.
In a private email discussion, Roland said the following:
And
In the Go 1.21 Release Note, the release team, as always, categorized the change as a "minor library change."
I believe most users are unaware of the potential security risks it may pose.
Most Gophers do not consider "minor library changes" to affect security.
This seems to violate the principle of transparency in open-source governance,
and is even somewhat similar to the XZ Utils Backdoor.And, in the Go 16th Anniversary Blog
we discussed FIPS 140-3 compliance and the Secure-by-default principle:
I'm not sure if this issue will affect FIPS 140-3 compliance, although the crypto/tls documentation suggests it's not fully compliant.
Roland email reply:
as well as
I submitted a patch to the Golang security team to restore trust chain verification. However, they did not adopt it.
If we have verified it before, it compares the validity of all certificates and verifies whether the root certificate is still in our RootCA.
This is just a SHA224 and O(1) performance in the session resumption process and solves the problem of cross-trust domain resumption.
It also solves the connection interruption caused by VerifyConnection failing on session resumption.
The new behavior is that if the resumption does not meet the requirements, we will degrade to a full handshake without causing a loss of availability.
I will subsequently submit a CL (I previous submit to them) that solves the compatibility issues before and after Go 1.21 while maintaining performance and security.
I'm not satisfied with the fix.
The Golang security team is concerned about performance regression before.
But now, the current patch will cause large-scale QUIC-Go session resumption failures, and it's ineffective for advanced users manually setting session ticket keys, such as in clusters.
https://github.com/quic-go/quic-go/blob/master/internal/handshake/tls_config.go
Furthermore, I have concerns about a series of changes in Go 1.21 commits:
In the QUIC CL,
returnis missing aftersendAlertIn my previous report, a previous QUIC CL reviewer (Filippo) commented
And the Roland said
My said:
The reviewer doesn't understand the code they personally approved, and the maintainer hasn't checked the call chains of the relevant functions.
In #31641
And Roland said
These two cases indicate that Go 1.21 exhibits a continuous, systematic review and transparency failure.
Our Peer Review, designed to ensure supply chain security, appears to have failed.
I need @rsc @aclements @ianlancetaylor to be aware of this matter.
I can provide full email PDF for this.
Furthermore, Go 1.24 has one last release, and Go 1.26 is coming soon.
As a native FIPS 140-3 milestone, I think we should reach a conclusion as soon as possible before the merge window closes to avoid leaving any potential problems.
I suggest partially reversing the erroneous decision in #31641 and reverting the changes in #71773. I will provide a new CL for this.
The behavior of
config.Cloneshould be restored.The call to
VerifyPeerCertificatecan be restored in Go 1.27 (but I still recommend immediately reversing the relevant changes).rbqvq commentedon Jan 17, 2026
CC
crypto/tls: add minimal certificate verification on resumption
crypto/tls: add minimal certificate verification on resumption
crypto/tls: add minimal certificate verification on resumption
crypto/tls: add minimal certificate verification on resumption
rbqvq commentedon Jan 17, 2026
Technical details of the new CL
Regarding performance,
rootCA.Verify(opts)is a SHA-224 hash lookup in a CertPool is an O(1) operation.I forked the official stack and added many extra features to demonstrate my technical prowess with it.
Welcome to use it :)
This stack, as a kTLS implementation of MinIO/warp, should have been extensively tested.
https://gitlab.com/go-extension/tls
gopherbot commentedon Jan 17, 2026
Change https://go.dev/cl/737200 mentions this issue:
crypto/tls: add minimal certificate verification on resumptionrbqvq commentedon Jan 17, 2026
While the behavior of
VerifyPeerCertificatesduring resumption has been documented since Go 1.21, documentation of a behavior does not mitigate the inherent security risk of a Trust Anchor Bypass.Security regressions should not be grandfathered in simply because they have persisted through major versions.
The goal of the Go security team should be "Secure-by-Default," not "Inconsistent-by-Legacy."
I think consistency of behavior should be to become safer together, not to become less safe together.
In addition, the risk of the client and the server is not equal, the session is stateful for the client and stateless for the server.
marten-seemann commentedon Jan 17, 2026
I don’t think that certificate chains should be re-verified during resumption. Quite the opposite: resumption is designed such that it’s not necessary to retransmit the certificate chains in the first place.
It is a rather annoying quirk that crypto/tls saves the certificate chains in the session ticket, as it dramatically increases the size of the session ticket, which basically erases most of the benefits of session resumption (at least when looking at bytes transferred).
As far as I understand, this is done purely to fill out the field in the ConnectionState.
If we’re willing to touch this logic, I’d strongly advocate for NOT storing the certificate chain at all. Again, this is how TLS session resumption is supposed to work: you trust your prior self to have verified the certificate chain. The correct way to handle changes in your trust configuration is to reject session resumption in that case.
rbqvq commentedon Jan 17, 2026
Yes, I completely agree with your point, so my suggestion is to only verify the root certificate (currently, the minimal change is simply performing a SHA224 comparison using RootCA).
As for future work, I think that if VerifyPeerCertificate and VerifyConnection are not configured, then only the SHA224 fingerprint of the root certificate and a global NotBefore and NotAfter should be encoded (this might require adding an x509 API, which cannot be rolled back to the current minor version).
Of course, refusing to refresh the validity period for tickets that include client authentication, that forces a full handshake to be performed every 7 days (or even a smaller window of 1 day).
What are your thoughts on the changes to #77113 cause quic-go session resumption fails?
espadolini commentedon Jan 17, 2026
@rbqvq
To clarify, the aforementioned change we implemented to support Go 1.25.6+ was made with full awareness of the implications of using the same session ticket keys as the trusted roots change. It was a deliberate choice before, when cloning a
tls.Configimplicitly shared session keys (and we were using the same workaround used by quic-go as well to regenerate the automatically rotated keys right before cloning) and it is a deliberate choice now. Claiming that we are "unaware" of how we usecrypto/tlsis quite rude.While the documentation of
tls.Config.SetSessionTicketKeyscould be improved by stressing out the behavior of client cert verification on resumed connections and how that relates to potential changes in which client certs are trusted, I don't think the (documented and very reasonable) behavior should be changed, except maybe to allow opting out of additional verifications for the sake of performance, as @marten-seemann mentioned.@marten-seemann
I wonder if we can (or maybe must?) make this an additional
ClientAuthType, since it affects which certificates and validated chains are passed in to callbacks and returned inConnectionState.16 remaining items
aclements commentedon Jan 30, 2026
I've placed a temporary block on @rbqvq while the Go Project Stewards investigate reports of Go Code of Conduct violations across this the Go issue tracker, code review (1, 2), and private conversations.
FiloSottile commentedon Jan 30, 2026
CL 737700 landed the fix described above (which might still need adjusting due to #77376).
This discussion produced some follow-up issues for Go 1.27 (or later):
We can close this as the short-term concern is addressed, and continue the discussion in the specific issues.
aclements commentedon Jan 30, 2026
Thank you to @FiloSottile , @rolandshoemaker , and everyone else involved for teasing apart this incredibly complex issue.
crypto/tls: document resumption behavior across Configs
gopherbot commentedon Feb 2, 2026
Change https://go.dev/cl/741180 mentions this issue:
[release-branch.go1.24] crypto/tls: document resumption behavior across Configsgopherbot commentedon Feb 2, 2026
Change https://go.dev/cl/741181 mentions this issue:
[release-branch.go1.26] crypto/tls: document resumption behavior across Configsgopherbot commentedon Feb 2, 2026
Change https://go.dev/cl/741200 mentions this issue:
[release-branch.go1.25] crypto/tls: document resumption behavior across Configs[release-branch.go1.26] crypto/tls: document resumption behavior acro…
[release-branch.go1.24] crypto/tls: document resumption behavior acro…
[release-branch.go1.25] crypto/tls: document resumption behavior acro…
[notify] go: updated to version 1.25.7; security update
security: upgrade to Go 1.25.7 to fix CVE-2025-68121
security: upgrade to Go 1.25.7 to fix CVE-2025-68121 (#490)
[release-branch.go1.26] crypto/tls: document resumption behavior acro…
[release-branch.go1.26] crypto/tls: document resumption behavior acro…