package main import ( "crypto/tls" "crypto/x509" _ "embed" "errors" "fmt" "net" "os" "strings" ) // The Mozilla CA list as extracted by the curl project, refreshed with // // curl -o ca-bundle.pem https://curl.se/ca/cacert.pem // // It is not used unless the system store fails: on a machine old enough that // its ca-certificates package predates Let's Encrypt's ISRG roots, every // https:// forge is unreachable otherwise, and updating that package is often // no longer possible there. // //go:embed ca-bundle.pem var caBundle []byte // How certificates are verified. Starts at the system store and falls back one // step when that store turns out not to know the issuer. type caMode int const ( caSystem caMode = iota caBundled caFile caNone ) func (m caMode) String() string { switch m { case caBundled: return "bundled CA list" case caFile: return "--cacert " + caCertPath() case caNone: return "no verification" } return "system CA store" } // caCurrent is the mode every request uses; the fallback in httpGet moves it // forward at most once per run. var caCurrent = caSystem func bundledPool() *x509.CertPool { pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(caBundle) { return nil } return pool } func filePool(path string) (*x509.CertPool, error) { pem, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("cannot read --cacert %s: %w", path, err) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pem) { return nil, fmt.Errorf("no certificates found in %s", path) } return pool, nil } // tlsConfig builds the config for the mode currently in force. The system // store stays the default: it is the one the administrator controls. func tlsConfig(mode caMode) (*tls.Config, error) { switch mode { case caBundled: pool := bundledPool() if pool == nil { return nil, errors.New("the bundled CA list could not be parsed") } return &tls.Config{RootCAs: pool}, nil case caFile: pool, err := filePool(caCertPath()) if err != nil { return nil, err } return &tls.Config{RootCAs: pool}, nil case caNone: return &tls.Config{InsecureSkipVerify: true}, nil } return &tls.Config{}, nil } func caCertPath() string { if opt.cacert != "" { return expandTilde(opt.cacert) } return expandTilde(os.Getenv("UPD_CACERT")) } // A certificate the local store cannot chain up to a root it knows. This is // the failure the bundled list exists for; no other x509 problem (expired, // wrong host name) would be fixed by more roots. func isUnknownAuthority(err error) bool { var unknown x509.UnknownAuthorityError if errors.As(err, &unknown) { return true } // Some paths only carry the verifier's verdict as text. return strings.Contains(err.Error(), "certificate signed by unknown authority") || strings.Contains(err.Error(), "x509: failed to load system roots") } // Errors that will still be errors on the next attempt: retrying a rejected // certificate, an unresolvable host or a handshake the two sides cannot agree // on only makes the output longer. func isPermanent(err error) bool { var ( hostname x509.HostnameError invalid x509.CertificateInvalidError verify *tls.CertificateVerificationError record tls.RecordHeaderError dns *net.DNSError ) switch { case isUnknownAuthority(err), errors.As(err, &hostname), errors.As(err, &invalid), errors.As(err, &verify), errors.As(err, &record): return true case errors.As(err, &dns): return !dns.IsTemporary } msg := err.Error() return strings.Contains(msg, "tls: ") || strings.Contains(msg, "x509: ") }