139 lines
3.5 KiB
Go
139 lines
3.5 KiB
Go
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: ")
|
|
}
|