diff --git a/internal/autocert/config.go b/internal/autocert/config.go index 670db93..a6efe9a 100644 --- a/internal/autocert/config.go +++ b/internal/autocert/config.go @@ -4,10 +4,13 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/x509" + "os" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/lego" E "github.com/yusing/go-proxy/internal/error" + "github.com/yusing/go-proxy/internal/logging" "github.com/yusing/go-proxy/internal/utils" "github.com/yusing/go-proxy/internal/utils/strutils" @@ -33,6 +36,9 @@ func NewConfig(cfg *types.AutoCertConfig) *Config { if cfg.Provider == "" { cfg.Provider = ProviderLocal } + if cfg.ACMEKeyPath == "" { + cfg.ACMEKeyPath = ACMEKeyFileDefault + } return (*Config)(cfg) } @@ -62,10 +68,18 @@ func (cfg *Config) GetProvider() (*Provider, E.Error) { return nil, b.Error() } - privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - b.Addf("generate private key: %w", err) - return nil, b.Error() + var privKey *ecdsa.PrivateKey + var err error + + if privKey, err = cfg.loadACMEKey(); err != nil { + logging.Err(err).Msg("load ACME private key failed, generating one...") + privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, E.New("generate ACME private key").With(err) + } + if err = cfg.saveACMEKey(privKey); err != nil { + return nil, E.New("save ACME private key").With(err) + } } user := &User{ @@ -82,3 +96,19 @@ func (cfg *Config) GetProvider() (*Provider, E.Error) { legoCfg: legoCfg, }, nil } + +func (cfg *Config) loadACMEKey() (*ecdsa.PrivateKey, error) { + data, err := os.ReadFile(cfg.ACMEKeyPath) + if err != nil { + return nil, err + } + return x509.ParseECPrivateKey(data) +} + +func (cfg *Config) saveACMEKey(key *ecdsa.PrivateKey) error { + data, err := x509.MarshalECPrivateKey(key) + if err != nil { + return err + } + return os.WriteFile(cfg.ACMEKeyPath, data, 0o600) +} diff --git a/internal/autocert/constants.go b/internal/autocert/constants.go index 308d3dd..f50a109 100644 --- a/internal/autocert/constants.go +++ b/internal/autocert/constants.go @@ -8,10 +8,10 @@ import ( ) const ( - certBasePath = "certs/" - CertFileDefault = certBasePath + "cert.crt" - KeyFileDefault = certBasePath + "priv.key" - RegistrationFile = certBasePath + "registration.json" + certBasePath = "certs/" + CertFileDefault = certBasePath + "cert.crt" + KeyFileDefault = certBasePath + "priv.key" + ACMEKeyFileDefault = certBasePath + "acme.key" ) const ( diff --git a/internal/autocert/provider.go b/internal/autocert/provider.go index b887173..499161e 100644 --- a/internal/autocert/provider.go +++ b/internal/autocert/provider.go @@ -28,6 +28,7 @@ type ( legoCfg *lego.Config client *lego.Client + legoCert *certificate.Resource tlsCert *tls.Certificate certExpiries CertExpiries } @@ -78,14 +79,29 @@ func (p *Provider) ObtainCert() E.Error { } } - client := p.client - req := certificate.ObtainRequest{ - Domains: p.cfg.Domains, - Bundle: true, + var cert *certificate.Resource + var err error + + if p.legoCert != nil { + cert, err = p.client.Certificate.RenewWithOptions(*p.legoCert, &certificate.RenewOptions{ + Bundle: true, + }) + if err != nil { + p.legoCert = nil + logger.Err(err).Msg("cert renew failed, fallback to obtain") + } else { + p.legoCert = cert + } } - cert, err := client.Certificate.Obtain(req) - if err != nil { - return E.From(err) + + if cert == nil { + cert, err = p.client.Certificate.Obtain(certificate.ObtainRequest{ + Domains: p.cfg.Domains, + Bundle: true, + }) + if err != nil { + return E.From(err) + } } if err = p.saveCert(cert); err != nil { @@ -179,11 +195,18 @@ func (p *Provider) registerACME() error { if p.user.Registration != nil { return nil } - reg, err := p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - if err != nil { - return err + if reg, err := p.client.Registration.ResolveAccountByKey(); err == nil { + p.user.Registration = reg + logger.Info().Msg("reused acme registration from private key") + return nil + } else { + reg, err := p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if err != nil { + return err + } + p.user.Registration = reg + logger.Info().Interface("reg", reg).Msg("acme registered") } - p.user.Registration = reg return nil } diff --git a/internal/config/types/autocert_config.go b/internal/config/types/autocert_config.go index d5687d7..b656403 100644 --- a/internal/config/types/autocert_config.go +++ b/internal/config/types/autocert_config.go @@ -2,12 +2,13 @@ package types type ( AutoCertConfig struct { - Email string `json:"email,omitempty" yaml:"email"` - Domains []string `json:"domains,omitempty" yaml:",flow"` - CertPath string `json:"cert_path,omitempty" yaml:"cert_path"` - KeyPath string `json:"key_path,omitempty" yaml:"key_path"` - Provider string `json:"provider,omitempty" yaml:"provider"` - Options AutocertProviderOpt `json:"options,omitempty" yaml:",flow"` + Email string `json:"email,omitempty" yaml:"email"` + Domains []string `json:"domains,omitempty" yaml:",flow"` + CertPath string `json:"cert_path,omitempty" yaml:"cert_path"` + KeyPath string `json:"key_path,omitempty" yaml:"key_path"` + ACMEKeyPath string `json:"acme_key_path,omitempty" yaml:"acme_key_path"` + Provider string `json:"provider,omitempty" yaml:"provider"` + Options AutocertProviderOpt `json:"options,omitempty" yaml:",flow"` } AutocertProviderOpt map[string]any )