aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2022-05-21 19:28:05 -0700
committerMike Crute <mike@crute.us>2022-05-21 19:28:05 -0700
commit2a781cb5d2dc0f8df290033699d1918d9eac3098 (patch)
treeb2a4decc382997e42dbe863e7613a4dae08e0d7c
parent2d1a293315de90b31269b92ec854a6c3b32a0302 (diff)
downloadgolib-2a781cb5d2dc0f8df290033699d1918d9eac3098.tar.bz2
golib-2a781cb5d2dc0f8df290033699d1918d9eac3098.tar.xz
golib-2a781cb5d2dc0f8df290033699d1918d9eac3098.zip
crypto: remove ocsp, add acme wrapper
-rw-r--r--crypto/acme/autocert/autocert.go85
-rw-r--r--crypto/tls/ocsp.go94
-rw-r--r--crypto/tls/ocsp_manager.go134
3 files changed, 85 insertions, 228 deletions
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 @@
1package autocert
2
3import (
4 "context"
5 "crypto/tls"
6 "fmt"
7 "net"
8
9 "code.crute.us/mcrute/golib/service"
10 "golang.org/x/net/idna"
11)
12
13type InfoReporter interface {
14 Info(...interface{})
15}
16
17type CertProvider interface {
18 GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error)
19 Listener() net.Listener
20 TLSConfig() *tls.Config
21}
22
23type PrimingCertProvider interface {
24 CertProvider
25 PrimeCache() error
26 PrimingReporter(InfoReporter) service.RunnerFunc
27}
28
29type ACMEHostList struct {
30 hosts map[string]bool
31}
32
33func NewACMEHostList(hosts ...string) *ACMEHostList {
34 hl := make(map[string]bool, len(hosts))
35 for _, h := range hosts {
36 if h, err := idna.Lookup.ToASCII(h); err == nil {
37 hl[h] = true
38 }
39 }
40 return &ACMEHostList{hl}
41}
42
43func (h *ACMEHostList) HostPolicy(_ context.Context, host string) error {
44 if !h.hosts[host] {
45 return fmt.Errorf("acme/autocert: host %q not configured in HostWhitelist", host)
46 }
47 return nil
48}
49
50func (h *ACMEHostList) Hosts() []string {
51 out := []string{}
52 for k, _ := range h.hosts {
53 out = append(out, k)
54 }
55 return out
56}
57
58// PrimeCache makes a request to the autocert.Manager.GetCertificate function
59// for each host in the host list. It will request ECDSA certificates. This
60// will fill a certificate cache if it has not yet been filled and has the side
61// effect of starting renewal goroutines for each allowed host.
62//
63// If reportcb is not nil it will be called with a log message for each host
64// that is being primed.
65func (h *ACMEHostList) PrimeCache(m CertProvider, report chan<- string) error {
66 for k, _ := range h.hosts {
67 if report != nil {
68 report <- fmt.Sprintf("Priming certificate cache for %s", k)
69 }
70 // ECDSA Version
71 if _, err := m.GetCertificate(&tls.ClientHelloInfo{
72 ServerName: k,
73 SignatureSchemes: []tls.SignatureScheme{tls.ECDSAWithP256AndSHA256},
74 }); err != nil {
75 return err
76 }
77 // RSA Version
78 if _, err := m.GetCertificate(&tls.ClientHelloInfo{
79 ServerName: k,
80 }); err != nil {
81 return err
82 }
83 }
84 return nil
85}
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 @@
1package tls
2
3import (
4 "bytes"
5 "crypto/tls"
6 "crypto/x509"
7 "fmt"
8 "io"
9 "net/http"
10
11 "golang.org/x/crypto/ocsp"
12)
13
14func GetOcspResponse(chain *tls.Certificate) ([]byte, *ocsp.Response, error) {
15 var certs []*x509.Certificate
16 for _, c := range chain.Certificate {
17 cert, err := x509.ParseCertificate(c)
18 if err != nil {
19 return nil, nil, err
20 }
21 certs = append(certs, cert)
22 }
23 if len(certs) == 0 {
24 return nil, nil, fmt.Errorf("no certificates found in bundle")
25 }
26
27 // We expect the certificate slice to be ordered downwards the chain.
28 // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it,
29 // which should always be the first two certificates. If there's no
30 // OCSP server listed in the leaf cert, there's nothing to do. And if
31 // we have only one certificate so far, we need to get the issuer cert.
32 leaf := certs[0]
33 if len(leaf.OCSPServer) == 0 {
34 return nil, nil, fmt.Errorf("no OCSP server specified in certificate")
35 }
36
37 if len(certs) == 1 {
38 if len(leaf.IssuingCertificateURL) == 0 {
39 return nil, nil, fmt.Errorf("no URL to issuing certificate")
40 }
41
42 resp, err := http.Get(leaf.IssuingCertificateURL[0])
43 if err != nil {
44 return nil, nil, fmt.Errorf("getting issuer certificate: %w", err)
45 }
46 defer resp.Body.Close()
47
48 issuerBytes, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
49 if err != nil {
50 return nil, nil, fmt.Errorf("reading issuer certificate: %w", err)
51 }
52
53 issuer, err := x509.ParseCertificate(issuerBytes)
54 if err != nil {
55 return nil, nil, fmt.Errorf("parsing issuer certificate: %w", err)
56 }
57
58 certs = append(certs, issuer)
59 }
60
61 issuer := certs[1]
62
63 req, err := ocsp.CreateRequest(leaf, issuer, nil)
64 if err != nil {
65 return nil, nil, fmt.Errorf("creating OCSP request: %w", err)
66 }
67
68 httpRes, err := http.Post(leaf.OCSPServer[0], "application/ocsp-request", bytes.NewReader(req))
69 if err != nil {
70 return nil, nil, fmt.Errorf("making OCSP request: %w", err)
71 }
72 defer httpRes.Body.Close()
73
74 rawRes, err := io.ReadAll(io.LimitReader(httpRes.Body, 1024*1024))
75 if err != nil {
76 return nil, nil, fmt.Errorf("reading OCSP response: %w", err)
77 }
78
79 res, err := ocsp.ParseResponse(rawRes, issuer)
80 if err != nil {
81 return nil, nil, fmt.Errorf("parsing OCSP response: %w", err)
82 }
83
84 if res.Status != ocsp.Good {
85 return nil, nil, fmt.Errorf("invalid: OCSP response was not of Good status")
86 }
87
88 // This is invalid, the response expires after the certificate
89 if res.NextUpdate.After(leaf.NotAfter) {
90 return nil, nil, fmt.Errorf("invalid: OCSP response valid after certificate expiration")
91 }
92
93 return rawRes, res, nil
94}
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 @@
1package tls
2
3import (
4 "context"
5 "crypto/tls"
6 "errors"
7 "fmt"
8 "sync"
9 "time"
10
11 "golang.org/x/crypto/ocsp"
12)
13
14type OcspError struct {
15 Err error
16 AtBoot bool
17}
18
19func (e OcspError) Error() string {
20 return e.Err.Error()
21}
22
23func (e OcspError) Unwrap() error {
24 return e.Err
25}
26
27type OcspLogger interface {
28 Info(...interface{})
29 Errorf(string, ...interface{})
30}
31
32func OcspErrorLogger(l OcspLogger, c <-chan OcspError) func(context.Context, *sync.WaitGroup) error {
33 return func(ctx context.Context, wg *sync.WaitGroup) error {
34 wg.Add(1)
35 defer wg.Done()
36
37 for {
38 select {
39 case err := <-c:
40 l.Errorf("Error in OCSP stapling: %w", errors.Unwrap(err))
41 case <-ctx.Done():
42 l.Info("Shutting down OCSP logger")
43 return nil
44 }
45 }
46 }
47}
48
49type OcspManager struct {
50 CertPath, KeyPath string
51 Errors chan<- OcspError
52 cert *tls.Certificate
53 ocspRes *ocsp.Response
54 sync.RWMutex
55}
56
57func (m *OcspManager) loadCert() error {
58 cert, err := tls.LoadX509KeyPair(m.CertPath, m.KeyPath)
59 if err != nil {
60 return err
61 }
62 m.Lock()
63 m.cert = &cert
64 m.Unlock()
65
66 return nil
67}
68
69func (m *OcspManager) stapleCert() error {
70 // This makes a network request to an unknown server so don't hold the full
71 // lock while this is happening
72 m.RLock()
73 raw, ocspRes, err := GetOcspResponse(m.cert)
74 if err != nil {
75 return err
76 }
77 m.RUnlock()
78
79 m.Lock()
80 m.cert.OCSPStaple = raw
81 m.ocspRes = ocspRes
82 m.Unlock()
83
84 return nil
85}
86
87func (m *OcspManager) Init() error {
88 // All functions called here will handle locking themselves
89 if err := m.loadCert(); err != nil {
90 return err
91 }
92
93 if err := m.stapleCert(); err != nil {
94 return err
95 }
96
97 return nil
98}
99
100func (m *OcspManager) Run(ctx context.Context, wg *sync.WaitGroup) error {
101 wg.Add(1)
102 defer wg.Done()
103
104 t := time.NewTimer(m.ocspRes.NextUpdate.Sub(time.Now()) - time.Hour)
105
106 for {
107 select {
108 case <-t.C:
109 if err := m.stapleCert(); err != nil {
110 if m.Errors != nil {
111 m.Errors <- OcspError{err, false}
112 }
113 t.Reset(time.Hour)
114 continue
115 }
116 // We own this object and only we write it, no need to lock
117 t.Reset(m.ocspRes.NextUpdate.Sub(time.Now()) - time.Hour)
118 case <-ctx.Done():
119 return nil
120 }
121 }
122}
123
124// TODO: TLS.GetCertificate for dyanmic certs for LE (cache these)
125func (m *OcspManager) GetCertificate(h *tls.ClientHelloInfo) (*tls.Certificate, error) {
126 m.RLock()
127 defer m.RUnlock()
128
129 if m.cert != nil {
130 return m.cert, nil
131 }
132
133 return nil, fmt.Errorf("OCSP manager has no certificate stored")
134}