package certs import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/pem" "fmt" "os" "path/filepath" "debtik/internal/logging" "strings" "time" ) func (cm *CertManager) renewCandidates(now time.Time) []LetsEncryptRequest { cm.mu.RLock() defer cm.mu.RUnlock() var requests []LetsEncryptRequest for _, cert := range cm.certs { if cert.Type == TypeACME && cert.ValidTo.Before(now.Add(30*24*time.Hour)) { req := LetsEncryptRequest{Name: cert.Name, Domain: cert.CommonName, Comment: cert.Comment, Staging: strings.Contains(cert.Issuer, "Staging")} if cert.ACMERequest != nil { req = *cert.ACMERequest } if req.Domain == "" { continue } req.renew = true requests = append(requests, req) } } return requests } func (cm *CertManager) RenewDue(ctx context.Context) { cm.renewMu.Lock() defer cm.renewMu.Unlock() for _, request := range cm.renewCandidates(time.Now()) { if ctx.Err() != nil { return } attemptCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) _, err := cm.IssueLetsEncrypt(attemptCtx, request) cancel() cm.mu.Lock() if cert := cm.certs[strings.ToLower(request.Name)]; cert != nil { cert.LastRenewAttempt = time.Now() cert.RenewalError = "" if err != nil { cert.RenewalError = err.Error() } if saveErr := cm.saveLocked(); saveErr != nil { logging.Log("certs,error", "Failed to persist renewal result: %v", saveErr) } } cm.mu.Unlock() if err != nil { logging.Log("certs,error", "Certificate %s renewal failed: %v", request.Name, err) } else { logging.Log("certs,info", "Certificate %s renewed", request.Name) } } } func (cm *CertManager) RunRenewal(ctx context.Context) { // Allow the HTTP challenge listener to become available first. timer := time.NewTimer(5 * time.Minute) defer timer.Stop() for { select { case <-ctx.Done(): return case <-timer.C: cm.RenewDue(ctx) timer.Reset(12 * time.Hour) } } } func (cm *CertManager) acmeAccountKey(staging bool) (*ecdsa.PrivateKey, error) { cm.accountMu.Lock() defer cm.accountMu.Unlock() name := "acme-account-production.key" if staging { name = "acme-account-staging.key" } path := filepath.Join(cm.dir, name) data, err := os.ReadFile(path) if err == nil { block, _ := pem.Decode(data) if block == nil { return nil, fmt.Errorf("invalid ACME account key") } return x509.ParseECPrivateKey(block.Bytes) } if !os.IsNotExist(err) { return nil, err } key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, err } if err = savePrivateKeyPEM(path, key); err != nil { return nil, err } return key, nil }