diff --git a/cmd/cmd_run.go b/cmd/cmd_run.go index 8135e3546d..589db53d60 100644 --- a/cmd/cmd_run.go +++ b/cmd/cmd_run.go @@ -182,6 +182,9 @@ func register(ctx *cli.Context, client *lego.Client) (*registration.Resource, er log.Fatal("You did not accept the TOS. Unable to proceed.") } + if ctx.String("server") == lego.ZeroSSLDirectory { + return client.Registration.RegisterWithZeroSSL(registration.RegisterOptions{TermsOfServiceAgreed: true}) + } if ctx.Bool(flgEAB) { kid := ctx.String(flgKID) hmacEncoded := ctx.String(flgHMAC) diff --git a/cmd/setup.go b/cmd/setup.go index 28c2c8eef3..3d16a28a23 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -82,7 +82,7 @@ func newClient(ctx *cli.Context, acc registration.User, keyType certcrypto.KeyTy log.Fatalf("Could not create client: %v", err) } - if client.GetExternalAccountRequired() && !ctx.IsSet(flgEAB) { + if client.GetExternalAccountRequired() && !ctx.IsSet(flgEAB) && config.CADirURL != lego.ZeroSSLDirectory { log.Fatalf("Server requires External Account Binding. Use --%s with --%s and --%s.", flgEAB, flgKID, flgHMAC) } diff --git a/lego/client_config.go b/lego/client_config.go index fdf1a55f85..f8cc3bc727 100644 --- a/lego/client_config.go +++ b/lego/client_config.go @@ -38,6 +38,9 @@ const ( // LEDirectoryStaging URL to the Let's Encrypt staging. LEDirectoryStaging = "https://acme-staging-v02.api.letsencrypt.org/directory" + + // ZeroSSLDirectory URL to the ZeroSSL production. + ZeroSSLDirectory = "https://acme.zerossl.com/v2/DV90" ) type Config struct { diff --git a/registration/registar.go b/registration/registar.go index 15c28bad67..34a32e3c76 100644 --- a/registration/registar.go +++ b/registration/registar.go @@ -1,8 +1,13 @@ package registration import ( + "bytes" + "encoding/json" "errors" + "fmt" + "io" "net/http" + "net/url" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" @@ -69,6 +74,54 @@ func (r *Registrar) Register(options RegisterOptions) (*Resource, error) { return &Resource{URI: account.Location, Body: account.Account}, nil } +func createZeroSSLAccount(email string) (string, string, error) { + newAccountURL := "https://api.zerossl.com/acme/eab-credentials-email" + data := struct { + Success bool `json:"success"` + KID string `json:"eab_kid"` + HMAC string `json:"eab_hmac_key"` + }{} + + resp, err := http.PostForm(newAccountURL, url.Values{"email": {email}}) + if err != nil { + return "", "", fmt.Errorf("sending request: %w", err) + } + defer resp.Body.Close() + + // ZeroSSL might return errors as plain-text messages instead of JSON, + // so we buffer the response to be able to return it as error. + var rawResp bytes.Buffer + r := io.TeeReader(io.LimitReader(resp.Body, 10*1024), &rawResp) // Limit response to 10KB + if err := json.NewDecoder(r).Decode(&data); err != nil { + // It is likely not a JSON but a plain-text error message + _, _ = io.ReadAll(r) // read the rest of the body + return "", "", fmt.Errorf("parsing response: %w. Original response:\n%s", err, rawResp.String()) + } + + if !data.Success { + return "", "", errors.New("received success=false") + } + return data.KID, data.HMAC, nil +} + +// RegisterWithZeroSSL Register the current account to the ZeroSSL server. +func (r *Registrar) RegisterWithZeroSSL(options RegisterOptions) (*Resource, error) { + if r.user.GetEmail() == "" { + return nil, errors.New("acme: cannot register ZeroSSL account without email address") + } + + kid, hmac, err := createZeroSSLAccount(r.user.GetEmail()) + if err != nil { + return nil, fmt.Errorf("acme: error registering new ZeroSSL account: %w", err) + } + + return r.RegisterWithExternalAccountBinding(RegisterEABOptions{ + TermsOfServiceAgreed: options.TermsOfServiceAgreed, + Kid: kid, + HmacEncoded: hmac, + }) +} + // RegisterWithExternalAccountBinding Register the current account to the ACME server. func (r *Registrar) RegisterWithExternalAccountBinding(options RegisterEABOptions) (*Resource, error) { accMsg := acme.Account{