You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
smallstep-certificates/ca/acmeClient_test.go

1470 lines
36 KiB
Go

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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
switch {
case i == 0:
render.JSONStatus(w, tc.r1, tc.rc1)
i++
case i == 1:
w.Header().Set("Replay-Nonce", "abc123")
render.JSONStatus(w, []byte{}, 200)
i++
default:
w.Header().Set("Location", accLocation)
render.JSONStatus(w, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
render.JSONStatus(w, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
if i == 0 {
render.JSONStatus(w, tc.r1, tc.rc1)
i++
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
if i == 0 {
render.JSONStatus(w, tc.r1, tc.rc1)
i++
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
if i == 0 {
render.JSONStatus(w, tc.r1, tc.rc1)
i++
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
if i == 0 {
render.JSONStatus(w, tc.r1, tc.rc1)
i++
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
if i == 0 {
render.JSONStatus(w, tc.r1, tc.rc1)
i++
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
if i == 0 {
render.JSONStatus(w, tc.r1, tc.rc1)
i++
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
t.Log(req.RequestURI)
w.Header().Set("Replay-Nonce", "nonce")
switch req.RequestURI {
case "/nonce":
render.JSONStatus(w, []byte{}, 200)
return
case "/fail-nonce":
render.JSONStatus(w, acme.NewError(acme.ErrorMalformedType, "malformed request"), 400)
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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 req.RequestURI {
case "/ok":
render.JSONStatus(w, acme.Challenge{
Type: "device-attestation-01",
Status: "valid",
Token: "foo",
}, 200)
case "/fail":
render.JSONStatus(w, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
if i == 0 {
render.JSONStatus(w, tc.r1, tc.rc1)
i++
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
if i == 0 {
render.JSONStatus(w, tc.r1, tc.rc1)
i++
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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, 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, req *http.Request) {
assert.Equals(t, "step-http-client/1.0", req.Header.Get("User-Agent")) // check default User-Agent header
w.Header().Set("Replay-Nonce", expectedNonce)
if i == 0 {
render.JSONStatus(w, tc.r1, tc.rc1)
i++
return
}
// validate jws request protected headers and body
body, err := io.ReadAll(req.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, 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})
}
}
})
}
}