diff --git a/acme/api/account.go b/acme/api/account.go index 30d406e4..2e15ad40 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -105,7 +105,7 @@ func (h *Handler) NewAccount(w http.ResponseWriter, r *http.Request) { return } - acc := &acme.Account{ + acc = &acme.Account{ Key: jwk, Contact: nar.Contact, Status: acme.StatusValid, diff --git a/acme/api/account_test.go b/acme/api/account_test.go index 831a218a..d8fdff84 100644 --- a/acme/api/account_test.go +++ b/acme/api/account_test.go @@ -278,18 +278,13 @@ func TestHandler_GetOrdersByAccountID(t *testing.T) { } func TestHandler_NewAccount(t *testing.T) { - accID := "accountID" - acc := acme.Account{ - ID: accID, - Status: "valid", - Orders: fmt.Sprintf("https://ca.smallstep.com/acme/account/%s/orders", accID), - } prov := newProv() provName := url.PathEscape(prov.GetName()) baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} type test struct { db acme.DB + acc *acme.Account ctx context.Context statusCode int err *acme.Error @@ -372,7 +367,7 @@ func TestHandler_NewAccount(t *testing.T) { err: acme.NewErrorISE("jwk expected in request context"), } }, - "fail/NewAccount-error": func(t *testing.T) test { + "fail/db.CreateAccount-error": func(t *testing.T) test { nar := &NewAccountRequest{ Contact: []string{"foo", "bar"}, } @@ -410,20 +405,18 @@ func TestHandler_NewAccount(t *testing.T) { return test{ db: &acme.MockDB{ MockCreateAccount: func(ctx context.Context, acc *acme.Account) error { + acc.ID = "accountID" assert.Equals(t, acc.Contact, nar.Contact) assert.Equals(t, acc.Key, jwk) return nil }, - /* - getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string { - assert.Equals(t, typ, acme.AccountLink) - assert.True(t, abs) - assert.True(t, abs) - assert.Equals(t, baseURL, acme.BaseURLFromContext(ctx)) - return fmt.Sprintf("%s/acme/%s/account/%s", - baseURL.String(), provName, accID) - }, - */ + }, + acc: &acme.Account{ + ID: "accountID", + Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + Orders: "https://test.ca.smallstep.com/acme/test@acme-provisioner.com/account/accountID/orders", }, ctx: ctx, statusCode: 201, @@ -435,12 +428,21 @@ func TestHandler_NewAccount(t *testing.T) { } b, err := json.Marshal(nar) assert.FatalError(t, err) + jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + acc := &acme.Account{ + ID: "accountID", + Key: jwk, + Status: acme.StatusValid, + Contact: []string{"foo", "bar"}, + } ctx := context.WithValue(context.Background(), provisionerContextKey, prov) ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{value: b}) - ctx = context.WithValue(ctx, accContextKey, &acc) + ctx = context.WithValue(ctx, accContextKey, acc) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ ctx: ctx, + acc: acc, statusCode: 200, } }, @@ -448,7 +450,7 @@ func TestHandler_NewAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db} + h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} req := httptest.NewRequest("GET", "/foo/bar", nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() @@ -471,19 +473,19 @@ func TestHandler_NewAccount(t *testing.T) { assert.Equals(t, ae.Subproblems, tc.err.Subproblems) assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"}) } else { - expB, err := json.Marshal(acc) + expB, err := json.Marshal(tc.acc) assert.FatalError(t, err) assert.Equals(t, bytes.TrimSpace(body), expB) assert.Equals(t, res.Header["Location"], []string{fmt.Sprintf("%s/acme/%s/account/%s", baseURL.String(), - provName, accID)}) + provName, "accountID")}) assert.Equals(t, res.Header["Content-Type"], []string{"application/json"}) } }) } } -func TestHandlerGetUpdateAccount(t *testing.T) { +func TestHandler_GetUpdateAccount(t *testing.T) { accID := "accountID" acc := acme.Account{ ID: accID, @@ -594,16 +596,6 @@ func TestHandlerGetUpdateAccount(t *testing.T) { assert.Equals(t, upd.ID, acc.ID) return nil }, - /* - getLink: func(ctx context.Context, typ acme.Link, abs bool, ins ...string) string { - assert.Equals(t, typ, acme.AccountLink) - assert.True(t, abs) - assert.Equals(t, acme.BaseURLFromContext(ctx), baseURL) - assert.Equals(t, ins, []string{accID}) - return fmt.Sprintf("%s/acme/%s/account/%s", - baseURL.String(), provName, accID) - }, - */ }, ctx: ctx, statusCode: 200, @@ -639,16 +631,6 @@ func TestHandlerGetUpdateAccount(t *testing.T) { assert.Equals(t, upd.ID, acc.ID) return nil }, - /* - getLink: func(ctx context.Context, typ acme.Link, abs bool, ins ...string) string { - assert.Equals(t, typ, acme.AccountLink) - assert.True(t, abs) - assert.Equals(t, acme.BaseURLFromContext(ctx), baseURL) - assert.Equals(t, ins, []string{accID}) - return fmt.Sprintf("%s/acme/%s/account/%s", - baseURL.String(), provName, accID) - }, - */ }, ctx: ctx, statusCode: 200, @@ -660,18 +642,6 @@ func TestHandlerGetUpdateAccount(t *testing.T) { ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isPostAsGet: true}) ctx = context.WithValue(ctx, baseURLContextKey, baseURL) return test{ - /* - auth: &mockAcmeAuthority{ - getLink: func(ctx context.Context, typ acme.Link, abs bool, ins ...string) string { - assert.Equals(t, typ, acme.AccountLink) - assert.True(t, abs) - assert.Equals(t, acme.BaseURLFromContext(ctx), baseURL) - assert.Equals(t, ins, []string{accID}) - return fmt.Sprintf("%s/acme/%s/account/%s", - baseURL, provName, accID) - }, - }, - */ ctx: ctx, statusCode: 200, } @@ -680,7 +650,7 @@ func TestHandlerGetUpdateAccount(t *testing.T) { for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db} + h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} req := httptest.NewRequest("GET", "/foo/bar", nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() diff --git a/acme/api/handler.go b/acme/api/handler.go index 5960d49c..47c93dfc 100644 --- a/acme/api/handler.go +++ b/acme/api/handler.go @@ -38,10 +38,11 @@ type payloadInfo struct { // Handler is the ACME API request handler. type Handler struct { - db acme.DB - backdate provisioner.Duration - ca acme.CertificateAuthority - linker Linker + db acme.DB + backdate provisioner.Duration + ca acme.CertificateAuthority + linker Linker + validateChallengeOptions *acme.ValidateChallengeOptions } // HandlerOptions required to create a new ACME API request handler. @@ -63,11 +64,24 @@ type HandlerOptions struct { // NewHandler returns a new ACME API handler. func NewHandler(ops HandlerOptions) api.RouterHandler { + client := http.Client{ + Timeout: time.Duration(30 * time.Second), + } + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + } return &Handler{ ca: ops.CA, db: ops.DB, backdate: ops.Backdate, linker: NewLinker(ops.DNS, ops.Prefix), + validateChallengeOptions: &acme.ValidateChallengeOptions{ + HTTPGet: client.Get, + LookupTxt: net.LookupTXT, + TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { + return tls.DialWithDialer(dialer, network, addr, config) + }, + }, } } @@ -212,24 +226,12 @@ func (h *Handler) GetChallenge(w http.ResponseWriter, r *http.Request) { "account '%s' does not own challenge '%s'", acc.ID, ch.ID)) return } - client := http.Client{ - Timeout: time.Duration(30 * time.Second), - } - dialer := &net.Dialer{ - Timeout: 30 * time.Second, - } jwk, err := jwkFromContext(ctx) if err != nil { api.WriteError(w, err) return } - if err = ch.Validate(ctx, h.db, jwk, acme.ValidateOptions{ - HTTPGet: client.Get, - LookupTxt: net.LookupTXT, - TLSDial: func(network, addr string, config *tls.Config) (*tls.Conn, error) { - return tls.DialWithDialer(dialer, network, addr, config) - }, - }); err != nil { + if err = ch.Validate(ctx, h.db, jwk, h.validateChallengeOptions); err != nil { api.WriteError(w, acme.WrapErrorISE(err, "error validating challenge")) return } diff --git a/acme/api/handler_test.go b/acme/api/handler_test.go index 34c720f1..70d2dc14 100644 --- a/acme/api/handler_test.go +++ b/acme/api/handler_test.go @@ -8,14 +8,17 @@ import ( "encoding/pem" "fmt" "io/ioutil" + "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/go-chi/chi" + "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" + "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" ) @@ -438,16 +441,21 @@ func ch() acme.Challenge { func TestHandler_GetChallenge(t *testing.T) { chiCtx := chi.NewRouteContext() chiCtx.URLParams.Add("chID", "chID") + chiCtx.URLParams.Add("authzID", "authzID") prov := newProv() provName := url.PathEscape(prov.GetName()) + baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"} - url := fmt.Sprintf("%s/acme/challenge/%s", baseURL, "chID") + + url := fmt.Sprintf("%s/acme/%s/challenge/%s/%s", + baseURL.String(), provName, "authzID", "chID") type test struct { db acme.DB + vco *acme.ValidateChallengeOptions ctx context.Context statusCode int - ch acme.Challenge + ch *acme.Challenge err *acme.Error } var tests = map[string]func(t *testing.T) test{ @@ -485,84 +493,177 @@ func TestHandler_GetChallenge(t *testing.T) { err: acme.NewError(acme.ErrorMalformedType, "payload expected in request context"), } }, - /* - "fail/validate-challenge-error": func(t *testing.T) test { - acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, accContextKey, acc) - ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) - ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) - return test{ - db: &acme.MockDB{ - MockError: acme.NewError(acme.ErrorUnauthorizedType, "unauthorized"), + "fail/db.GetChallenge-error": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + return test{ + db: &acme.MockDB{ + MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) { + assert.Equals(t, chID, "chID") + assert.Equals(t, azID, "authzID") + return nil, acme.NewErrorISE("force") }, - ctx: ctx, - statusCode: 401, - err: acme.NewError(acme.ErrorUnauthorizedType, "unauthorized"), - } - }, - "fail/get-challenge-error": func(t *testing.T) test { - acc := &acme.Account{ID: "accID"} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, accContextKey, acc) - ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isPostAsGet: true}) - ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) - return test{ - db: &acme.MockDB{ - MockError: acme.NewError(acme.ErrorUnauthorizedType, "unauthorized"), + }, + ctx: ctx, + statusCode: 500, + err: acme.NewErrorISE("force"), + } + }, + "fail/account-id-mismatch": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + return test{ + db: &acme.MockDB{ + MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) { + assert.Equals(t, chID, "chID") + assert.Equals(t, azID, "authzID") + return &acme.Challenge{AccountID: "foo"}, nil }, - ctx: ctx, - statusCode: 401, - err: acme.NewError(acme.ErrorUnauthorizedType, "unauthorized"), - } - }, - "ok/validate-challenge": func(t *testing.T) test { - key, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) - assert.FatalError(t, err) - acc := &acme.Account{ID: "accID", Key: key} - ctx := context.WithValue(context.Background(), provisionerContextKey, prov) - ctx = context.WithValue(ctx, accContextKey, acc) - ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) - ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) - ctx = context.WithValue(ctx, baseURLContextKey, baseURL) - ch := ch() - ch.Status = "valid" - ch.Validated = time.Now().UTC().Format(time.RFC3339) - return test{ - db: &acme.MockDB{ - MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) { - assert.Equals(t, chID, ch.ID) - return &ch, nil - }, - getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string { - var ret string - switch count { - case 0: - assert.Equals(t, typ, acme.AuthzLink) - assert.True(t, abs) - assert.Equals(t, in, []string{ch.AuthzID}) - ret = fmt.Sprintf("%s/acme/%s/authz/%s", baseURL.String(), provName, ch.AuthzID) - case 1: - assert.Equals(t, typ, acme.ChallengeLink) - assert.True(t, abs) - assert.Equals(t, in, []string{ch.ID}) - ret = url - } - count++ - return ret - }, + }, + ctx: ctx, + statusCode: 401, + err: acme.NewError(acme.ErrorUnauthorizedType, "accout id mismatch"), + } + }, + "fail/no-jwk": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + return test{ + db: &acme.MockDB{ + MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) { + assert.Equals(t, chID, "chID") + assert.Equals(t, azID, "authzID") + return &acme.Challenge{AccountID: "accID"}, nil }, - ctx: ctx, - statusCode: 200, - ch: ch, - } - }, - */ + }, + ctx: ctx, + statusCode: 500, + err: acme.NewErrorISE("missing jwk"), + } + }, + "fail/nil-jwk": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) + ctx = context.WithValue(ctx, jwkContextKey, nil) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + return test{ + db: &acme.MockDB{ + MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) { + assert.Equals(t, chID, "chID") + assert.Equals(t, azID, "authzID") + return &acme.Challenge{AccountID: "accID"}, nil + }, + }, + ctx: ctx, + statusCode: 500, + err: acme.NewErrorISE("nil jwk"), + } + }, + "fail/validate-challenge-error": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) + _jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + _pub := _jwk.Public() + ctx = context.WithValue(ctx, jwkContextKey, &_pub) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + return test{ + db: &acme.MockDB{ + MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) { + assert.Equals(t, chID, "chID") + assert.Equals(t, azID, "authzID") + return &acme.Challenge{ + Status: acme.StatusPending, + Type: "http-01", + AccountID: "accID", + }, nil + }, + MockUpdateChallenge: func(ctx context.Context, ch *acme.Challenge) error { + assert.Equals(t, ch.Status, acme.StatusPending) + assert.Equals(t, ch.Type, "http-01") + assert.Equals(t, ch.AccountID, "accID") + assert.HasSuffix(t, ch.Error.Type, acme.ErrorConnectionType.String()) + return acme.NewErrorISE("force") + }, + }, + vco: &acme.ValidateChallengeOptions{ + HTTPGet: func(string) (*http.Response, error) { + return nil, errors.New("force") + }, + }, + ctx: ctx, + statusCode: 500, + err: acme.NewErrorISE("force"), + } + }, + "ok": func(t *testing.T) test { + acc := &acme.Account{ID: "accID"} + ctx := context.WithValue(context.Background(), provisionerContextKey, prov) + ctx = context.WithValue(ctx, accContextKey, acc) + ctx = context.WithValue(ctx, payloadContextKey, &payloadInfo{isEmptyJSON: true}) + _jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) + assert.FatalError(t, err) + _pub := _jwk.Public() + ctx = context.WithValue(ctx, jwkContextKey, &_pub) + ctx = context.WithValue(ctx, baseURLContextKey, baseURL) + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + return test{ + db: &acme.MockDB{ + MockGetChallenge: func(ctx context.Context, chID, azID string) (*acme.Challenge, error) { + assert.Equals(t, chID, "chID") + assert.Equals(t, azID, "authzID") + return &acme.Challenge{ + ID: "chID", + AuthzID: "authzID", + Status: acme.StatusPending, + Type: "http-01", + AccountID: "accID", + }, nil + }, + MockUpdateChallenge: func(ctx context.Context, ch *acme.Challenge) error { + assert.Equals(t, ch.Status, acme.StatusPending) + assert.Equals(t, ch.Type, "http-01") + assert.Equals(t, ch.AccountID, "accID") + assert.HasSuffix(t, ch.Error.Type, acme.ErrorConnectionType.String()) + return nil + }, + }, + ch: &acme.Challenge{ + ID: "chID", + AuthzID: "authzID", + Status: acme.StatusPending, + Type: "http-01", + AccountID: "accID", + URL: url, + Error: acme.NewError(acme.ErrorConnectionType, "force"), + }, + vco: &acme.ValidateChallengeOptions{ + HTTPGet: func(string) (*http.Response, error) { + return nil, errors.New("force") + }, + }, + ctx: ctx, + statusCode: 200, + } + }, } for name, run := range tests { tc := run(t) t.Run(name, func(t *testing.T) { - h := &Handler{db: tc.db, linker: NewLinker("dns", "acme")} + h := &Handler{db: tc.db, linker: NewLinker("dns", "acme"), validateChallengeOptions: tc.vco} req := httptest.NewRequest("GET", url, nil) req = req.WithContext(tc.ctx) w := httptest.NewRecorder() diff --git a/acme/api/middleware.go b/acme/api/middleware.go index a021c936..f2a35c3a 100644 --- a/acme/api/middleware.go +++ b/acme/api/middleware.go @@ -462,7 +462,7 @@ func provisionerFromContext(ctx context.Context) (acme.Provisioner, error) { func payloadFromContext(ctx context.Context) (*payloadInfo, error) { val, ok := ctx.Value(payloadContextKey).(*payloadInfo) if !ok || val == nil { - return nil, acme.NewError(acme.ErrorMalformedType, "payload expected in request context") + return nil, acme.NewErrorISE("payload expected in request context") } return val, nil } diff --git a/acme/challenge.go b/acme/challenge.go index 2abc808c..2c6f5fb1 100644 --- a/acme/challenge.go +++ b/acme/challenge.go @@ -47,7 +47,7 @@ func (ch *Challenge) ToLog() (interface{}, error) { // type using the DB interface. // satisfactorily validated, the 'status' and 'validated' attributes are // updated. -func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, vo ValidateOptions) error { +func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { // If already valid or invalid then return without performing validation. if ch.Status == StatusValid || ch.Status == StatusInvalid { return nil @@ -64,7 +64,7 @@ func (ch *Challenge) Validate(ctx context.Context, db DB, jwk *jose.JSONWebKey, } } -func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo ValidateOptions) error { +func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { url := fmt.Sprintf("http://%s/.well-known/acme-challenge/%s", ch.Value, ch.Token) resp, err := vo.HTTPGet(url) @@ -105,7 +105,7 @@ func http01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWeb return nil } -func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo ValidateOptions) error { +func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { config := &tls.Config{ NextProtos: []string{"acme-tls/1"}, ServerName: ch.Value, @@ -197,7 +197,7 @@ func tlsalpn01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSON "incorrect certificate for tls-alpn-01 challenge: missing acmeValidationV1 extension")) } -func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo ValidateOptions) error { +func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, vo *ValidateChallengeOptions) error { // Normalize domain for wildcard DNS names // This is done to avoid making TXT lookups for domains like // _acme-challenge.*.example.com @@ -263,8 +263,8 @@ type httpGetter func(string) (*http.Response, error) type lookupTxt func(string) ([]string, error) type tlsDialer func(network, addr string, config *tls.Config) (*tls.Conn, error) -// ValidateOptions are ACME challenge validator functions. -type ValidateOptions struct { +// ValidateChallengeOptions are ACME challenge validator functions. +type ValidateChallengeOptions struct { HTTPGet httpGetter LookupTxt lookupTxt TLSDial tlsDialer