diff --git a/net/bakedroots/bakedroots.go b/net/bakedroots/bakedroots.go index 70d947c6b..84978995f 100644 --- a/net/bakedroots/bakedroots.go +++ b/net/bakedroots/bakedroots.go @@ -15,13 +15,11 @@ import ( // 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 { roots.once.Do(func() { - roots.parsePEM(append( - []byte(letsEncryptX1), - letsEncryptX2..., - )) + roots.parsePEM([]byte(letsEncryptX1 + letsEncryptX2 + letsEncryptYE + letsEncryptYR)) }) return roots.p } @@ -145,3 +143,63 @@ tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 /q4AaOeMSQ+2b1tbFfLn -----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----- +MIIFKTCCAxGgAwIBAgIRAOxGNJNgz0sP+KmC2Tqpyj0wDQYJKoZIhvcNAQELBQAw +LjELMAkGA1UEBhMCVVMxDTALBgNVBAoTBElTUkcxEDAOBgNVBAMTB1Jvb3QgWVIw +HhcNMjUwOTAzMDAwMDAwWhcNNDUwOTAyMjM1OTU5WjAuMQswCQYDVQQGEwJVUzEN +MAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZUjCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANvGJnN78CTJdWL3+eGfsLN5TrNBJs+VH9hRXqRbwxu9 +sGNiB0BD1fcOxbSUQCJIM1xE13Db+5Cw1w0s0EBYsvuIP/6joF0w8cuImbgR1OGg +YbSQ4OpzI+DG8SGuTlcE873OCS+kh3srlo6vl43M5OJg4Aeo1sfHp6kTJDoIiFBN +JAY+OKfX/FUvYKuhjT+no49lmqmupSBI5PkBQiqrEGtWU5uxU/cQWHGu8jSjFBzn +ZqvbNPLMXMLFxCb3WTfrJBXXjqvWG+v4bjzxjjeAtOlU7qarRDvNOyAuQYLln904 +M+faKx8hnLCpJ15ZqaEgcNlY+9MMWcC5yvL2A2j3l9+2buggZX+dOE91zYmIdawT +vSZuVvlbRrAlLxIB6pwMBjneXCjYQ8+3BCCjssbSNpZU3hTcBDdhfAlEDlYr6pEa +tnMdmDT5BqnKC92bd0EhM1fbLHioLccLCuievT8ZkPhZrq7Mii7gNXAcUEAR8+lz +Yal+9zTg7C5DALyVOeG/CqfRAMn1KSHCR0NSA6P8tn/mGRlnCct5rtVCLnVySVpU +6H1qGg3DgTOuskf8eahTMiYbI5ezPJmO5ertalskQ1utp74+eDy92PI4ftHKTbq9 +IWhH4YZKh3WnJEIt+oQvlYZbY8tpEroKrFB6PFGzrJIDRyts4HqvuH52RFj2zv/B +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBTe51tg0CJtQCh9Pw0B/qS1UrRRlDANBgkqhkiG9w0BAQsFAAOCAgEAWHnf +713Bdkq7t5yN2dNIgQakUb94X9WuyhMEHHkgx4oDpSUlnG0w4g94MoqaEUE31ZjR +LU7L5LD1g9ujFHTQu8AD215AHMVQFbm6j8hQxdXHAzDajFNQnOlDJrLjzIx176oy +AjvUtejZx2NNmdb5fd0WGVGsCdoAJ3N8ozo7ajE8t6vfxStZb4BQ9WYJGHUDrv2N +i5tJF6CNiPnlzs3BUfECRbE4JSk+jvy8+VoGiFE8qsH/j78x2fjgQhAQFV7P7Zxy +dBTZ1wEkNpZNW2qnaK1SKBLa+xf6E06YRIq5uaI+HWH8SY1y5VbRgzq40EKg3yxP +06fz+uYAUIFJoLNfhwRCc3Q6pQVuMX3yAjHAes4gk4moGcLQ5p7HAh39yeylZc1J +41sx/jKwLIkPE6Rr1Nf4pxdsxf9SA4yOEiAkDgq04DVxn8hgYFdUtBCuiuVC2heA +EiqVEa+8QZjuw8Gj0EbHXcRd1nInvGqRS1o9Is7YBdQN57X1AYveGBNNqjICSb7c +awuw1EawTDrs13VUlJVEsbQ0/O/1aaV73mCdOQ8azqL2KTv1Ewu1xbquE2S+kdQU +To9TUwat3wUA6cwXh1EfpS/3fJ0aGah5hdpRyoCLDlsSn8tkrjMfFFX0viC+GxHc +sI1ANRYvqSFC2X1VRZfDg+wD6E21BccmifG4yWc= +-----END CERTIFICATE----- +` diff --git a/net/bakedroots/bakedroots_test.go b/net/bakedroots/bakedroots_test.go index 12a656d4e..a5a6193e1 100644 --- a/net/bakedroots/bakedroots_test.go +++ b/net/bakedroots/bakedroots_test.go @@ -4,16 +4,23 @@ 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) != 2 { - t.Errorf("subjects = %v; want 2", len(got)) + 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 @@ -25,8 +32,49 @@ func TestBakedInRoots(t *testing.T) { 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) + }) + } +}