fix(pki): derive cache expiry from real leaf NotAfter; extract validateLoadedIntermediate
- service.go: parse cert.Certificate[0] instead of relying on cert.Leaf (tls.X509KeyPair does not populate .Leaf), so cache eviction matches the real NotAfter — prevents serving expired certs during intermediate renewal - ca.go: extract validateLoadedIntermediate helper from loadOrGenerateIntermediate to bring cyclomatic complexity below the gocyclo max-15 CI gate
- commit
- b1d7815e7285271920e54cae82112bf9c7d6f930
- author
- brice · · 2026-04-01 04:24 UTC
- parent
- 29dcc3b
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +31 -12
M internal/adapters/out/pki/ca.go +26 -10
| @@ -231,6 +231,31 @@ func (ca *CA) bootstrapRoot(certPath, keyPath string) error { | |||
| 231 | 231 | return nil | |
| 232 | 232 | } | |
| 233 | 233 | ||
| 234 | + | // validateLoadedIntermediate parses, verifies, and checks the expiry of a loaded | |
| 235 | + | // intermediate CA cert/key pair. Returns (cert, key, true) on success, or | |
| 236 | + | // (nil, nil, false) if the pair should be regenerated (parse error, key mismatch, | |
| 237 | + | // wrong signer, or expired). Logging is done inside this function. | |
| 238 | + | func (ca *CA) validateLoadedIntermediate(certPEM, keyPEM []byte) (*x509.Certificate, crypto.Signer, bool) { | |
| 239 | + | cert, key, err := parseCertAndKey(certPEM, keyPEM) | |
| 240 | + | if err != nil { | |
| 241 | + | ca.log.Warn().Err(err).Msg("failed to parse intermediate CA, regenerating") | |
| 242 | + | return nil, nil, false | |
| 243 | + | } | |
| 244 | + | if err := verifyKeyPair(cert, key); err != nil { | |
| 245 | + | ca.log.Warn().Err(err).Msg("intermediate CA cert/key mismatch, regenerating") | |
| 246 | + | return nil, nil, false | |
| 247 | + | } | |
| 248 | + | if err := cert.CheckSignatureFrom(ca.rootCert); err != nil { | |
| 249 | + | ca.log.Warn().Err(err).Msg("intermediate CA not signed by current root, regenerating") | |
| 250 | + | return nil, nil, false | |
| 251 | + | } | |
| 252 | + | if !time.Now().Before(cert.NotAfter) { | |
| 253 | + | ca.log.Warn().Msg("intermediate CA expired, regenerating") | |
| 254 | + | return nil, nil, false | |
| 255 | + | } | |
| 256 | + | return cert, key, true | |
| 257 | + | } | |
| 258 | + | ||
| 234 | 259 | func (ca *CA) loadOrGenerateIntermediate() error { | |
| 235 | 260 | certPath := filepath.Join(ca.dataDir, pkiDir, "intermediate.crt") | |
| 236 | 261 | keyPath := filepath.Join(ca.dataDir, pkiDir, "intermediate.key") | |
| @@ -243,14 +268,7 @@ func (ca *CA) loadOrGenerateIntermediate() error { | |||
| 243 | 268 | ||
| 244 | 269 | switch { | |
| 245 | 270 | case certErr == nil && keyErr == nil: | |
| 246 | - | cert, key, err := parseCertAndKey(certPEM, keyPEM) | |
| 247 | - | if err != nil { | |
| 248 | - | ca.log.Warn().Err(err).Msg("failed to parse intermediate CA, regenerating") | |
| 249 | - | } else if err := verifyKeyPair(cert, key); err != nil { | |
| 250 | - | ca.log.Warn().Err(err).Msg("intermediate CA cert/key mismatch, regenerating") | |
| 251 | - | } else if err := cert.CheckSignatureFrom(ca.rootCert); err != nil { | |
| 252 | - | ca.log.Warn().Err(err).Msg("intermediate CA not signed by current root, regenerating") | |
| 253 | - | } else if time.Now().Before(cert.NotAfter) { | |
| 271 | + | if cert, key, ok := ca.validateLoadedIntermediate(certPEM, keyPEM); ok { | |
| 254 | 272 | ca.interCert = cert | |
| 255 | 273 | ca.interKey = key | |
| 256 | 274 | ca.log.Info(). | |
| @@ -258,8 +276,6 @@ func (ca *CA) loadOrGenerateIntermediate() error { | |||
| 258 | 276 | Time("expires", cert.NotAfter). | |
| 259 | 277 | Msg("loaded existing intermediate CA") | |
| 260 | 278 | return nil | |
| 261 | - | } else { | |
| 262 | - | ca.log.Warn().Msg("intermediate CA expired, regenerating") | |
| 263 | 279 | } | |
| 264 | 280 | ||
| 265 | 281 | case certMissing && keyMissing: | |
| @@ -231,6 +231,31 @@ func (ca *CA) bootstrapRoot(certPath, keyPath string) error { | |||||
| 231 | return nil | 231 | return nil | ||
| 232 | } | 232 | } | ||
| 233 | 233 | ||||
| 234 | + | // validateLoadedIntermediate parses, verifies, and checks the expiry of a loaded | |||
| 235 | + | // intermediate CA cert/key pair. Returns (cert, key, true) on success, or | |||
| 236 | + | // (nil, nil, false) if the pair should be regenerated (parse error, key mismatch, | |||
| 237 | + | // wrong signer, or expired). Logging is done inside this function. | |||
| 238 | + | func (ca *CA) validateLoadedIntermediate(certPEM, keyPEM []byte) (*x509.Certificate, crypto.Signer, bool) { | |||
| 239 | + | cert, key, err := parseCertAndKey(certPEM, keyPEM) | |||
| 240 | + | if err != nil { | |||
| 241 | + | ca.log.Warn().Err(err).Msg("failed to parse intermediate CA, regenerating") | |||
| 242 | + | return nil, nil, false | |||
| 243 | + | } | |||
| 244 | + | if err := verifyKeyPair(cert, key); err != nil { | |||
| 245 | + | ca.log.Warn().Err(err).Msg("intermediate CA cert/key mismatch, regenerating") | |||
| 246 | + | return nil, nil, false | |||
| 247 | + | } | |||
| 248 | + | if err := cert.CheckSignatureFrom(ca.rootCert); err != nil { | |||
| 249 | + | ca.log.Warn().Err(err).Msg("intermediate CA not signed by current root, regenerating") | |||
| 250 | + | return nil, nil, false | |||
| 251 | + | } | |||
| 252 | + | if !time.Now().Before(cert.NotAfter) { | |||
| 253 | + | ca.log.Warn().Msg("intermediate CA expired, regenerating") | |||
| 254 | + | return nil, nil, false | |||
| 255 | + | } | |||
| 256 | + | return cert, key, true | |||
| 257 | + | } | |||
| 258 | + | ||||
| 234 | func (ca *CA) loadOrGenerateIntermediate() error { | 259 | func (ca *CA) loadOrGenerateIntermediate() error { | ||
| 235 | certPath := filepath.Join(ca.dataDir, pkiDir, "intermediate.crt") | 260 | certPath := filepath.Join(ca.dataDir, pkiDir, "intermediate.crt") | ||
| 236 | keyPath := filepath.Join(ca.dataDir, pkiDir, "intermediate.key") | 261 | keyPath := filepath.Join(ca.dataDir, pkiDir, "intermediate.key") | ||
| @@ -243,14 +268,7 @@ func (ca *CA) loadOrGenerateIntermediate() error { | |||||
| 243 | 268 | ||||
| 244 | switch { | 269 | switch { | ||
| 245 | case certErr == nil && keyErr == nil: | 270 | case certErr == nil && keyErr == nil: | ||
| 246 | - | cert, key, err := parseCertAndKey(certPEM, keyPEM) | 271 | + | if cert, key, ok := ca.validateLoadedIntermediate(certPEM, keyPEM); ok { |
| 247 | - | if err != nil { | |||
| 248 | - | ca.log.Warn().Err(err).Msg("failed to parse intermediate CA, regenerating") | |||
| 249 | - | } else if err := verifyKeyPair(cert, key); err != nil { | |||
| 250 | - | ca.log.Warn().Err(err).Msg("intermediate CA cert/key mismatch, regenerating") | |||
| 251 | - | } else if err := cert.CheckSignatureFrom(ca.rootCert); err != nil { | |||
| 252 | - | ca.log.Warn().Err(err).Msg("intermediate CA not signed by current root, regenerating") | |||
| 253 | - | } else if time.Now().Before(cert.NotAfter) { | |||
| 254 | ca.interCert = cert | 272 | ca.interCert = cert | ||
| 255 | ca.interKey = key | 273 | ca.interKey = key | ||
| 256 | ca.log.Info(). | 274 | ca.log.Info(). | ||
| @@ -258,8 +276,6 @@ func (ca *CA) loadOrGenerateIntermediate() error { | |||||
| 258 | Time("expires", cert.NotAfter). | 276 | Time("expires", cert.NotAfter). | ||
| 259 | Msg("loaded existing intermediate CA") | 277 | Msg("loaded existing intermediate CA") | ||
| 260 | return nil | 278 | return nil | ||
| 261 | - | } else { | |||
| 262 | - | ca.log.Warn().Msg("intermediate CA expired, regenerating") | |||
| 263 | } | 279 | } | ||
| 264 | 280 | ||||
| 265 | case certMissing && keyMissing: | 281 | case certMissing && keyMissing: | ||
M internal/usecase/pki/service.go +5 -2
| @@ -3,6 +3,7 @@ package pki | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "crypto/tls" | |
| 6 | + | "crypto/x509" | |
| 6 | 7 | "sync" | |
| 7 | 8 | "time" | |
| 8 | 9 | ||
| @@ -115,8 +116,10 @@ func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, | |||
| 115 | 116 | } | |
| 116 | 117 | ||
| 117 | 118 | expiresAt := time.Now().Add(s.leafLifetime()) | |
| 118 | - | if cert.Leaf != nil { | |
| 119 | - | expiresAt = cert.Leaf.NotAfter | |
| 119 | + | if len(cert.Certificate) > 0 { | |
| 120 | + | if leaf, err := x509.ParseCertificate(cert.Certificate[0]); err == nil { | |
| 121 | + | expiresAt = leaf.NotAfter | |
| 122 | + | } | |
| 120 | 123 | } | |
| 121 | 124 | s.cache.Store(domain, &cachedCert{ | |
| 122 | 125 | cert: cert, | |
| @@ -3,6 +3,7 @@ package pki | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "crypto/tls" | 5 | "crypto/tls" | ||
| 6 | + | "crypto/x509" | |||
| 6 | "sync" | 7 | "sync" | ||
| 7 | "time" | 8 | "time" | ||
| 8 | 9 | ||||
| @@ -115,8 +116,10 @@ func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, | |||||
| 115 | } | 116 | } | ||
| 116 | 117 | ||||
| 117 | expiresAt := time.Now().Add(s.leafLifetime()) | 118 | expiresAt := time.Now().Add(s.leafLifetime()) | ||
| 118 | - | if cert.Leaf != nil { | 119 | + | if len(cert.Certificate) > 0 { |
| 119 | - | expiresAt = cert.Leaf.NotAfter | 120 | + | if leaf, err := x509.ParseCertificate(cert.Certificate[0]); err == nil { |
| 121 | + | expiresAt = leaf.NotAfter | |||
| 122 | + | } | |||
| 120 | } | 123 | } | ||
| 121 | s.cache.Store(domain, &cachedCert{ | 124 | s.cache.Store(domain, &cachedCert{ | ||
| 122 | cert: cert, | 125 | cert: cert, | ||