@@ -38,6 +38,12 @@ type cacheEntry struct {
3838 expires time.Time
3939}
4040
41+ // renewBefore is the gap between the cache deadline (now+ttl, when Get
42+ // re-mints) and the leaf's NotAfter (now+ttl+renewBefore). It gives a
43+ // connection that grabbed the leaf just before re-mint ample remaining
44+ // validity, and a window for Get's NotAfter backstop to act.
45+ const renewBefore = time .Hour
46+
4147func NewMinter (src CASource , o MinterOpts ) * Minter {
4248 if o .CacheMax <= 0 {
4349 o .CacheMax = 1024
@@ -71,7 +77,15 @@ func (m *Minter) GetCertificateForHost(host string) (*tls.Certificate, error) {
7177 defer m .mu .Unlock ()
7278 if el , ok := m .items [host ]; ok {
7379 e := el .Value .(* cacheEntry )
74- if time .Now ().Before (e .expires ) {
80+ // Gate freshness on WALL-clock deadlines, not the process monotonic
81+ // clock. Across a host suspend / VM pause the monotonic clock freezes
82+ // while wall time — and the leaf's x509 validity — keeps advancing, so
83+ // a monotonic deadline would keep serving a leaf the client already
84+ // rejects as expired. e.expires is monotonic-stripped (.Round(0) below);
85+ // the Leaf.NotAfter check is the backstop tied to the cert's real
86+ // validity, the only value the client actually verifies.
87+ now := time .Now ()
88+ if now .Before (e .expires ) && e .cert .Leaf != nil && now .Before (e .cert .Leaf .NotAfter ) {
7589 m .ll .MoveToFront (el )
7690 return e .cert , nil
7791 }
@@ -82,7 +96,10 @@ func (m *Minter) GetCertificateForHost(host string) (*tls.Certificate, error) {
8296 if err != nil {
8397 return nil , err
8498 }
85- el := m .ll .PushFront (& cacheEntry {host : host , cert : cert , expires : time .Now ().Add (m .ttl )})
99+ // .Round(0) strips the monotonic reading so the deadline is a pure wall-clock
100+ // time; comparisons against time.Now() then fall back to the wall clock and
101+ // survive suspend (see the cache-hit gate above).
102+ el := m .ll .PushFront (& cacheEntry {host : host , cert : cert , expires : time .Now ().Add (m .ttl ).Round (0 )})
86103 m .items [host ] = el
87104 for m .ll .Len () > m .max {
88105 back := m .ll .Back ()
@@ -102,8 +119,9 @@ func (m *Minter) mint(host string) (*tls.Certificate, error) {
102119 SerialNumber : serial ,
103120 Subject : pkix.Name {CommonName : host },
104121 NotBefore : time .Now ().Add (- time .Minute ),
105- // Leaf validity must exceed the cache TTL so a cached leaf never serves past expiry.
106- NotAfter : time .Now ().Add (m .ttl + time .Hour ),
122+ // Leaf validity outlasts the cache deadline (now+ttl) by renewBefore so
123+ // a cached leaf is always re-minted before it can serve past expiry.
124+ NotAfter : time .Now ().Add (m .ttl + renewBefore ),
107125 KeyUsage : x509 .KeyUsageDigitalSignature ,
108126 ExtKeyUsage : []x509.ExtKeyUsage {x509 .ExtKeyUsageServerAuth },
109127 }
@@ -116,8 +134,15 @@ func (m *Minter) mint(host string) (*tls.Certificate, error) {
116134 if err != nil {
117135 return nil , fmt .Errorf ("tlsbridge: mint leaf for %s: %w" , host , err )
118136 }
137+ leaf , err := x509 .ParseCertificate (der )
138+ if err != nil {
139+ return nil , fmt .Errorf ("tlsbridge: parse minted leaf for %s: %w" , host , err )
140+ }
119141 return & tls.Certificate {
120142 Certificate : [][]byte {der , caCert .Raw },
121143 PrivateKey : m .leafKey ,
144+ // Populate Leaf so Get can gate on the cert's real wall-clock NotAfter
145+ // (and the TLS stack avoids re-parsing on each handshake).
146+ Leaf : leaf ,
122147 }, nil
123148}
0 commit comments