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
81 lines
2.4 KiB
Go
81 lines
2.4 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package bakedroots
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"flag"
|
|
"io"
|
|
"net/http"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"tailscale.com/util/cibuild"
|
|
)
|
|
|
|
func TestBakedInRoots(t *testing.T) {
|
|
ResetForTest(t, nil)
|
|
p := Get()
|
|
got := p.Subjects()
|
|
if len(got) != 4 {
|
|
t.Errorf("subjects = %v; want 4", len(got))
|
|
}
|
|
|
|
// TODO(bradfitz): is there a way to easily make this test prettier without
|
|
// writing a DER decoder? I'm not seeing how.
|
|
var name []string
|
|
for _, der := range got {
|
|
name = append(name, string(der))
|
|
}
|
|
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 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) {
|
|
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)
|
|
})
|
|
}
|
|
}
|