autocert update:

- save ACME private key to reuse previous registered ACME account
- properly renew certificate with `Certificate.RenewWithOptions` instead of re-obtaining with `Certificate.Obtain`
This commit is contained in:
yusing 2024-11-29 05:06:23 +08:00
parent d3842ec3c3
commit acdb324f7d
4 changed files with 79 additions and 25 deletions

View file

@ -4,10 +4,13 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/x509"
"os"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
E "github.com/yusing/go-proxy/internal/error" 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"
"github.com/yusing/go-proxy/internal/utils/strutils" "github.com/yusing/go-proxy/internal/utils/strutils"
@ -33,6 +36,9 @@ func NewConfig(cfg *types.AutoCertConfig) *Config {
if cfg.Provider == "" { if cfg.Provider == "" {
cfg.Provider = ProviderLocal cfg.Provider = ProviderLocal
} }
if cfg.ACMEKeyPath == "" {
cfg.ACMEKeyPath = ACMEKeyFileDefault
}
return (*Config)(cfg) return (*Config)(cfg)
} }
@ -62,10 +68,18 @@ func (cfg *Config) GetProvider() (*Provider, E.Error) {
return nil, b.Error() return nil, b.Error()
} }
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 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 { if err != nil {
b.Addf("generate private key: %w", err) return nil, E.New("generate ACME private key").With(err)
return nil, b.Error() }
if err = cfg.saveACMEKey(privKey); err != nil {
return nil, E.New("save ACME private key").With(err)
}
} }
user := &User{ user := &User{
@ -82,3 +96,19 @@ func (cfg *Config) GetProvider() (*Provider, E.Error) {
legoCfg: legoCfg, legoCfg: legoCfg,
}, nil }, 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)
}

View file

@ -11,7 +11,7 @@ const (
certBasePath = "certs/" certBasePath = "certs/"
CertFileDefault = certBasePath + "cert.crt" CertFileDefault = certBasePath + "cert.crt"
KeyFileDefault = certBasePath + "priv.key" KeyFileDefault = certBasePath + "priv.key"
RegistrationFile = certBasePath + "registration.json" ACMEKeyFileDefault = certBasePath + "acme.key"
) )
const ( const (

View file

@ -28,6 +28,7 @@ type (
legoCfg *lego.Config legoCfg *lego.Config
client *lego.Client client *lego.Client
legoCert *certificate.Resource
tlsCert *tls.Certificate tlsCert *tls.Certificate
certExpiries CertExpiries certExpiries CertExpiries
} }
@ -78,15 +79,30 @@ func (p *Provider) ObtainCert() E.Error {
} }
} }
client := p.client var cert *certificate.Resource
req := certificate.ObtainRequest{ 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
}
}
if cert == nil {
cert, err = p.client.Certificate.Obtain(certificate.ObtainRequest{
Domains: p.cfg.Domains, Domains: p.cfg.Domains,
Bundle: true, Bundle: true,
} })
cert, err := client.Certificate.Obtain(req)
if err != nil { if err != nil {
return E.From(err) return E.From(err)
} }
}
if err = p.saveCert(cert); err != nil { if err = p.saveCert(cert); err != nil {
return E.From(err) return E.From(err)
@ -179,11 +195,18 @@ func (p *Provider) registerACME() error {
if p.user.Registration != nil { if p.user.Registration != nil {
return nil return nil
} }
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}) reg, err := p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil { if err != nil {
return err return err
} }
p.user.Registration = reg p.user.Registration = reg
logger.Info().Interface("reg", reg).Msg("acme registered")
}
return nil return nil
} }

View file

@ -6,6 +6,7 @@ type (
Domains []string `json:"domains,omitempty" yaml:",flow"` Domains []string `json:"domains,omitempty" yaml:",flow"`
CertPath string `json:"cert_path,omitempty" yaml:"cert_path"` CertPath string `json:"cert_path,omitempty" yaml:"cert_path"`
KeyPath string `json:"key_path,omitempty" yaml:"key_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"` Provider string `json:"provider,omitempty" yaml:"provider"`
Options AutocertProviderOpt `json:"options,omitempty" yaml:",flow"` Options AutocertProviderOpt `json:"options,omitempty" yaml:",flow"`
} }