Commit 8eefeb67 authored by Matthew Holt's avatar Matthew Holt

Begin improved OCSP stapling by persisting staple to disk

parent 5fb3c504
...@@ -10,7 +10,6 @@ import ( ...@@ -10,7 +10,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/xenolf/lego/acme"
"golang.org/x/crypto/ocsp" "golang.org/x/crypto/ocsp"
) )
...@@ -183,19 +182,13 @@ func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) { ...@@ -183,19 +182,13 @@ func makeCertificate(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
} }
} }
cert.NotAfter = leaf.NotAfter cert.NotAfter = leaf.NotAfter
cert.Certificate = tlsCert
// Staple OCSP err = stapleOCSP(&cert, certPEMBlock)
ocspBytes, ocspResp, err := acme.GetOCSPForCert(certPEMBlock)
if err != nil { if err != nil {
// An error here is not a problem because a certificate may simply log.Printf("[WARNING] Stapling OCSP: %v", err)
// not contain a link to an OCSP server. But we should log it anyway.
log.Printf("[WARNING] No OCSP stapling for %v: %v", cert.Names, err)
} else if ocspResp.Status == ocsp.Good {
tlsCert.OCSPStaple = ocspBytes
cert.OCSP = ocspResp
} }
cert.Certificate = tlsCert
return cert, nil return cert, nil
} }
......
...@@ -13,11 +13,19 @@ import ( ...@@ -13,11 +13,19 @@ import (
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"hash/fnv"
"io" "io"
"io/ioutil"
"log"
"math/big" "math/big"
"net" "net"
"os"
"path/filepath"
"time" "time"
"golang.org/x/crypto/ocsp"
"github.com/mholt/caddy"
"github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme"
) )
...@@ -59,7 +67,7 @@ func savePrivateKey(key crypto.PrivateKey) ([]byte, error) { ...@@ -59,7 +67,7 @@ func savePrivateKey(key crypto.PrivateKey) ([]byte, error) {
// stapleOCSP staples OCSP information to cert for hostname name. // stapleOCSP staples OCSP information to cert for hostname name.
// If you have it handy, you should pass in the PEM-encoded certificate // If you have it handy, you should pass in the PEM-encoded certificate
// bundle; otherwise the DER-encoded cert will have to be PEM-encoded. // bundle; otherwise the DER-encoded cert will have to be PEM-encoded.
// If you don't have the PEM blocks handy, just pass in nil. // If you don't have the PEM blocks already, just pass in nil.
// //
// Errors here are not necessarily fatal, it could just be that the // Errors here are not necessarily fatal, it could just be that the
// certificate doesn't have an issuer URL. // certificate doesn't have an issuer URL.
...@@ -73,13 +81,66 @@ func stapleOCSP(cert *Certificate, pemBundle []byte) error { ...@@ -73,13 +81,66 @@ func stapleOCSP(cert *Certificate, pemBundle []byte) error {
pemBundle = bundle.Bytes() pemBundle = bundle.Bytes()
} }
ocspBytes, ocspResp, err := acme.GetOCSPForCert(pemBundle) var ocspBytes []byte
var ocspResp *ocsp.Response
var ocspErr error
var gotNewOCSP bool
// First try to load OCSP staple from storage and see if
// we can still use it.
// TODO: Use Storage interface instead of disk directly
ocspFolder := filepath.Join(caddy.AssetsPath(), "ocsp")
ocspFileName := cert.Names[0] + "-" + fastHash(pemBundle)
ocspCachePath := filepath.Join(ocspFolder, ocspFileName)
cachedOCSP, err := ioutil.ReadFile(ocspCachePath)
if err == nil {
resp, err := ocsp.ParseResponse(cachedOCSP, nil)
if err == nil {
if freshOCSP(resp) {
// staple is still fresh; use it
ocspBytes = cachedOCSP
ocspResp = resp
}
} else {
// invalid contents; delete the file
err := os.Remove(ocspCachePath)
if err != nil { if err != nil {
return err log.Printf("[WARNING] Unable to delete invalid OCSP staple file: %v", err)
}
}
} }
// If we couldn't get a fresh staple by reading the cache,
// then we need to request it from the OCSP responder
if ocspResp == nil || len(ocspBytes) == 0 {
ocspBytes, ocspResp, ocspErr = acme.GetOCSPForCert(pemBundle)
if ocspErr != nil {
// An error here is not a problem because a certificate may simply
// not contain a link to an OCSP server. But we should log it anyway.
// There's nothing else we can do to get OCSP for this certificate,
// so we can return here with the error.
return fmt.Errorf("no OCSP stapling for %v: %v", cert.Names, ocspErr)
}
gotNewOCSP = true
}
// By now, we should have a response. If good, staple it to
// the certificate. If the OCSP response was not loaded from
// storage, we persist it for next time.
if ocspResp.Status == ocsp.Good {
cert.Certificate.OCSPStaple = ocspBytes cert.Certificate.OCSPStaple = ocspBytes
cert.OCSP = ocspResp cert.OCSP = ocspResp
if gotNewOCSP {
err := os.MkdirAll(filepath.Join(caddy.AssetsPath(), "ocsp"), 0700)
if err != nil {
return fmt.Errorf("unable to make OCSP staple path for %v: %v", cert.Names, err)
}
err = ioutil.WriteFile(ocspCachePath, ocspBytes, 0644)
if err != nil {
return fmt.Errorf("unable to write OCSP staple file for %v: %v", cert.Names, err)
}
}
}
return nil return nil
} }
...@@ -235,6 +296,15 @@ func standaloneTLSTicketKeyRotation(c *tls.Config, ticker *time.Ticker, exitChan ...@@ -235,6 +296,15 @@ func standaloneTLSTicketKeyRotation(c *tls.Config, ticker *time.Ticker, exitChan
} }
} }
// fastHash hashes input using a hashing algorithm that
// is fast, and returns the hash as a hex-encoded string.
// Do not use this for cryptographic purposes.
func fastHash(input []byte) string {
h := fnv.New32a()
h.Write([]byte(input))
return fmt.Sprintf("%x", h.Sum32())
}
const ( const (
// NumTickets is how many tickets to hold and consider // NumTickets is how many tickets to hold and consider
// to decrypt TLS sessions. // to decrypt TLS sessions.
......
...@@ -17,11 +17,11 @@ const ( ...@@ -17,11 +17,11 @@ const (
// RenewInterval is how often to check certificates for renewal. // RenewInterval is how often to check certificates for renewal.
RenewInterval = 12 * time.Hour RenewInterval = 12 * time.Hour
// OCSPInterval is how often to check if OCSP stapling needs updating.
OCSPInterval = 1 * time.Hour
// RenewDurationBefore is how long before expiration to renew certificates. // RenewDurationBefore is how long before expiration to renew certificates.
RenewDurationBefore = (24 * time.Hour) * 30 RenewDurationBefore = (24 * time.Hour) * 30
// OCSPInterval is how often to check if OCSP stapling needs updating.
OCSPInterval = 1 * time.Hour
) )
// maintainAssets is a permanently-blocking function // maintainAssets is a permanently-blocking function
...@@ -154,6 +154,10 @@ func RenewManagedCertificates(allowPrompts bool) (err error) { ...@@ -154,6 +154,10 @@ func RenewManagedCertificates(allowPrompts bool) (err error) {
// UpdateOCSPStaples updates the OCSP stapling in all // UpdateOCSPStaples updates the OCSP stapling in all
// eligible, cached certificates. // eligible, cached certificates.
//
// OCSP maintenance strives to abide the relevant points on
// Ryan Sleevi's recommendations for good OCSP support:
// https://gist.github.com/sleevi/5efe9ef98961ecfb4da8
func UpdateOCSPStaples() { func UpdateOCSPStaples() {
// Create a temporary place to store updates // Create a temporary place to store updates
// until we release the potentially long-lived // until we release the potentially long-lived
...@@ -187,12 +191,9 @@ func UpdateOCSPStaples() { ...@@ -187,12 +191,9 @@ func UpdateOCSPStaples() {
var lastNextUpdate time.Time var lastNextUpdate time.Time
if cert.OCSP != nil { if cert.OCSP != nil {
// start checking OCSP staple about halfway through validity period for good measure
lastNextUpdate = cert.OCSP.NextUpdate lastNextUpdate = cert.OCSP.NextUpdate
refreshTime := cert.OCSP.ThisUpdate.Add(lastNextUpdate.Sub(cert.OCSP.ThisUpdate) / 2) if freshOCSP(cert.OCSP) {
// no need to update staple if ours is still fresh
// since OCSP is already stapled, we need only check if we're in that "refresh window"
if time.Now().Before(refreshTime) {
continue continue
} }
} }
...@@ -201,7 +202,7 @@ func UpdateOCSPStaples() { ...@@ -201,7 +202,7 @@ func UpdateOCSPStaples() {
if err != nil { if err != nil {
if cert.OCSP != nil { if cert.OCSP != nil {
// if there was no staple before, that's fine; otherwise we should log the error // if there was no staple before, that's fine; otherwise we should log the error
log.Printf("[ERROR] Checking OCSP for %v: %v", cert.Names, err) log.Printf("[ERROR] Checking OCSP: %v", err)
} }
continue continue
} }
...@@ -229,3 +230,9 @@ func UpdateOCSPStaples() { ...@@ -229,3 +230,9 @@ func UpdateOCSPStaples() {
} }
certCacheMu.Unlock() certCacheMu.Unlock()
} }
func freshOCSP(resp *ocsp.Response) bool {
// start checking OCSP staple about halfway through validity period for good measure
refreshTime := resp.ThisUpdate.Add(resp.NextUpdate.Sub(resp.ThisUpdate) / 2)
return time.Now().Before(refreshTime)
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment