package letsencrypt
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"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"
api "envoy-control-plane/internal/pkg/cert/api"
cert "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 {
}
// GetName returns the name of the issuer.
func (l *LetsEncryptIssuer) GetName() string {
return "LetsEncrypt (Production)"
}
// IssueCertificate implements the core certificate issuance logic using go-acme/lego.
func (l *LetsEncryptIssuer) IssueCertificate(domain, webrootPath, email string) (*cert.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,
}
// 2. Configure the ACME client
config := lego.NewConfig(acmeUser)
config.CADirURL = lego.LEDirectoryProduction
// Set the key type (this should reflect the key used in the private key)
config.Certificate.KeyType = certcrypto.EC256
client, err := lego.NewClient(config)
if err != nil {
return nil, fmt.Errorf("failed to create ACME client: %w", err)
}
// 3. Set up the HTTP-01 challenge provider (Webroot)
// Use the correct method to set up the Webroot provider for HTTP-01 challenge
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)
}
// 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
// Make sure to handle the private key properly here.
return &api.Certificate{
Domain: domain,
CertPEM: certResources.Certificate,
KeyPEM: certResources.PrivateKey,
FullChain: certResources.Certificate, // lego's Certificate field includes the chain
AccountKey: privateKey.D.Bytes(), // This is safer, as it assumes the type is ecdsa.PrivateKey
}, nil
}