package letsencrypt
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"math/big"
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/providers/http/webroot"
"github.com/go-acme/lego/v4/registration"
internalcertapi "envoy-control-plane/internal/pkg/cert/api"
)
// LEOptions is a simple struct to satisfy the necessary interface for the lego ACME client.
type LEOptions struct {
Email string
Registration *registration.Resource
key crypto.PrivateKey
}
func (u *LEOptions) GetEmail() string {
return u.Email
}
func (u *LEOptions) GetRegistration() *registration.Resource {
return u.Registration
}
func (u *LEOptions) GetPrivateKey() crypto.PrivateKey {
return u.key
}
// LetsEncryptIssuer implements the CertIssuer interface for Let's Encrypt.
type LetsEncryptIssuer struct {
UseStaging bool
}
// GetName returns the name of the issuer.
func (l *LetsEncryptIssuer) GetName() string {
if l.UseStaging {
return "LetsEncrypt (Staging)"
}
return "LetsEncrypt (Production)"
}
// ------------------------------------------------------------------------------------------------
// IssueCertificate implements the core certificate issuance logic using go-acme/lego.
func (l *LetsEncryptIssuer) IssueCertificate(domain, webrootPath, email string) (*internalcertapi.Certificate, error) {
// 1. Setup ACME Account Key and User
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate private key: %w", err)
}
acmeUser := &LEOptions{
Email: email,
key: privateKey,
}
client, err := createClient(acmeUser, webrootPath, l.UseStaging)
if err != nil {
return nil, fmt.Errorf("failed to create ACME client: %w", err)
}
// 4. Register the ACME account
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return nil, fmt.Errorf("failed to register ACME account: %w", err)
}
acmeUser.Registration = reg
// 5. Issue the certificate
request := certificate.ObtainRequest{
Domains: []string{domain},
Bundle: true, // Include full chain
}
certResources, err := client.Certificate.Obtain(request)
if err != nil {
return nil, fmt.Errorf("failed to obtain certificate: %w", err)
}
// 6. Map the results to the generic Certificate struct
return &internalcertapi.Certificate{
Domain: domain,
CertPEM: certResources.Certificate,
KeyPEM: certResources.PrivateKey,
FullChain: append(certResources.Certificate, certResources.IssuerCertificate...), // Modified: Explicitly append issuer certificate
AccountKey: privateKey.D.Bytes(),
// FIX: Persist the ACME Account URL (KID) for future renewal JWS signing.
// ASSUMPTION: internalcertapi.Certificate has an 'AccountURL' field.
AccountURL: reg.URI,
}, nil
}
// ------------------------------------------------------------------------------------------------
// createClient is a helper function to avoid code duplication in Issue and Renew.
func createClient(acmeUser *LEOptions, webrootPath string, useStaging bool) (*lego.Client, error) {
config := lego.NewConfig(acmeUser)
if useStaging {
config.CADirURL = lego.LEDirectoryStaging
} else {
config.CADirURL = lego.LEDirectoryProduction
}
config.Certificate.KeyType = certcrypto.EC256
client, err := lego.NewClient(config)
if err != nil {
return nil, fmt.Errorf("failed to create ACME client: %w", err)
}
httpProvider, err := webroot.NewHTTPProvider(webrootPath)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP-01 provider: %w", err)
}
if err := client.Challenge.SetHTTP01Provider(httpProvider); err != nil {
return nil, fmt.Errorf("failed to set HTTP-01 provider: %w", err)
}
return client, nil
}
// ------------------------------------------------------------------------------------------------
// loadACMEKeyFromDValue reconstructs the ecdsa.PrivateKey from its raw D-value bytes.
func loadACMEKeyFromDValue(dValue []byte) (*ecdsa.PrivateKey, error) {
if len(dValue) != 32 {
return nil, fmt.Errorf("invalid D-value length: expected 32 bytes for P256, got %d", len(dValue))
}
key := new(ecdsa.PrivateKey)
key.PublicKey.Curve = elliptic.P256()
key.D = new(big.Int).SetBytes(dValue)
// Calculate public key components X and Y from D
key.PublicKey.X, key.PublicKey.Y = key.PublicKey.Curve.ScalarBaseMult(key.D.Bytes())
return key, nil
}