diff options
author | Mike Crute <mike@crute.us> | 2022-05-21 19:28:05 -0700 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2022-05-21 19:28:05 -0700 |
commit | 2a781cb5d2dc0f8df290033699d1918d9eac3098 (patch) | |
tree | b2a4decc382997e42dbe863e7613a4dae08e0d7c | |
parent | 2d1a293315de90b31269b92ec854a6c3b32a0302 (diff) | |
download | golib-2a781cb5d2dc0f8df290033699d1918d9eac3098.tar.bz2 golib-2a781cb5d2dc0f8df290033699d1918d9eac3098.tar.xz golib-2a781cb5d2dc0f8df290033699d1918d9eac3098.zip |
crypto: remove ocsp, add acme wrapper
-rw-r--r-- | crypto/acme/autocert/autocert.go | 85 | ||||
-rw-r--r-- | crypto/tls/ocsp.go | 94 | ||||
-rw-r--r-- | crypto/tls/ocsp_manager.go | 134 |
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 @@ | |||
1 | package autocert | ||
2 | |||
3 | import ( | ||
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 | |||
13 | type InfoReporter interface { | ||
14 | Info(...interface{}) | ||
15 | } | ||
16 | |||
17 | type CertProvider interface { | ||
18 | GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) | ||
19 | Listener() net.Listener | ||
20 | TLSConfig() *tls.Config | ||
21 | } | ||
22 | |||
23 | type PrimingCertProvider interface { | ||
24 | CertProvider | ||
25 | PrimeCache() error | ||
26 | PrimingReporter(InfoReporter) service.RunnerFunc | ||
27 | } | ||
28 | |||
29 | type ACMEHostList struct { | ||
30 | hosts map[string]bool | ||
31 | } | ||
32 | |||
33 | func 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 | |||
43 | func (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 | |||
50 | func (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. | ||
65 | func (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 @@ | |||
1 | package tls | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "crypto/tls" | ||
6 | "crypto/x509" | ||
7 | "fmt" | ||
8 | "io" | ||
9 | "net/http" | ||
10 | |||
11 | "golang.org/x/crypto/ocsp" | ||
12 | ) | ||
13 | |||
14 | func 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 @@ | |||
1 | package tls | ||
2 | |||
3 | import ( | ||
4 | "context" | ||
5 | "crypto/tls" | ||
6 | "errors" | ||
7 | "fmt" | ||
8 | "sync" | ||
9 | "time" | ||
10 | |||
11 | "golang.org/x/crypto/ocsp" | ||
12 | ) | ||
13 | |||
14 | type OcspError struct { | ||
15 | Err error | ||
16 | AtBoot bool | ||
17 | } | ||
18 | |||
19 | func (e OcspError) Error() string { | ||
20 | return e.Err.Error() | ||
21 | } | ||
22 | |||
23 | func (e OcspError) Unwrap() error { | ||
24 | return e.Err | ||
25 | } | ||
26 | |||
27 | type OcspLogger interface { | ||
28 | Info(...interface{}) | ||
29 | Errorf(string, ...interface{}) | ||
30 | } | ||
31 | |||
32 | func 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 | |||
49 | type OcspManager struct { | ||
50 | CertPath, KeyPath string | ||
51 | Errors chan<- OcspError | ||
52 | cert *tls.Certificate | ||
53 | ocspRes *ocsp.Response | ||
54 | sync.RWMutex | ||
55 | } | ||
56 | |||
57 | func (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 | |||
69 | func (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 | |||
87 | func (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 | |||
100 | func (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) | ||
125 | func (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 | } | ||