Fix renew require new challenge

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-09-30 15:24:25 +02:00
parent 5ae8699535
commit acb6348d40
2 changed files with 108 additions and 17 deletions

View File

@@ -15,7 +15,6 @@
package cert package cert
import ( import (
"bytes"
"context" "context"
"crypto" "crypto"
"crypto/rand" "crypto/rand"
@@ -26,6 +25,9 @@ import (
"github.com/getprobo/probo/pkg/crypto/keys" "github.com/getprobo/probo/pkg/crypto/keys"
cryptopem "github.com/getprobo/probo/pkg/crypto/pem" cryptopem "github.com/getprobo/probo/pkg/crypto/pem"
"github.com/getprobo/probo/pkg/version"
"go.gearno.de/kit/httpclient"
"go.gearno.de/kit/log"
"golang.org/x/crypto/acme" "golang.org/x/crypto/acme"
) )
@@ -41,6 +43,7 @@ type (
client *acme.Client client *acme.Client
email string email string
keyType keys.Type keyType keys.Type
logger *log.Logger
} }
DNSChallenge struct { DNSChallenge struct {
@@ -51,20 +54,41 @@ type (
URL string URL string
OrderURL string OrderURL string
} }
ErrDNSChallengeRequired struct {
Domain string
Challenge *DNSChallenge
}
) )
func NewACMEService(email string, keyType keys.Type, directoryURL string) (*ACMEService, error) { func (e *ErrDNSChallengeRequired) Error() string {
return fmt.Sprintf("DNS challenge required for domain %s", e.Domain)
}
func NewACMEService(email string, keyType keys.Type, directoryURL string, logger *log.Logger) (*ACMEService, error) {
accountKey, err := keys.Generate(keyType) accountKey, err := keys.Generate(keyType)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot generate account key: %w", err) return nil, fmt.Errorf("cannot generate account key: %w", err)
} }
client := &acme.Client{Key: accountKey, DirectoryURL: directoryURL} httpClient := httpclient.DefaultPooledClient(
httpclient.WithLogger(logger),
// httpclient.WithTracerProvider(tp),
// httpclient.WithRegisterer(r),
)
client := &acme.Client{
Key: accountKey,
DirectoryURL: directoryURL,
UserAgent: version.UserAgent("acme"),
HTTPClient: httpClient,
}
service := &ACMEService{ service := &ACMEService{
client: client, client: client,
email: email, email: email,
keyType: keyType, keyType: keyType,
logger: logger.Named("acme"),
} }
ctx := context.Background() ctx := context.Background()
@@ -180,41 +204,99 @@ func (s *ACMEService) CompleteDNSChallenge(
return nil, fmt.Errorf("cannot encode key: %w", err) return nil, fmt.Errorf("cannot encode key: %w", err)
} }
var buf bytes.Buffer var chainDER [][]byte
for i := 1; i < len(der); i++ { if len(der) > 1 {
buf.Write(cryptopem.EncodeCertificate(der[i])) chainDER = der[1:]
} }
chainPEM := cryptopem.EncodeCertificateChain(chainDER)
return &Certificate{ return &Certificate{
CertPEM: certPEM, CertPEM: certPEM,
KeyPEM: keyPEM, KeyPEM: keyPEM,
ChainPEM: buf.Bytes(), ChainPEM: chainPEM,
ExpiresAt: cert.NotAfter, ExpiresAt: cert.NotAfter,
}, nil }, nil
} }
func (s *ACMEService) InitiateRenewal( func (s *ACMEService) RenewCertificate(
ctx context.Context, ctx context.Context,
domain string, domain string,
) (*DNSChallenge, error) { ) (*Certificate, error) {
cert, err := s.renewWithExistingAuth(ctx, domain)
if err == nil {
return cert, nil
}
// If renewal with existing auth fails, it might mean:
// 1. The authorization has expired (usually after 30-90 days of no renewal)
// 2. This is a first-time certificate request
// In these cases, we need a new challenge
s.logger.WarnCtx(ctx, "renewal with existing authorization failed, initiating new challenge",
log.String("domain", domain),
log.Error(err))
challenge, err := s.GetDNSChallenge(ctx, domain) challenge, err := s.GetDNSChallenge(ctx, domain)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot get DNS challenge for renewal: %w", err) return nil, fmt.Errorf("cannot get DNS challenge for renewal: %w", err)
} }
return challenge, nil return nil, &ErrDNSChallengeRequired{
Domain: domain,
Challenge: challenge,
}
} }
func (s *ACMEService) CompleteRenewal( func (s *ACMEService) renewWithExistingAuth(ctx context.Context, domain string) (*Certificate, error) {
ctx context.Context, order, err := s.client.AuthorizeOrder(ctx, acme.DomainIDs(domain))
challenge *DNSChallenge,
) (*Certificate, error) {
cert, err := s.CompleteDNSChallenge(ctx, challenge)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot complete renewal challenge: %w", err) return nil, fmt.Errorf("cannot create renewal order: %w", err)
} }
return cert, nil if order.Status != acme.StatusReady {
order, err = s.client.WaitOrder(ctx, order.URI)
if err != nil {
return nil, fmt.Errorf("authorization not valid or expired: %w", err)
}
}
certKey, err := keys.Generate(s.keyType)
if err != nil {
return nil, fmt.Errorf("cannot generate certificate key: %w", err)
}
csr, err := createCSR(domain, certKey)
if err != nil {
return nil, fmt.Errorf("cannot create CSR: %w", err)
}
der, _, err := s.client.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
if err != nil {
return nil, fmt.Errorf("cannot create certificate: %w", err)
}
cert, err := x509.ParseCertificate(der[0])
if err != nil {
return nil, fmt.Errorf("cannot parse certificate: %w", err)
}
certPEM := cryptopem.EncodeCertificate(der[0])
keyPEM, err := cryptopem.EncodePrivateKey(certKey)
if err != nil {
return nil, fmt.Errorf("cannot encode key: %w", err)
}
var chainDER [][]byte
if len(der) > 1 {
chainDER = der[1:]
}
chainPEM := cryptopem.EncodeCertificateChain(chainDER)
return &Certificate{
CertPEM: certPEM,
KeyPEM: keyPEM,
ChainPEM: chainPEM,
ExpiresAt: cert.NotAfter,
}, nil
} }
func (s *ACMEService) CheckRenewalNeeded(expiresAt time.Time, threshold time.Duration) bool { func (s *ACMEService) CheckRenewalNeeded(expiresAt time.Time, threshold time.Duration) bool {

View File

@@ -33,6 +33,15 @@ func EncodeCertificate(der []byte) []byte {
return pem.EncodeToMemory(block) return pem.EncodeToMemory(block)
} }
// EncodeCertificateChain encodes multiple DER-encoded certificates into a single PEM chain
func EncodeCertificateChain(derCerts [][]byte) []byte {
var chain []byte
for _, der := range derCerts {
chain = append(chain, EncodeCertificate(der)...)
}
return chain
}
func EncodePrivateKey(key crypto.Signer) ([]byte, error) { func EncodePrivateKey(key crypto.Signer) ([]byte, error) {
var keyDER []byte var keyDER []byte
var keyType string var keyType string