net/bakedroots: add LetsEncrypt Generation Y roots (YE, YR)
LetsEncrypt announced its new "Generation Y" root hierarchy on 2025-11-24 and switched its default ACME profile to issue from the new roots in May 2026. Our baked-in fallback root store only contained the Generation X roots (ISRG Root X1 and X2), so chains terminating at the new ISRG Root YE (ECDSA P-384) or ISRG Root YR (RSA 4096) roots failed to verify when the system roots were also missing them. Add both new self-signed roots, fetched from https://letsencrypt.org/certificates/ (valid 2025-09-03 to 2045-09-02). Also add a live network test, run by default in CI only (or with --run-live-lets-encrypt-test), that verifies the baked-in roots alone are sufficient to validate LetsEncrypt's per-root test endpoints for X1, X2, YE, and YR. Fixes #20527 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: Ic69076d8ae5aae08db167095745e92c53357afe3
This commit is contained in:
committed by
Brad Fitzpatrick
parent
82cfea90ca
commit
f65372c9ba
@@ -15,13 +15,11 @@ import (
|
|||||||
|
|
||||||
// Get returns the baked-in roots.
|
// Get returns the baked-in roots.
|
||||||
//
|
//
|
||||||
// As of 2025-01-21, this includes only the LetsEncrypt ISRG Root X1 & X2 roots.
|
// As of 2026-07-20, this includes only the LetsEncrypt ISRG roots:
|
||||||
|
// the Generation X roots (X1 & X2) and the Generation Y roots (YE & YR).
|
||||||
func Get() *x509.CertPool {
|
func Get() *x509.CertPool {
|
||||||
roots.once.Do(func() {
|
roots.once.Do(func() {
|
||||||
roots.parsePEM(append(
|
roots.parsePEM([]byte(letsEncryptX1 + letsEncryptX2 + letsEncryptYE + letsEncryptYR))
|
||||||
[]byte(letsEncryptX1),
|
|
||||||
letsEncryptX2...,
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
return roots.p
|
return roots.p
|
||||||
}
|
}
|
||||||
@@ -145,3 +143,63 @@ tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
|
|||||||
/q4AaOeMSQ+2b1tbFfLn
|
/q4AaOeMSQ+2b1tbFfLn
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// letsEncryptYE is the ISRG Root YE, part of LetsEncrypt's
|
||||||
|
// "Generation Y" hierarchy announced 2025-11-24.
|
||||||
|
//
|
||||||
|
// Subject: C = US, O = ISRG, CN = Root YE
|
||||||
|
// Key type: ECDSA P-384
|
||||||
|
// Validity: 2025-09-03 to 2045-09-02
|
||||||
|
const letsEncryptYE = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIB2TCCAWCgAwIBAgIRAKQCa6LvbHwg1AR+XmWmk4AwCgYIKoZIzj0EAwMwLjEL
|
||||||
|
MAkGA1UEBhMCVVMxDTALBgNVBAoTBElTUkcxEDAOBgNVBAMTB1Jvb3QgWUUwHhcN
|
||||||
|
MjUwOTAzMDAwMDAwWhcNNDUwOTAyMjM1OTU5WjAuMQswCQYDVQQGEwJVUzENMAsG
|
||||||
|
A1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZRTB2MBAGByqGSM49AgEGBSuBBAAi
|
||||||
|
A2IABDwS/6vhrcVqcbBo+wgdI3fwn9x7DNJJOY/lTOti0vkwuRN87RhEhTH17E7X
|
||||||
|
yFjWsPYhIPt/wzOqxTd2b+4ZJNy9ID04YywF9U5zasDVyGSNErVNtz8uSGh5izW8
|
||||||
|
7j77GaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O
|
||||||
|
BBYEFKPIJlqOoUzQNWP8myPIOq5W809WMAoGCCqGSM49BAMDA2cAMGQCMHhMr8N9
|
||||||
|
LdL1VQKs9BdV81r76eXRB6mtjuNjzk6/lBsPNToWLTDzGYgtQKO1jl63uAIwGV7m
|
||||||
|
onyF377c+MM1oqVNs17sgu7F9YKZwgLmVbeOMDbKAXHtKMDLbiGllCcs8f47
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|
||||||
|
// letsEncryptYR is the ISRG Root YR, part of LetsEncrypt's
|
||||||
|
// "Generation Y" hierarchy announced 2025-11-24.
|
||||||
|
//
|
||||||
|
// Subject: C = US, O = ISRG, CN = Root YR
|
||||||
|
// Key type: RSA 4096
|
||||||
|
// Validity: 2025-09-03 to 2045-09-02
|
||||||
|
const letsEncryptYR = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
sI1ANRYvqSFC2X1VRZfDg+wD6E21BccmifG4yWc=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
|||||||
@@ -4,16 +4,23 @@
|
|||||||
package bakedroots
|
package bakedroots
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tailscale.com/util/cibuild"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBakedInRoots(t *testing.T) {
|
func TestBakedInRoots(t *testing.T) {
|
||||||
ResetForTest(t, nil)
|
ResetForTest(t, nil)
|
||||||
p := Get()
|
p := Get()
|
||||||
got := p.Subjects()
|
got := p.Subjects()
|
||||||
if len(got) != 2 {
|
if len(got) != 4 {
|
||||||
t.Errorf("subjects = %v; want 2", len(got))
|
t.Errorf("subjects = %v; want 4", len(got))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(bradfitz): is there a way to easily make this test prettier without
|
// TODO(bradfitz): is there a way to easily make this test prettier without
|
||||||
@@ -25,8 +32,49 @@ func TestBakedInRoots(t *testing.T) {
|
|||||||
want := []string{
|
want := []string{
|
||||||
"0O1\v0\t\x06\x03U\x04\x06\x13\x02US1)0'\x06\x03U\x04\n\x13 Internet Security Research Group1\x150\x13\x06\x03U\x04\x03\x13\fISRG Root X1",
|
"0O1\v0\t\x06\x03U\x04\x06\x13\x02US1)0'\x06\x03U\x04\n\x13 Internet Security Research Group1\x150\x13\x06\x03U\x04\x03\x13\fISRG Root X1",
|
||||||
"0O1\v0\t\x06\x03U\x04\x06\x13\x02US1)0'\x06\x03U\x04\n\x13 Internet Security Research Group1\x150\x13\x06\x03U\x04\x03\x13\fISRG Root X2",
|
"0O1\v0\t\x06\x03U\x04\x06\x13\x02US1)0'\x06\x03U\x04\n\x13 Internet Security Research Group1\x150\x13\x06\x03U\x04\x03\x13\fISRG Root X2",
|
||||||
|
"0.1\v0\t\x06\x03U\x04\x06\x13\x02US1\r0\v\x06\x03U\x04\n\x13\x04ISRG1\x100\x0e\x06\x03U\x04\x03\x13\aRoot YE",
|
||||||
|
"0.1\v0\t\x06\x03U\x04\x06\x13\x02US1\r0\v\x06\x03U\x04\n\x13\x04ISRG1\x100\x0e\x06\x03U\x04\x03\x13\aRoot YR",
|
||||||
}
|
}
|
||||||
if !slices.Equal(name, want) {
|
if !slices.Equal(name, want) {
|
||||||
t.Errorf("subjects = %q; want %q", name, want)
|
t.Errorf("subjects = %q; want %q", name, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var runLiveLetsEncryptTest = flag.Bool("run-live-lets-encrypt-test", cibuild.On(),
|
||||||
|
"run tests that hit LetsEncrypt's live test-certs endpoints over the network")
|
||||||
|
|
||||||
|
// TestLiveLetsEncrypt verifies that the baked-in roots alone (without any
|
||||||
|
// system roots) are sufficient to validate the certificate chains served by
|
||||||
|
// LetsEncrypt's per-root test endpoints.
|
||||||
|
func TestLiveLetsEncrypt(t *testing.T) {
|
||||||
|
if !*runLiveLetsEncryptTest {
|
||||||
|
t.Skip("skipping live network test; set --run-live-lets-encrypt-test to run")
|
||||||
|
}
|
||||||
|
ResetForTest(t, nil)
|
||||||
|
|
||||||
|
for _, host := range []string{
|
||||||
|
"valid.ye.test-certs.letsencrypt.org",
|
||||||
|
"valid.yr.test-certs.letsencrypt.org",
|
||||||
|
"valid.x2.test-certs.letsencrypt.org",
|
||||||
|
"valid.x1.test-certs.letsencrypt.org",
|
||||||
|
} {
|
||||||
|
t.Run(host, func(t *testing.T) {
|
||||||
|
c := &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DisableKeepAlives: true,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: Get(), // baked-in roots only; no system roots
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
res, err := c.Get("https://" + host + "/")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
io.Copy(io.Discard, res.Body)
|
||||||
|
t.Logf("status: %v", res.Status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user