detached b1d7815

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 {
231231 return nil
232232 }
233233
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+
234259 func (ca *CA) loadOrGenerateIntermediate() error {
235260 certPath := filepath.Join(ca.dataDir, pkiDir, "intermediate.crt")
236261 keyPath := filepath.Join(ca.dataDir, pkiDir, "intermediate.key")
@@ -243,14 +268,7 @@ func (ca *CA) loadOrGenerateIntermediate() error {
243268
244269 switch {
245270 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 {
254272 ca.interCert = cert
255273 ca.interKey = key
256274 ca.log.Info().
@@ -258,8 +276,6 @@ func (ca *CA) loadOrGenerateIntermediate() error {
258276 Time("expires", cert.NotAfter).
259277 Msg("loaded existing intermediate CA")
260278 return nil
261- } else {
262- ca.log.Warn().Msg("intermediate CA expired, regenerating")
263279 }
264280
265281 case certMissing && keyMissing:
@@ -231,6 +231,31 @@ func (ca *CA) bootstrapRoot(certPath, keyPath string) error {
231 return nil231 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 = cert272 ca.interCert = cert
255 ca.interKey = key273 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 nil278 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
33 import (
44 "context"
55 "crypto/tls"
6+ "crypto/x509"
67 "sync"
78 "time"
89
@@ -115,8 +116,10 @@ func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
115116 }
116117
117118 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+ }
120123 }
121124 s.cache.Store(domain, &cachedCert{
122125 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.NotAfter120+ 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,

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close