diff --git a/internal/autocert/config.go b/internal/autocert/config.go index 987a105e..5126bf15 100644 --- a/internal/autocert/config.go +++ b/internal/autocert/config.go @@ -25,10 +25,16 @@ type Config struct { KeyPath string `json:"key_path,omitempty"` ACMEKeyPath string `json:"acme_key_path,omitempty"` Provider string `json:"provider,omitempty"` - CADirURL string `json:"ca_dir_url,omitempty"` - CACerts []string `json:"ca_certs,omitempty"` Options map[string]any `json:"options,omitempty"` + // Custom ACME CA + CADirURL string `json:"ca_dir_url,omitempty"` + CACerts []string `json:"ca_certs,omitempty"` + + // EAB + EABKid string `json:"eab_kid,omitempty" validate:"required_with=EABHmac"` + EABHmac string `json:"eab_hmac,omitempty" validate:"required_with=EABKid"` // base64 encoded + HTTPClient *http.Client `json:"-"` // for tests only challengeProvider challenge.Provider diff --git a/internal/autocert/config_test.go b/internal/autocert/config_test.go new file mode 100644 index 00000000..61ab836e --- /dev/null +++ b/internal/autocert/config_test.go @@ -0,0 +1,31 @@ +package autocert + +import ( + "fmt" + "testing" + + "github.com/yusing/go-proxy/internal/serialization" +) + +func TestEABConfigRequired(t *testing.T) { + tests := []struct { + name string + cfg *Config + wantErr bool + }{ + {name: "Missing EABKid", cfg: &Config{EABHmac: "1234567890"}, wantErr: true}, + {name: "Missing EABHmac", cfg: &Config{EABKid: "1234567890"}, wantErr: true}, + {name: "Valid EAB", cfg: &Config{EABKid: "1234567890", EABHmac: "1234567890"}, wantErr: false}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + yaml := fmt.Appendf(nil, "eab_kid: %s\neab_hmac: %s", test.cfg.EABKid, test.cfg.EABHmac) + cfg := Config{} + err := serialization.UnmarshalValidateYAML(yaml, &cfg) + if (err != nil) != test.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, test.wantErr) + } + }) + } +} diff --git a/internal/autocert/provider.go b/internal/autocert/provider.go index 731346e8..83dda295 100644 --- a/internal/autocert/provider.go +++ b/internal/autocert/provider.go @@ -81,6 +81,10 @@ func (p *Provider) GetExpiries() CertExpiries { } func (p *Provider) GetLastFailure() (time.Time, error) { + if common.IsTest { + return time.Time{}, nil + } + if p.lastFailure.IsZero() { data, err := os.ReadFile(LastFailureFile) if err != nil { @@ -95,12 +99,18 @@ func (p *Provider) GetLastFailure() (time.Time, error) { } func (p *Provider) UpdateLastFailure() error { + if common.IsTest { + return nil + } t := time.Now() p.lastFailure = t return os.WriteFile(LastFailureFile, t.AppendFormat(nil, time.RFC3339), 0o600) } func (p *Provider) ClearLastFailure() error { + if common.IsTest { + return nil + } p.lastFailure = time.Time{} return os.Remove(LastFailureFile) } @@ -289,13 +299,23 @@ func (p *Provider) registerACME() error { if p.user.Registration != nil { return nil } - if reg, err := p.client.Registration.ResolveAccountByKey(); err == nil { + + reg, err := p.client.Registration.ResolveAccountByKey() + if err == nil { p.user.Registration = reg log.Info().Msg("reused acme registration from private key") return nil } - reg, err := p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if p.cfg.EABKid != "" && p.cfg.EABHmac != "" { + reg, err = p.client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: p.cfg.EABKid, + HmacEncoded: p.cfg.EABHmac, + }) + } else { + reg, err = p.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + } if err != nil { return err } diff --git a/internal/autocert/provider_test/custom_test.go b/internal/autocert/provider_test/custom_test.go index e51a9ac5..3f04762d 100644 --- a/internal/autocert/provider_test/custom_test.go +++ b/internal/autocert/provider_test/custom_test.go @@ -138,6 +138,45 @@ func TestObtainCertFromCustomProvider(t *testing.T) { require.True(t, time.Now().Before(x509Cert.NotAfter)) require.True(t, time.Now().After(x509Cert.NotBefore)) }) + + t.Run("obtain cert with EAB from custom step-ca server", func(t *testing.T) { + cfg := &autocert.Config{ + Email: "test@example.com", + Domains: []string{"test.example.com"}, + Provider: autocert.ProviderCustom, + CADirURL: acmeServer.URL() + "/acme/acme/directory", + CertPath: "certs/stepca-eab-test.crt", + KeyPath: "certs/stepca-eab-test.key", + ACMEKeyPath: "certs/stepca-eab-test-acme.key", + HTTPClient: acmeServer.httpClient(), + EABKid: "kid-123", + EABHmac: base64.RawURLEncoding.EncodeToString([]byte("secret")), + } + + err := error(cfg.Validate()) + require.NoError(t, err) + + user, legoCfg, err := cfg.GetLegoConfig() + require.NoError(t, err) + require.NotNil(t, user) + require.NotNil(t, legoCfg) + + provider := autocert.NewProvider(cfg, user, legoCfg) + require.NotNil(t, provider) + + err = provider.ObtainCert() + require.NoError(t, err) + + cert, err := provider.GetCert(nil) + require.NoError(t, err) + require.NotNil(t, cert) + + x509Cert, err := x509.ParseCertificate(cert.Certificate[0]) + require.NoError(t, err) + require.Contains(t, x509Cert.DNSNames, "test.example.com") + require.True(t, time.Now().Before(x509Cert.NotAfter)) + require.True(t, time.Now().After(x509Cert.NotBefore)) + }) } // testACMEServer implements a minimal ACME server for testing.