package main import ( "crypto/x509" "encoding/pem" "io" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) // resetTransport clears the memoised client so a test can change the // verification mode. func resetTransport(t *testing.T, mode caMode) { t.Helper() client, clientMode, caCurrent = nil, -1, mode opt.timeout = 10 t.Cleanup(func() { client, clientMode, caCurrent = nil, -1, caSystem opt.cacert, opt.insecure = "", false }) } // tlsServer returns a server with a certificate no public CA has signed, plus // that certificate as PEM - the stand-in for a root the system does not know. func tlsServer(t *testing.T) (*httptest.Server, []byte) { t.Helper() srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, `{"tag_name":"v1.2.3"}`) })) t.Cleanup(srv.Close) certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: srv.Certificate().Raw}) return srv, certPEM } func TestSystemStoreRejectsUnknownIssuer(t *testing.T) { srv, _ := tlsServer(t) resetTransport(t, caSystem) // Nothing to fall back to: the bundled list does not know it either. old := caBundle caBundle = nil t.Cleanup(func() { caBundle = old }) _, err := httpGet(srv.URL, nil, nil) if err == nil { t.Fatal("expected the system store to reject this certificate") } if !isUnknownAuthority(err) { t.Errorf("not recognised as an unknown authority: %v", err) } if !isPermanent(err) { t.Error("a rejected certificate must not be retried") } if !strings.Contains(err.Error(), "--cacert") { t.Errorf("error should point at the way out, got: %v", err) } } // The failure from the old box: the system store predates the issuer's root. // upd must notice, switch to its own list and carry on. func TestFallbackToBundledCAs(t *testing.T) { srv, certPEM := tlsServer(t) resetTransport(t, caSystem) old := caBundle caBundle = certPEM t.Cleanup(func() { caBundle = old }) resp, err := httpGet(srv.URL, nil, nil) if err != nil { t.Fatalf("fallback did not happen: %v", err) } if resp.status != http.StatusOK { t.Errorf("status %d", resp.status) } if caCurrent != caBundled { t.Errorf("mode is %v, want the bundled list", caCurrent) } } func TestCacertFile(t *testing.T) { srv, certPEM := tlsServer(t) path := filepath.Join(t.TempDir(), "ca.pem") if err := os.WriteFile(path, certPEM, 0o644); err != nil { t.Fatal(err) } resetTransport(t, caFile) opt.cacert = path if _, err := httpGet(srv.URL, nil, nil); err != nil { t.Fatalf("--cacert did not verify: %v", err) } // A bundle without the right root must still fail, and say which file. resetTransport(t, caFile) opt.cacert = filepath.Join(t.TempDir(), "empty.pem") os.WriteFile(opt.cacert, []byte("not a certificate\n"), 0o644) if _, err := httpGet(srv.URL, nil, nil); err == nil { t.Error("expected an error for a bundle without certificates") } else if !strings.Contains(err.Error(), "no certificates found") { t.Errorf("unexpected error: %v", err) } } func TestInsecureSkipsVerification(t *testing.T) { srv, _ := tlsServer(t) resetTransport(t, caNone) if _, err := httpGet(srv.URL, nil, nil); err != nil { t.Fatalf("--insecure should have connected anyway: %v", err) } } // The bundled list has to be a usable pool and carry the roots that the old // machines are missing - that is the whole point of embedding it. func TestBundledCAList(t *testing.T) { pool := bundledPool() if pool == nil { t.Fatal("the embedded CA bundle does not parse") } var subjects []string for block, rest := pem.Decode(caBundle); block != nil; block, rest = pem.Decode(rest) { cert, err := x509.ParseCertificate(block.Bytes) if err != nil { t.Fatalf("unparsable certificate in the bundle: %v", err) } subjects = append(subjects, cert.Subject.CommonName) } if len(subjects) < 100 { t.Errorf("only %d roots in the bundle, expected the full Mozilla list", len(subjects)) } for _, want := range []string{"ISRG Root X1", "ISRG Root X2"} { found := false for _, s := range subjects { if s == want { found = true } } if !found { t.Errorf("%s missing - Let's Encrypt hosts would still fail", want) } } } func TestPermanentClassification(t *testing.T) { // A DNS failure for a name that cannot exist is permanent, a plain // connection refused is not: the second one is worth another attempt. resetTransport(t, caSystem) if _, err := httpGet("https://no-such-host.invalid/x", nil, nil); err == nil { t.Fatal("expected a DNS failure") } else if !isPermanent(err) { t.Errorf("an unresolvable host should not be retried: %v", err) } }