Newer
Older
EnvoyControlPlane / internal / pkg / cert / letsencrypt / renewer.go
package letsencrypt

import (
	internalcertapi "envoy-control-plane/internal/pkg/cert/api"
	"fmt"

	"github.com/go-acme/lego/v4/certificate"
	"github.com/go-acme/lego/v4/registration"
)

// ------------------------------------------------------------------------------------------------

// RenewCertificate renews an existing certificate using go-acme/lego.
func (l *LetsEncryptIssuer) RenewCertificate(oldCert *internalcertapi.Certificate, webrootPath string, email string) (*internalcertapi.Certificate, error) {
	// FIX 1: Load the ACME Account Private Key from the D-value stored in AccountKey.
	acmePrivateKey, err := loadACMEKeyFromDValue(oldCert.AccountKey)
	if err != nil {
		return nil, fmt.Errorf("failed to load ACME account key: %w", err)
	}

	// 2. Setup ACME User (LEOptions) with the loaded account key
	acmeUser := &LEOptions{
		Email: email,
		key:   acmePrivateKey,
		// FIX 2: Provide the Registration URI (KID) to the client for JWS signing.
		// ASSUMPTION: The stored oldCert.AccountURL contains the ACME account URI (KID).
		Registration: &registration.Resource{
			URI: oldCert.AccountURL,
		},
	}

	// 3. Configure and create the ACME client
	client, err := createClient(acmeUser, webrootPath, l.UseStaging)
	if err != nil {
		return nil, err
	}

	// 4. Reconstruct the certificate.Resource for renewal
	certResource := certificate.Resource{
		Domain:      oldCert.Domain,
		Certificate: oldCert.CertPEM,
		// The PrivateKey here is the DOMAIN's old private key.
		PrivateKey: oldCert.KeyPEM,
	}

	// 5. Renew the certificate
	newCertResources, err := client.Certificate.Renew(certResource, false, false, "")
	if err != nil {
		// Fallback: If renewal fails (e.g., certificate not found on ACME server, 404), try obtaining a fresh certificate.
		// This handles cases where the certificate is expired beyond the renewal window or revoked/missing on the CA side.
		fmt.Printf("Warning: Failed to renew certificate for domain %s: %v. Attempting fresh issuance...\n", oldCert.Domain, err)

		request := certificate.ObtainRequest{
			Domains: []string{oldCert.Domain},
			Bundle:  true,
		}
		newCertResources, err = client.Certificate.Obtain(request)
		if err != nil {
			return nil, fmt.Errorf("failed to renew certificate and failed fallback issuance: %w", err)
		}
	}

	// 6. Map the results
	return &internalcertapi.Certificate{
		Domain:  oldCert.Domain,
		CertPEM: newCertResources.Certificate,
		// The renewed key (newCertResources.PrivateKey) is the domain's NEW private key.
		KeyPEM:    newCertResources.PrivateKey,
		FullChain: append(newCertResources.Certificate, newCertResources.IssuerCertificate...), // Ensure full chain is stored
		// The ACME account key and URL remain the same.
		AccountKey: oldCert.AccountKey,
		AccountURL: oldCert.AccountURL,
		// The rotation strategy remains the same.
		EnableRotation: oldCert.EnableRotation,
		RenewBefore:    oldCert.RenewBefore,
	}, nil
}