Fix renew require new challenge
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
116
pkg/cert/acme.go
116
pkg/cert/acme.go
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user