From 2a781cb5d2dc0f8df290033699d1918d9eac3098 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sat, 21 May 2022 19:28:05 -0700 Subject: crypto: remove ocsp, add acme wrapper --- crypto/acme/autocert/autocert.go | 85 +++++++++++++++++++++++++ crypto/tls/ocsp.go | 94 --------------------------- crypto/tls/ocsp_manager.go | 134 --------------------------------------- 3 files changed, 85 insertions(+), 228 deletions(-) create mode 100644 crypto/acme/autocert/autocert.go delete mode 100644 crypto/tls/ocsp.go delete mode 100644 crypto/tls/ocsp_manager.go diff --git a/crypto/acme/autocert/autocert.go b/crypto/acme/autocert/autocert.go new file mode 100644 index 0000000..f99c36e --- /dev/null +++ b/crypto/acme/autocert/autocert.go @@ -0,0 +1,85 @@ +package autocert + +import ( + "context" + "crypto/tls" + "fmt" + "net" + + "code.crute.us/mcrute/golib/service" + "golang.org/x/net/idna" +) + +type InfoReporter interface { + Info(...interface{}) +} + +type CertProvider interface { + GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) + Listener() net.Listener + TLSConfig() *tls.Config +} + +type PrimingCertProvider interface { + CertProvider + PrimeCache() error + PrimingReporter(InfoReporter) service.RunnerFunc +} + +type ACMEHostList struct { + hosts map[string]bool +} + +func NewACMEHostList(hosts ...string) *ACMEHostList { + hl := make(map[string]bool, len(hosts)) + for _, h := range hosts { + if h, err := idna.Lookup.ToASCII(h); err == nil { + hl[h] = true + } + } + return &ACMEHostList{hl} +} + +func (h *ACMEHostList) HostPolicy(_ context.Context, host string) error { + if !h.hosts[host] { + return fmt.Errorf("acme/autocert: host %q not configured in HostWhitelist", host) + } + return nil +} + +func (h *ACMEHostList) Hosts() []string { + out := []string{} + for k, _ := range h.hosts { + out = append(out, k) + } + return out +} + +// PrimeCache makes a request to the autocert.Manager.GetCertificate function +// for each host in the host list. It will request ECDSA certificates. This +// will fill a certificate cache if it has not yet been filled and has the side +// effect of starting renewal goroutines for each allowed host. +// +// If reportcb is not nil it will be called with a log message for each host +// that is being primed. +func (h *ACMEHostList) PrimeCache(m CertProvider, report chan<- string) error { + for k, _ := range h.hosts { + if report != nil { + report <- fmt.Sprintf("Priming certificate cache for %s", k) + } + // ECDSA Version + if _, err := m.GetCertificate(&tls.ClientHelloInfo{ + ServerName: k, + SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256}, + }); err != nil { + return err + } + // RSA Version + if _, err := m.GetCertificate(&tls.ClientHelloInfo{ + ServerName: k, + }); err != nil { + return err + } + } + return nil +} diff --git a/crypto/tls/ocsp.go b/crypto/tls/ocsp.go deleted file mode 100644 index 9ae9828..0000000 --- a/crypto/tls/ocsp.go +++ /dev/null @@ -1,94 +0,0 @@ -package tls - -import ( - "bytes" - "crypto/tls" - "crypto/x509" - "fmt" - "io" - "net/http" - - "golang.org/x/crypto/ocsp" -) - -func GetOcspResponse(chain *tls.Certificate) ([]byte, *ocsp.Response, error) { - var certs []*x509.Certificate - for _, c := range chain.Certificate { - cert, err := x509.ParseCertificate(c) - if err != nil { - return nil, nil, err - } - certs = append(certs, cert) - } - if len(certs) == 0 { - return nil, nil, fmt.Errorf("no certificates found in bundle") - } - - // We expect the certificate slice to be ordered downwards the chain. - // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it, - // which should always be the first two certificates. If there's no - // OCSP server listed in the leaf cert, there's nothing to do. And if - // we have only one certificate so far, we need to get the issuer cert. - leaf := certs[0] - if len(leaf.OCSPServer) == 0 { - return nil, nil, fmt.Errorf("no OCSP server specified in certificate") - } - - if len(certs) == 1 { - if len(leaf.IssuingCertificateURL) == 0 { - return nil, nil, fmt.Errorf("no URL to issuing certificate") - } - - resp, err := http.Get(leaf.IssuingCertificateURL[0]) - if err != nil { - return nil, nil, fmt.Errorf("getting issuer certificate: %w", err) - } - defer resp.Body.Close() - - issuerBytes, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) - if err != nil { - return nil, nil, fmt.Errorf("reading issuer certificate: %w", err) - } - - issuer, err := x509.ParseCertificate(issuerBytes) - if err != nil { - return nil, nil, fmt.Errorf("parsing issuer certificate: %w", err) - } - - certs = append(certs, issuer) - } - - issuer := certs[1] - - req, err := ocsp.CreateRequest(leaf, issuer, nil) - if err != nil { - return nil, nil, fmt.Errorf("creating OCSP request: %w", err) - } - - httpRes, err := http.Post(leaf.OCSPServer[0], "application/ocsp-request", bytes.NewReader(req)) - if err != nil { - return nil, nil, fmt.Errorf("making OCSP request: %w", err) - } - defer httpRes.Body.Close() - - rawRes, err := io.ReadAll(io.LimitReader(httpRes.Body, 1024*1024)) - if err != nil { - return nil, nil, fmt.Errorf("reading OCSP response: %w", err) - } - - res, err := ocsp.ParseResponse(rawRes, issuer) - if err != nil { - return nil, nil, fmt.Errorf("parsing OCSP response: %w", err) - } - - if res.Status != ocsp.Good { - return nil, nil, fmt.Errorf("invalid: OCSP response was not of Good status") - } - - // This is invalid, the response expires after the certificate - if res.NextUpdate.After(leaf.NotAfter) { - return nil, nil, fmt.Errorf("invalid: OCSP response valid after certificate expiration") - } - - return rawRes, res, nil -} diff --git a/crypto/tls/ocsp_manager.go b/crypto/tls/ocsp_manager.go deleted file mode 100644 index dac4c5e..0000000 --- a/crypto/tls/ocsp_manager.go +++ /dev/null @@ -1,134 +0,0 @@ -package tls - -import ( - "context" - "crypto/tls" - "errors" - "fmt" - "sync" - "time" - - "golang.org/x/crypto/ocsp" -) - -type OcspError struct { - Err error - AtBoot bool -} - -func (e OcspError) Error() string { - return e.Err.Error() -} - -func (e OcspError) Unwrap() error { - return e.Err -} - -type OcspLogger interface { - Info(...interface{}) - Errorf(string, ...interface{}) -} - -func OcspErrorLogger(l OcspLogger, c <-chan OcspError) func(context.Context, *sync.WaitGroup) error { - return func(ctx context.Context, wg *sync.WaitGroup) error { - wg.Add(1) - defer wg.Done() - - for { - select { - case err := <-c: - l.Errorf("Error in OCSP stapling: %w", errors.Unwrap(err)) - case <-ctx.Done(): - l.Info("Shutting down OCSP logger") - return nil - } - } - } -} - -type OcspManager struct { - CertPath, KeyPath string - Errors chan<- OcspError - cert *tls.Certificate - ocspRes *ocsp.Response - sync.RWMutex -} - -func (m *OcspManager) loadCert() error { - cert, err := tls.LoadX509KeyPair(m.CertPath, m.KeyPath) - if err != nil { - return err - } - m.Lock() - m.cert = &cert - m.Unlock() - - return nil -} - -func (m *OcspManager) stapleCert() error { - // This makes a network request to an unknown server so don't hold the full - // lock while this is happening - m.RLock() - raw, ocspRes, err := GetOcspResponse(m.cert) - if err != nil { - return err - } - m.RUnlock() - - m.Lock() - m.cert.OCSPStaple = raw - m.ocspRes = ocspRes - m.Unlock() - - return nil -} - -func (m *OcspManager) Init() error { - // All functions called here will handle locking themselves - if err := m.loadCert(); err != nil { - return err - } - - if err := m.stapleCert(); err != nil { - return err - } - - return nil -} - -func (m *OcspManager) Run(ctx context.Context, wg *sync.WaitGroup) error { - wg.Add(1) - defer wg.Done() - - t := time.NewTimer(m.ocspRes.NextUpdate.Sub(time.Now()) - time.Hour) - - for { - select { - case <-t.C: - if err := m.stapleCert(); err != nil { - if m.Errors != nil { - m.Errors <- OcspError{err, false} - } - t.Reset(time.Hour) - continue - } - // We own this object and only we write it, no need to lock - t.Reset(m.ocspRes.NextUpdate.Sub(time.Now()) - time.Hour) - case <-ctx.Done(): - return nil - } - } -} - -// TODO: TLS.GetCertificate for dyanmic certs for LE (cache these) -func (m *OcspManager) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) { - m.RLock() - defer m.RUnlock() - - if m.cert != nil { - return m.cert, nil - } - - return nil, fmt.Errorf("OCSP manager has no certificate stored") -} -- cgit v1.2.3