package ca import ( "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/pkg/errors" "github.com/smallstep/assert" "github.com/smallstep/certificates/acme" acmeAPI "github.com/smallstep/certificates/acme/api" "github.com/smallstep/certificates/api/render" "go.step.sm/crypto/jose" "go.step.sm/crypto/pemutil" ) func TestNewACMEClient(t *testing.T) { type test struct { ops []ClientOption r1, r2 interface{} rc1, rc2 int err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", NewAccount: srv.URL + "/bar", NewOrder: srv.URL + "/baz", RevokeCert: srv.URL + "/zip", KeyChange: srv.URL + "/blorp", } acc := acme.Account{ Contact: []string{"max", "mariano"}, Status: "valid", OrdersURL: "orders-url", } tests := map[string]func(t *testing.T) test{ "fail/client-option-error": func(t *testing.T) test { return test{ ops: []ClientOption{ func(o *clientOptions) error { return errors.New("force") }, }, err: errors.New("force"), } }, "fail/get-directory": func(t *testing.T) test { return test{ ops: []ClientOption{WithTransport(http.DefaultTransport)}, r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "fail/bad-directory": func(t *testing.T) test { return test{ ops: []ClientOption{WithTransport(http.DefaultTransport)}, r1: "foo", rc1: 200, err: errors.New("error reading http://127.0.0.1"), } }, "fail/error-post-newAccount": func(t *testing.T) test { return test{ ops: []ClientOption{WithTransport(http.DefaultTransport)}, r1: dir, rc1: 200, r2: acme.NewError(acme.ErrorAccountDoesNotExistType, "account does not exist"), rc2: 400, err: errors.New("Account does not exist"), } }, "fail/error-bad-account": func(t *testing.T) test { return test{ ops: []ClientOption{WithTransport(http.DefaultTransport)}, r1: dir, rc1: 200, r2: "foo", rc2: 200, err: errors.New("error reading http://127.0.0.1"), } }, "ok": func(t *testing.T) test { return test{ ops: []ClientOption{WithTransport(http.DefaultTransport)}, r1: dir, rc1: 200, r2: acc, rc2: 200, } }, } accLocation := "linkitylink" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header switch { case i == 0: render.JSONStatus(w, r, tc.r1, tc.rc1) i++ case i == 1: w.Header().Set("Replay-Nonce", "abc123") render.JSONStatus(w, r, []byte{}, 200) i++ default: w.Header().Set("Location", accLocation) render.JSONStatus(w, r, tc.r2, tc.rc2) } }) if client, err := NewACMEClient(srv.URL, []string{"max", "mariano"}, tc.ops...); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, *client.dir, dir) assert.NotNil(t, client.Key) assert.NotNil(t, client.acc) assert.Equals(t, client.kid, accLocation) } } }) } } func TestACMEClient_GetDirectory(t *testing.T) { c := &ACMEClient{ dir: &acmeAPI.Directory{ NewNonce: "/foo", NewAccount: "/bar", NewOrder: "/baz", RevokeCert: "/zip", KeyChange: "/blorp", }, } dir, err := c.GetDirectory() assert.FatalError(t, err) assert.Equals(t, c.dir, dir) } func TestACMEClient_GetNonce(t *testing.T) { type test struct { r1 interface{} rc1 int err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, } tests := map[string]func(t *testing.T) test{ "fail/GET-nonce": func(t *testing.T) test { return test{ r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "ok": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, } }, } expectedNonce := "abc123" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) render.JSONStatus(w, r, tc.r1, tc.rc1) }) if nonce, err := ac.GetNonce(); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, expectedNonce, nonce) } } }) } } func TestACMEClient_post(t *testing.T) { type test struct { payload []byte Key *jose.JSONWebKey ops []withHeaderOption r1, r2 interface{} rc1, rc2 int jwkInJWS bool client *ACMEClient err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) acc := acme.Account{ Contact: []string{"max", "mariano"}, Status: "valid", OrdersURL: "orders-url", } ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, Key: jwk, kid: "foobar", } tests := map[string]func(t *testing.T) test{ "fail/account-not-configured": func(t *testing.T) test { return test{ client: &ACMEClient{}, r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("acme client not configured with account"), } }, "fail/GET-nonce": func(t *testing.T) test { return test{ client: ac, r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "ok/jwk": func(t *testing.T) test { return test{ client: ac, r1: []byte{}, rc1: 200, r2: acc, rc2: 200, ops: []withHeaderOption{withJWK(ac)}, jwkInJWS: true, } }, "ok/kid": func(t *testing.T) test { return test{ client: ac, r1: []byte{}, rc1: 200, r2: acc, rc2: 200, ops: []withHeaderOption{withKid(ac)}, } }, } expectedNonce := "abc123" url := srv.URL + "/foo" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) if i == 0 { render.JSONStatus(w, r, tc.r1, tc.rc1) i++ return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, expectedNonce) jwsURL, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, jwsURL, url) if tc.jwkInJWS { assert.Equals(t, hdr.JSONWebKey.KeyID, ac.Key.KeyID) } else { assert.Equals(t, hdr.KeyID, ac.kid) } render.JSONStatus(w, r, tc.r2, tc.rc2) }) if resp, err := tc.client.post(tc.payload, url, tc.ops...); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { var res acme.Account assert.FatalError(t, readJSON(resp.Body, &res)) assert.Equals(t, res, acc) } } }) } } func TestACMEClient_NewOrder(t *testing.T) { type test struct { ops []withHeaderOption r1, r2 interface{} rc1, rc2 int err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", NewOrder: srv.URL + "/bar", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) now := time.Now().UTC().Round(time.Second) nor := acmeAPI.NewOrderRequest{ Identifiers: []acme.Identifier{ {Type: "dns", Value: "example.com"}, {Type: "dns", Value: "acme.example.com"}, }, NotBefore: now, NotAfter: now.Add(time.Minute), } norb, err := json.Marshal(nor) assert.FatalError(t, err) ord := acme.Order{ Status: "valid", ExpiresAt: now, // "soon" FinalizeURL: "finalize-url", } ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, Key: jwk, kid: "foobar", } tests := map[string]func(t *testing.T) test{ "fail/client-post": func(t *testing.T) test { return test{ r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "fail/newOrder-error": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc2: 400, ops: []withHeaderOption{withKid(ac)}, err: errors.New("The request message was malformed"), } }, "fail/bad-order": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: "foo", rc2: 200, ops: []withHeaderOption{withKid(ac)}, err: errors.New("error reading http://127.0.0.1"), } }, "ok": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: ord, rc2: 200, ops: []withHeaderOption{withKid(ac)}, } }, } expectedNonce := "abc123" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) if i == 0 { render.JSONStatus(w, r, tc.r1, tc.rc1) i++ return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, expectedNonce) jwsURL, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, jwsURL, dir.NewOrder) assert.Equals(t, hdr.KeyID, ac.kid) payload, err := jws.Verify(ac.Key.Public()) assert.FatalError(t, err) assert.Equals(t, payload, norb) render.JSONStatus(w, r, tc.r2, tc.rc2) }) if res, err := ac.NewOrder(norb); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, *res, ord) } } }) } } func TestACMEClient_GetOrder(t *testing.T) { type test struct { r1, r2 interface{} rc1, rc2 int err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) ord := acme.Order{ Status: "valid", ExpiresAt: time.Now().UTC().Round(time.Second), // "soon" FinalizeURL: "finalize-url", } ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, Key: jwk, kid: "foobar", } tests := map[string]func(t *testing.T) test{ "fail/client-post": func(t *testing.T) test { return test{ r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "fail/getOrder-error": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc2: 400, err: errors.New("The request message was malformed"), } }, "fail/bad-order": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: "foo", rc2: 200, err: errors.New("error reading http://127.0.0.1"), } }, "ok": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: ord, rc2: 200, } }, } expectedNonce := "abc123" url := srv.URL + "/hullaballoo" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) if i == 0 { render.JSONStatus(w, r, tc.r1, tc.rc1) i++ return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, expectedNonce) jwsURL, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, jwsURL, url) assert.Equals(t, hdr.KeyID, ac.kid) payload, err := jws.Verify(ac.Key.Public()) assert.FatalError(t, err) assert.Equals(t, len(payload), 0) render.JSONStatus(w, r, tc.r2, tc.rc2) }) if res, err := ac.GetOrder(url); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, *res, ord) } } }) } } func TestACMEClient_GetAuthz(t *testing.T) { type test struct { r1, r2 interface{} rc1, rc2 int err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) az := acme.Authorization{ Status: "valid", ExpiresAt: time.Now().UTC().Round(time.Second), Identifier: acme.Identifier{Type: "dns", Value: "example.com"}, } ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, Key: jwk, kid: "foobar", } tests := map[string]func(t *testing.T) test{ "fail/client-post": func(t *testing.T) test { return test{ r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "fail/getChallenge-error": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc2: 400, err: errors.New("The request message was malformed"), } }, "fail/bad-challenge": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: "foo", rc2: 200, err: errors.New("error reading http://127.0.0.1"), } }, "ok": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: az, rc2: 200, } }, } expectedNonce := "abc123" url := srv.URL + "/hullaballoo" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) if i == 0 { render.JSONStatus(w, r, tc.r1, tc.rc1) i++ return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, expectedNonce) jwsURL, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, jwsURL, url) assert.Equals(t, hdr.KeyID, ac.kid) payload, err := jws.Verify(ac.Key.Public()) assert.FatalError(t, err) assert.Equals(t, len(payload), 0) render.JSONStatus(w, r, tc.r2, tc.rc2) }) if res, err := ac.GetAuthz(url); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, *res, az) } } }) } } func TestACMEClient_GetChallenge(t *testing.T) { type test struct { r1, r2 interface{} rc1, rc2 int err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) ch := acme.Challenge{ Type: "http-01", Status: "valid", Token: "foo", } ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, Key: jwk, kid: "foobar", } tests := map[string]func(t *testing.T) test{ "fail/client-post": func(t *testing.T) test { return test{ r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "fail/getChallenge-error": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc2: 400, err: errors.New("The request message was malformed"), } }, "fail/bad-challenge": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: "foo", rc2: 200, err: errors.New("error reading http://127.0.0.1"), } }, "ok": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: ch, rc2: 200, } }, } expectedNonce := "abc123" url := srv.URL + "/hullaballoo" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) if i == 0 { render.JSONStatus(w, r, tc.r1, tc.rc1) i++ return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, expectedNonce) jwsURL, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, jwsURL, url) assert.Equals(t, hdr.KeyID, ac.kid) payload, err := jws.Verify(ac.Key.Public()) assert.FatalError(t, err) assert.Equals(t, len(payload), 0) render.JSONStatus(w, r, tc.r2, tc.rc2) }) if res, err := ac.GetChallenge(url); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, *res, ch) } } }) } } func TestACMEClient_ValidateChallenge(t *testing.T) { type test struct { r1, r2 interface{} rc1, rc2 int err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) ch := acme.Challenge{ Type: "http-01", Status: "valid", Token: "foo", } ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, Key: jwk, kid: "foobar", } tests := map[string]func(t *testing.T) test{ "fail/client-post": func(t *testing.T) test { return test{ r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "fail/getChallenge-error": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc2: 400, err: errors.New("The request message was malformed"), } }, "fail/bad-challenge": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: "foo", rc2: 200, err: errors.New("error reading http://127.0.0.1"), } }, "ok": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: ch, rc2: 200, } }, } expectedNonce := "abc123" url := srv.URL + "/hullaballoo" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) if i == 0 { render.JSONStatus(w, r, tc.r1, tc.rc1) i++ return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, expectedNonce) jwsURL, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, jwsURL, url) assert.Equals(t, hdr.KeyID, ac.kid) payload, err := jws.Verify(ac.Key.Public()) assert.FatalError(t, err) assert.Equals(t, payload, []byte("{}")) render.JSONStatus(w, r, tc.r2, tc.rc2) }) if err := ac.ValidateChallenge(url); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } }) } } func TestACMEClient_ValidateWithPayload(t *testing.T) { key, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header t.Log(r.RequestURI) w.Header().Set("Replay-Nonce", "nonce") switch r.RequestURI { case "/nonce": render.JSONStatus(w, r, []byte{}, 200) return case "/fail-nonce": render.JSONStatus(w, r, acme.NewError(acme.ErrorMalformedType, "malformed request"), 400) return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, "nonce") _, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, hdr.KeyID, "kid") payload, err := jws.Verify(key.Public()) assert.FatalError(t, err) assert.Equals(t, payload, []byte("the-payload")) switch r.RequestURI { case "/ok": render.JSONStatus(w, r, acme.Challenge{ Type: "device-attestation-01", Status: "valid", Token: "foo", }, 200) case "/fail": render.JSONStatus(w, r, acme.NewError(acme.ErrorMalformedType, "malformed request"), 400) } })) defer srv.Close() type fields struct { client *http.Client dirLoc string dir *acmeAPI.Directory acc *acme.Account Key *jose.JSONWebKey kid string } type args struct { url string payload []byte } tests := []struct { name string fields fields args args wantErr bool }{ {"ok", fields{srv.Client(), srv.URL, &acmeAPI.Directory{ NewNonce: srv.URL + "/nonce", }, nil, key, "kid"}, args{srv.URL + "/ok", []byte("the-payload")}, false}, {"fail nonce", fields{srv.Client(), srv.URL, &acmeAPI.Directory{ NewNonce: srv.URL + "/fail-nonce", }, nil, key, "kid"}, args{srv.URL + "/ok", []byte("the-payload")}, true}, {"fail payload", fields{srv.Client(), srv.URL, &acmeAPI.Directory{ NewNonce: srv.URL + "/nonce", }, nil, key, "kid"}, args{srv.URL + "/fail", []byte("the-payload")}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &ACMEClient{ client: tt.fields.client, dirLoc: tt.fields.dirLoc, dir: tt.fields.dir, acc: tt.fields.acc, Key: tt.fields.Key, kid: tt.fields.kid, } if err := c.ValidateWithPayload(tt.args.url, tt.args.payload); (err != nil) != tt.wantErr { t.Errorf("ACMEClient.ValidateWithPayload() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestACMEClient_FinalizeOrder(t *testing.T) { type test struct { r1, r2 interface{} rc1, rc2 int err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) ord := acme.Order{ Status: "valid", ExpiresAt: time.Now(), // "soon" FinalizeURL: "finalize-url", CertificateURL: "cert-url", } _csr, err := pemutil.Read("../authority/testdata/certs/foo.csr") assert.FatalError(t, err) csr, ok := _csr.(*x509.CertificateRequest) assert.Fatal(t, ok) fr := acmeAPI.FinalizeRequest{CSR: base64.RawURLEncoding.EncodeToString(csr.Raw)} frb, err := json.Marshal(fr) assert.FatalError(t, err) ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, Key: jwk, kid: "foobar", } tests := map[string]func(t *testing.T) test{ "fail/client-post": func(t *testing.T) test { return test{ r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "fail/finalizeOrder-error": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc2: 400, err: errors.New("The request message was malformed"), } }, "fail/bad-order": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: "foo", rc2: 200, err: errors.New("error reading http://127.0.0.1"), } }, "ok": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: ord, rc2: 200, } }, } expectedNonce := "abc123" url := srv.URL + "/hullaballoo" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) if i == 0 { render.JSONStatus(w, r, tc.r1, tc.rc1) i++ return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, expectedNonce) jwsURL, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, jwsURL, url) assert.Equals(t, hdr.KeyID, ac.kid) payload, err := jws.Verify(ac.Key.Public()) assert.FatalError(t, err) assert.Equals(t, payload, frb) render.JSONStatus(w, r, tc.r2, tc.rc2) }) if err := ac.FinalizeOrder(url, csr); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } }) } } func TestACMEClient_GetAccountOrders(t *testing.T) { type test struct { r1, r2 interface{} rc1, rc2 int err error client *ACMEClient } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) orders := []string{"foo", "bar", "baz"} ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, Key: jwk, kid: "foobar", acc: &acme.Account{ Contact: []string{"max", "mariano"}, Status: "valid", OrdersURL: srv.URL + "/orders-url", }, } tests := map[string]func(t *testing.T) test{ "fail/account-not-configured": func(t *testing.T) test { return test{ client: &ACMEClient{}, err: errors.New("acme client not configured with account"), } }, "fail/client-post": func(t *testing.T) test { return test{ client: ac, r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "fail/getAccountOrders-error": func(t *testing.T) test { return test{ client: ac, r1: []byte{}, rc1: 200, r2: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc2: 400, err: errors.New("The request message was malformed"), } }, "fail/bad-accountOrders": func(t *testing.T) test { return test{ client: ac, r1: []byte{}, rc1: 200, r2: "foo", rc2: 200, err: errors.New("error reading http://127.0.0.1"), } }, "ok": func(t *testing.T) test { return test{ client: ac, r1: []byte{}, rc1: 200, r2: orders, rc2: 200, } }, } expectedNonce := "abc123" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) if i == 0 { render.JSONStatus(w, r, tc.r1, tc.rc1) i++ return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, expectedNonce) jwsURL, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, jwsURL, ac.acc.OrdersURL) assert.Equals(t, hdr.KeyID, ac.kid) payload, err := jws.Verify(ac.Key.Public()) assert.FatalError(t, err) assert.Equals(t, len(payload), 0) render.JSONStatus(w, r, tc.r2, tc.rc2) }) if res, err := tc.client.GetAccountOrders(); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, res, orders) } } }) } } func TestACMEClient_GetCertificate(t *testing.T) { type test struct { r1, r2 interface{} certBytes []byte rc1, rc2 int err error } srv := httptest.NewServer(nil) defer srv.Close() dir := acmeAPI.Directory{ NewNonce: srv.URL + "/foo", } // Retrieve transport from options. o := new(clientOptions) assert.FatalError(t, o.apply([]ClientOption{WithTransport(http.DefaultTransport)})) tr, err := o.getTransport(srv.URL) assert.FatalError(t, err) jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0) assert.FatalError(t, err) leaf, err := pemutil.ReadCertificate("../authority/testdata/certs/foo.crt") assert.FatalError(t, err) leafb := pem.EncodeToMemory(&pem.Block{ Type: "Certificate", Bytes: leaf.Raw, }) //nolint:gocritic certBytes := append(leafb, leafb...) certBytes = append(certBytes, leafb...) ac := &ACMEClient{ client: &http.Client{ Transport: tr, }, dirLoc: srv.URL, dir: &dir, Key: jwk, kid: "foobar", acc: &acme.Account{ Contact: []string{"max", "mariano"}, Status: "valid", OrdersURL: srv.URL + "/orders-url", }, } tests := map[string]func(t *testing.T) test{ "fail/client-post": func(t *testing.T) test { return test{ r1: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc1: 400, err: errors.New("The request message was malformed"), } }, "fail/getAccountOrders-error": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: acme.NewError(acme.ErrorMalformedType, "malformed request"), rc2: 400, err: errors.New("The request message was malformed"), } }, "fail/bad-certificate": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, r2: "foo", rc2: 200, err: errors.New("failed to parse any certificates from response"), } }, "ok": func(t *testing.T) test { return test{ r1: []byte{}, rc1: 200, certBytes: certBytes, } }, } expectedNonce := "abc123" url := srv.URL + "/cert/foo" for name, run := range tests { t.Run(name, func(t *testing.T) { tc := run(t) i := 0 srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equals(t, "step-http-client/1.0", r.Header.Get("User-Agent")) // check default User-Agent header w.Header().Set("Replay-Nonce", expectedNonce) if i == 0 { render.JSONStatus(w, r, tc.r1, tc.rc1) i++ return } // validate jws request protected headers and body body, err := io.ReadAll(r.Body) assert.FatalError(t, err) jws, err := jose.ParseJWS(string(body)) assert.FatalError(t, err) hdr := jws.Signatures[0].Protected assert.Equals(t, hdr.Nonce, expectedNonce) jwsURL, ok := hdr.ExtraHeaders["url"].(string) assert.Fatal(t, ok) assert.Equals(t, jwsURL, url) assert.Equals(t, hdr.KeyID, ac.kid) payload, err := jws.Verify(ac.Key.Public()) assert.FatalError(t, err) assert.Equals(t, len(payload), 0) if tc.certBytes != nil { w.Write(tc.certBytes) } else { render.JSONStatus(w, r, tc.r2, tc.rc2) } }) if crt, chain, err := ac.GetCertificate(url); err != nil { if assert.NotNil(t, tc.err) { assert.HasPrefix(t, err.Error(), tc.err.Error()) } } else { if assert.Nil(t, tc.err) { assert.Equals(t, crt, leaf) assert.Equals(t, chain, []*x509.Certificate{leaf, leaf}) } } }) } }