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: certResources.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 }