tsd, all: add Sys.ExtraRootCAs, plumb through TLS dial paths

Add ExtraRootCAs *x509.CertPool to tsd.System and plumb it through
the control client, noise transport, DERP, and wgengine layers so
that platforms like Android can inject user-installed CA certificates
into Go's TLS verification.

tlsdial.Config now honors base.RootCAs as additional trusted roots,
tried after system roots and before the baked-in LetsEncrypt fallback.
SetConfigExpectedCert gets the same treatment for domain-fronted DERP.

The Android client will set sys.ExtraRootCAs with a pool built from
x509.SystemCertPool + user-installed certs obtained via the Android
KeyStore API, replacing the current SSL_CERT_DIR environment variable
approach.

Updates #8085

Change-Id: Iecce0fd140cd5aa0331b124e55a7045e24d8e0c2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-04-07 19:09:19 +00:00
committed by Brad Fitzpatrick
parent c4cb5eb809
commit a182b864ac
13 changed files with 108 additions and 4 deletions
+56 -4
View File
@@ -59,15 +59,26 @@ var mitmBlockWarnable = health.Register(&health.Warnable{
// the baked-in LetsEncrypt roots as a fallback validation method.
//
// If base is non-nil, it's cloned as the base config before
// being configured and returned.
// being configured and returned. If base.RootCAs is non-nil, it is
// used as an additional set of trusted roots (after system roots,
// before baked-in LetsEncrypt roots). This is used on Android to
// trust user-installed CA certificates that Go's crypto/x509
// does not see.
//
// If ht is non-nil, it's used to report health errors.
func Config(ht *health.Tracker, base *tls.Config) *tls.Config {
var extraRoots *x509.CertPool
if base != nil {
extraRoots = base.RootCAs
}
var conf *tls.Config
if base == nil {
conf = new(tls.Config)
} else {
conf = base.Clone()
}
conf.RootCAs = nil // we do our own verification in VerifyConnection
// Note: we do NOT set conf.ServerName here (as we accidentally did
// previously), as this path is also used when dialing an HTTPS proxy server
@@ -165,7 +176,26 @@ func Config(ht *health.Tracker, base *tls.Config) *tls.Config {
if debug() {
log.Printf("tlsdial(sys %q): %v", dialedHost, errSys)
}
if !buildfeatures.HasBakedRoots || (errSys == nil && !debug()) {
if errSys == nil && !debug() {
return nil
}
// If extra roots were provided (e.g. user-installed CAs on
// Android), try those next.
if extraRoots != nil {
opts.Roots = extraRoots
_, errExtra := cs.PeerCertificates[0].Verify(opts)
if debug() {
log.Printf("tlsdial(extra %q): %v", dialedHost, errExtra)
}
if errExtra == nil {
atomic.AddInt32(&counterFallbackOK, 1)
return nil
}
opts.Roots = nil // reset for baked roots check
}
if !buildfeatures.HasBakedRoots {
return errSys
}
@@ -178,7 +208,11 @@ func Config(ht *health.Tracker, base *tls.Config) *tls.Config {
} else if bakedErr != nil {
if _, loaded := tlsdialWarningPrinted.LoadOrStore(dialedHost, true); !loaded {
if errSys != nil {
log.Printf("tlsdial: error: server cert for %q failed both system roots & Let's Encrypt root validation", dialedHost)
if extraRoots != nil {
log.Printf("tlsdial: error: server cert for %q failed system roots, extra roots & Let's Encrypt root validation", dialedHost)
} else {
log.Printf("tlsdial: error: server cert for %q failed both system roots & Let's Encrypt root validation", dialedHost)
}
}
}
}
@@ -213,6 +247,10 @@ func SetConfigExpectedCert(c *tls.Config, certDNSName string) {
c.ServerName = certDNSName
return
}
extraRoots := c.RootCAs
c.RootCAs = nil
// Set InsecureSkipVerify to prevent crypto/tls from doing its
// own cert verification, but do the same work that it'd do
// (but using certDNSName) in the VerifyPeerCertificate hook.
@@ -242,7 +280,21 @@ func SetConfigExpectedCert(c *tls.Config, certDNSName string) {
if debug() {
log.Printf("tlsdial(sys %q/%q): %v", c.ServerName, certDNSName, errSys)
}
if !buildfeatures.HasBakedRoots || errSys == nil {
if errSys == nil {
return nil
}
if extraRoots != nil {
opts.Roots = extraRoots
_, errExtra := certs[0].Verify(opts)
if debug() {
log.Printf("tlsdial(extra %q/%q): %v", c.ServerName, certDNSName, errExtra)
}
if errExtra == nil {
return nil
}
opts.Roots = nil
}
if !buildfeatures.HasBakedRoots {
return errSys
}
opts.Roots = bakedroots.Get()