From 8c5b1d33cc3d4af0b7c0e0fc37a90ef80ba25fe5 Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Wed, 10 Nov 2021 22:16:10 -0800 Subject: Nest tls package in crypto, split ocsp --- crypto/tls/ocsp.go | 94 +++++++++++++++++++ crypto/tls/ocsp_manager.go | 134 +++++++++++++++++++++++++++ tls/ocsp.go | 220 --------------------------------------------- 3 files changed, 228 insertions(+), 220 deletions(-) create mode 100644 crypto/tls/ocsp.go create mode 100644 crypto/tls/ocsp_manager.go delete mode 100644 tls/ocsp.go diff --git a/crypto/tls/ocsp.go b/crypto/tls/ocsp.go new file mode 100644 index 0000000..9ae9828 --- /dev/null +++ b/crypto/tls/ocsp.go @@ -0,0 +1,94 @@ +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 new file mode 100644 index 0000000..dac4c5e --- /dev/null +++ b/crypto/tls/ocsp_manager.go @@ -0,0 +1,134 @@ +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") +} diff --git a/tls/ocsp.go b/tls/ocsp.go deleted file mode 100644 index b80764f..0000000 --- a/tls/ocsp.go +++ /dev/null @@ -1,220 +0,0 @@ -package tls - -import ( - "bytes" - "context" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io" - "net/http" - "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") -} - -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 -} -- cgit v1.2.3