smallstep-certificates/acme/api/middleware_test.go
max furman e1409349f3 Allow relative URL for all links in ACME api ...
* Pass the request context all the way down the ACME stack.
* Save baseURL in context and use when generating ACME urls.
2020-05-14 17:32:54 -07:00

1577 lines
48 KiB
Go

package api
import (
"bytes"
"context"
"crypto"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/acme"
"github.com/smallstep/cli/jose"
"github.com/smallstep/nosql/database"
)
var testBody = []byte("foo")
func testNext(w http.ResponseWriter, r *http.Request) {
w.Write(testBody)
}
func Test_baseURLFromRequest(t *testing.T) {
tests := []struct {
name string
targetURL string
expectedResult *url.URL
requestPreparer func(*http.Request)
}{
{
"HTTPS host pass-through failed.",
"https://my.dummy.host",
&url.URL{Scheme: "https", Host: "my.dummy.host"},
nil,
},
{
"Port pass-through failed",
"https://host.with.port:8080",
&url.URL{Scheme: "https", Host: "host.with.port:8080"},
nil,
},
{
"Explicit host from Request.Host was not used.",
"https://some.target.host:8080",
&url.URL{Scheme: "https", Host: "proxied.host"},
func(r *http.Request) {
r.Host = "proxied.host"
},
},
{
"Missing Request.Host value did not result in empty string result.",
"https://some.host",
nil,
func(r *http.Request) {
r.Host = ""
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
request := httptest.NewRequest("GET", tc.targetURL, nil)
if tc.requestPreparer != nil {
tc.requestPreparer(request)
}
result := baseURLFromRequest(request)
if result == nil || tc.expectedResult == nil {
assert.Equals(t, result, tc.expectedResult)
} else if result.String() != tc.expectedResult.String() {
t.Errorf("Expected %q, but got %q", tc.expectedResult.String(), result.String())
}
})
}
}
func TestHandlerBaseURLFromRequest(t *testing.T) {
h := New(&mockAcmeAuthority{}).(*Handler)
req := httptest.NewRequest("GET", "/foo", nil)
req.Host = "test.ca.smallstep.com:8080"
w := httptest.NewRecorder()
next := func(w http.ResponseWriter, r *http.Request) {
bu := acme.BaseURLFromContext(r.Context())
if assert.NotNil(t, bu) {
assert.Equals(t, bu.Host, "test.ca.smallstep.com:8080")
assert.Equals(t, bu.Scheme, "https")
}
}
h.baseURLFromRequest(next)(w, req)
req = httptest.NewRequest("GET", "/foo", nil)
req.Host = ""
next = func(w http.ResponseWriter, r *http.Request) {
assert.Equals(t, acme.BaseURLFromContext(r.Context()), nil)
}
h.baseURLFromRequest(next)(w, req)
}
func TestHandlerAddNonce(t *testing.T) {
url := "https://ca.smallstep.com/acme/new-nonce"
type test struct {
auth acme.Interface
problem *acme.Error
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/AddNonce-error": func(t *testing.T) test {
return test{
auth: &mockAcmeAuthority{
newNonce: func() (string, error) {
return "", acme.ServerInternalErr(errors.New("force"))
},
},
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("force")),
}
},
"ok": func(t *testing.T) test {
return test{
auth: &mockAcmeAuthority{
newNonce: func() (string, error) {
return "bar", nil
},
},
statusCode: 200,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
h := New(tc.auth).(*Handler)
req := httptest.NewRequest("GET", url, nil)
w := httptest.NewRecorder()
h.addNonce(testNext)(w, req)
res := w.Result()
assert.Equals(t, res.StatusCode, tc.statusCode)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
var ae acme.AError
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
prob := tc.problem.ToACME()
assert.Equals(t, ae.Type, prob.Type)
assert.Equals(t, ae.Detail, prob.Detail)
assert.Equals(t, ae.Identifier, prob.Identifier)
assert.Equals(t, ae.Subproblems, prob.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.Equals(t, res.Header["Replay-Nonce"], []string{"bar"})
assert.Equals(t, res.Header["Cache-Control"], []string{"no-store"})
assert.Equals(t, bytes.TrimSpace(body), testBody)
}
})
}
}
func TestHandlerAddDirLink(t *testing.T) {
prov := newProv()
provName := url.PathEscape(prov.GetName())
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
type test struct {
auth acme.Interface
link string
statusCode int
ctx context.Context
problem *acme.Error
}
var tests = map[string]func(t *testing.T) test{
"ok": func(t *testing.T) test {
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.BaseURLContextKey, baseURL)
return test{
auth: &mockAcmeAuthority{
getLink: func(ctx context.Context, typ acme.Link, abs bool, ins ...string) string {
assert.Equals(t, acme.BaseURLFromContext(ctx), baseURL)
return fmt.Sprintf("%s/acme/%s/directory", baseURL.String(), provName)
},
},
ctx: ctx,
link: fmt.Sprintf("%s/acme/%s/directory", baseURL.String(), provName),
statusCode: 200,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
h := New(tc.auth).(*Handler)
req := httptest.NewRequest("GET", "/foo", nil)
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
h.addDirLink(testNext)(w, req)
res := w.Result()
assert.Equals(t, res.StatusCode, tc.statusCode)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
var ae acme.AError
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
prob := tc.problem.ToACME()
assert.Equals(t, ae.Type, prob.Type)
assert.Equals(t, ae.Detail, prob.Detail)
assert.Equals(t, ae.Identifier, prob.Identifier)
assert.Equals(t, ae.Subproblems, prob.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.Equals(t, res.Header["Link"], []string{fmt.Sprintf("<%s>;rel=\"index\"", tc.link)})
assert.Equals(t, bytes.TrimSpace(body), testBody)
}
})
}
}
func TestHandlerVerifyContentType(t *testing.T) {
prov := newProv()
provName := prov.GetName()
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
url := fmt.Sprintf("%s/acme/%s/certificate/abc123", baseURL.String(), provName)
type test struct {
h Handler
ctx context.Context
contentType string
problem *acme.Error
statusCode int
url string
}
var tests = map[string]func(t *testing.T) test{
"fail/general-bad-content-type": func(t *testing.T) test {
return test{
h: Handler{
Auth: &mockAcmeAuthority{
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.CertificateLink)
assert.Equals(t, abs, false)
assert.Equals(t, in, []string{""})
return fmt.Sprintf("/acme/%s/certificate/", provName)
},
},
},
url: fmt.Sprintf("%s/acme/%s/new-account", baseURL.String(), provName),
ctx: context.WithValue(context.Background(), acme.ProvisionerContextKey, prov),
contentType: "foo",
statusCode: 400,
problem: acme.MalformedErr(errors.New("expected content-type to be in [application/jose+json], but got foo")),
}
},
"fail/certificate-bad-content-type": func(t *testing.T) test {
return test{
h: Handler{
Auth: &mockAcmeAuthority{
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.CertificateLink)
assert.Equals(t, abs, false)
assert.Equals(t, in, []string{""})
return "/certificate/"
},
},
},
ctx: context.WithValue(context.Background(), acme.ProvisionerContextKey, prov),
contentType: "foo",
statusCode: 400,
problem: acme.MalformedErr(errors.New("expected content-type to be in [application/jose+json application/pkix-cert application/pkcs7-mime], but got foo")),
}
},
"ok": func(t *testing.T) test {
return test{
h: Handler{
Auth: &mockAcmeAuthority{
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.CertificateLink)
assert.Equals(t, abs, false)
assert.Equals(t, in, []string{""})
return "/certificate/"
},
},
},
ctx: context.WithValue(context.Background(), acme.ProvisionerContextKey, prov),
contentType: "application/jose+json",
statusCode: 200,
}
},
"ok/certificate/pkix-cert": func(t *testing.T) test {
return test{
h: Handler{
Auth: &mockAcmeAuthority{
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.CertificateLink)
assert.Equals(t, abs, false)
assert.Equals(t, in, []string{""})
return "/certificate/"
},
},
},
ctx: context.WithValue(context.Background(), acme.ProvisionerContextKey, prov),
contentType: "application/pkix-cert",
statusCode: 200,
}
},
"ok/certificate/jose+json": func(t *testing.T) test {
return test{
h: Handler{
Auth: &mockAcmeAuthority{
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.CertificateLink)
assert.Equals(t, abs, false)
assert.Equals(t, in, []string{""})
return "/certificate/"
},
},
},
ctx: context.WithValue(context.Background(), acme.ProvisionerContextKey, prov),
contentType: "application/jose+json",
statusCode: 200,
}
},
"ok/certificate/pkcs7-mime": func(t *testing.T) test {
return test{
h: Handler{
Auth: &mockAcmeAuthority{
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.CertificateLink)
assert.Equals(t, abs, false)
assert.Equals(t, in, []string{""})
return "/certificate/"
},
},
},
ctx: context.WithValue(context.Background(), acme.ProvisionerContextKey, prov),
contentType: "application/pkcs7-mime",
statusCode: 200,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
_url := url
if tc.url != "" {
_url = tc.url
}
req := httptest.NewRequest("GET", _url, nil)
req = req.WithContext(tc.ctx)
req.Header.Add("Content-Type", tc.contentType)
w := httptest.NewRecorder()
tc.h.verifyContentType(testNext)(w, req)
res := w.Result()
assert.Equals(t, res.StatusCode, tc.statusCode)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
var ae acme.AError
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
prob := tc.problem.ToACME()
assert.Equals(t, ae.Type, prob.Type)
assert.Equals(t, ae.Detail, prob.Detail)
assert.Equals(t, ae.Identifier, prob.Identifier)
assert.Equals(t, ae.Subproblems, prob.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.Equals(t, bytes.TrimSpace(body), testBody)
}
})
}
}
func TestHandlerIsPostAsGet(t *testing.T) {
url := "https://ca.smallstep.com/acme/new-account"
type test struct {
ctx context.Context
problem *acme.Error
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/no-payload": func(t *testing.T) test {
return test{
ctx: context.Background(),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
}
},
"fail/nil-payload": func(t *testing.T) test {
return test{
ctx: context.WithValue(context.Background(), acme.PayloadContextKey, nil),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("payload expected in request context")),
}
},
"fail/not-post-as-get": func(t *testing.T) test {
return test{
ctx: context.WithValue(context.Background(), acme.PayloadContextKey, &payloadInfo{}),
statusCode: 400,
problem: acme.MalformedErr(errors.New("expected POST-as-GET")),
}
},
"ok": func(t *testing.T) test {
return test{
ctx: context.WithValue(context.Background(), acme.PayloadContextKey, &payloadInfo{isPostAsGet: true}),
statusCode: 200,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
h := New(nil).(*Handler)
req := httptest.NewRequest("GET", url, nil)
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
h.isPostAsGet(testNext)(w, req)
res := w.Result()
assert.Equals(t, res.StatusCode, tc.statusCode)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
var ae acme.AError
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
prob := tc.problem.ToACME()
assert.Equals(t, ae.Type, prob.Type)
assert.Equals(t, ae.Detail, prob.Detail)
assert.Equals(t, ae.Identifier, prob.Identifier)
assert.Equals(t, ae.Subproblems, prob.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.Equals(t, bytes.TrimSpace(body), testBody)
}
})
}
}
type errReader int
func (errReader) Read(p []byte) (n int, err error) {
return 0, errors.New("force")
}
func (errReader) Close() error {
return nil
}
func TestHandlerParseJWS(t *testing.T) {
url := "https://ca.smallstep.com/acme/new-account"
type test struct {
next nextHTTP
body io.Reader
problem *acme.Error
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/read-body-error": func(t *testing.T) test {
return test{
body: errReader(0),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("failed to read request body: force")),
}
},
"fail/parse-jws-error": func(t *testing.T) test {
return test{
body: strings.NewReader("foo"),
statusCode: 400,
problem: acme.MalformedErr(errors.New("failed to parse JWS from request body: square/go-jose: compact JWS format must have three parts")),
}
},
"ok": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
Key: jwk.Key,
}, new(jose.SignerOptions))
assert.FatalError(t, err)
signed, err := signer.Sign([]byte("baz"))
assert.FatalError(t, err)
expRaw, err := signed.CompactSerialize()
assert.FatalError(t, err)
return test{
body: strings.NewReader(expRaw),
next: func(w http.ResponseWriter, r *http.Request) {
jws, err := acme.JwsFromContext(r.Context())
assert.FatalError(t, err)
gotRaw, err := jws.CompactSerialize()
assert.FatalError(t, err)
assert.Equals(t, gotRaw, expRaw)
w.Write(testBody)
},
statusCode: 200,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
h := New(nil).(*Handler)
req := httptest.NewRequest("GET", url, tc.body)
w := httptest.NewRecorder()
h.parseJWS(tc.next)(w, req)
res := w.Result()
assert.Equals(t, res.StatusCode, tc.statusCode)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
var ae acme.AError
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
prob := tc.problem.ToACME()
assert.Equals(t, ae.Type, prob.Type)
assert.Equals(t, ae.Detail, prob.Detail)
assert.Equals(t, ae.Identifier, prob.Identifier)
assert.Equals(t, ae.Subproblems, prob.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.Equals(t, bytes.TrimSpace(body), testBody)
}
})
}
}
func TestHandlerVerifyAndExtractJWSPayload(t *testing.T) {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
_pub := jwk.Public()
pub := &_pub
so := new(jose.SignerOptions)
so.WithHeader("alg", jose.SignatureAlgorithm(jwk.Algorithm))
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
Key: jwk.Key,
}, so)
assert.FatalError(t, err)
jws, err := signer.Sign([]byte("baz"))
assert.FatalError(t, err)
raw, err := jws.CompactSerialize()
assert.FatalError(t, err)
parsedJWS, err := jose.ParseJWS(raw)
assert.FatalError(t, err)
url := "https://ca.smallstep.com/acme/account/1234"
type test struct {
ctx context.Context
next func(http.ResponseWriter, *http.Request)
problem *acme.Error
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/no-jws": func(t *testing.T) test {
return test{
ctx: context.Background(),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jws expected in request context")),
}
},
"fail/nil-jws": func(t *testing.T) test {
return test{
ctx: context.WithValue(context.Background(), acme.JwsContextKey, nil),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jws expected in request context")),
}
},
"fail/no-jwk": func(t *testing.T) test {
return test{
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jwk expected in request context")),
}
},
"fail/nil-jwk": func(t *testing.T) test {
ctx := context.WithValue(context.Background(), acme.JwsContextKey, parsedJWS)
return test{
ctx: context.WithValue(ctx, acme.JwkContextKey, nil),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jwk expected in request context")),
}
},
"fail/verify-jws-failure": func(t *testing.T) test {
_jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
_pub := _jwk.Public()
ctx := context.WithValue(context.Background(), acme.JwsContextKey, parsedJWS)
ctx = context.WithValue(ctx, acme.JwkContextKey, &_pub)
return test{
ctx: ctx,
statusCode: 400,
problem: acme.MalformedErr(errors.New("error verifying jws: square/go-jose: error in cryptographic primitive")),
}
},
"fail/algorithm-mismatch": func(t *testing.T) test {
_pub := *pub
clone := &_pub
clone.Algorithm = jose.HS256
ctx := context.WithValue(context.Background(), acme.JwsContextKey, parsedJWS)
ctx = context.WithValue(ctx, acme.JwkContextKey, clone)
return test{
ctx: ctx,
statusCode: 400,
problem: acme.MalformedErr(errors.New("verifier and signature algorithm do not match")),
}
},
"ok": func(t *testing.T) test {
ctx := context.WithValue(context.Background(), acme.JwsContextKey, parsedJWS)
ctx = context.WithValue(ctx, acme.JwkContextKey, pub)
return test{
ctx: ctx,
statusCode: 200,
next: func(w http.ResponseWriter, r *http.Request) {
p, err := payloadFromContext(r.Context())
assert.FatalError(t, err)
if assert.NotNil(t, p) {
assert.Equals(t, p.value, []byte("baz"))
assert.False(t, p.isPostAsGet)
assert.False(t, p.isEmptyJSON)
}
w.Write(testBody)
},
}
},
"ok/empty-algorithm-in-jwk": func(t *testing.T) test {
_pub := *pub
clone := &_pub
clone.Algorithm = ""
ctx := context.WithValue(context.Background(), acme.JwsContextKey, parsedJWS)
ctx = context.WithValue(ctx, acme.JwkContextKey, pub)
return test{
ctx: ctx,
statusCode: 200,
next: func(w http.ResponseWriter, r *http.Request) {
p, err := payloadFromContext(r.Context())
assert.FatalError(t, err)
if assert.NotNil(t, p) {
assert.Equals(t, p.value, []byte("baz"))
assert.False(t, p.isPostAsGet)
assert.False(t, p.isEmptyJSON)
}
w.Write(testBody)
},
}
},
"ok/post-as-get": func(t *testing.T) test {
_jws, err := signer.Sign([]byte(""))
assert.FatalError(t, err)
_raw, err := _jws.CompactSerialize()
assert.FatalError(t, err)
_parsed, err := jose.ParseJWS(_raw)
assert.FatalError(t, err)
ctx := context.WithValue(context.Background(), acme.JwsContextKey, _parsed)
ctx = context.WithValue(ctx, acme.JwkContextKey, pub)
return test{
ctx: ctx,
statusCode: 200,
next: func(w http.ResponseWriter, r *http.Request) {
p, err := payloadFromContext(r.Context())
assert.FatalError(t, err)
if assert.NotNil(t, p) {
assert.Equals(t, p.value, []byte{})
assert.True(t, p.isPostAsGet)
assert.False(t, p.isEmptyJSON)
}
w.Write(testBody)
},
}
},
"ok/empty-json": func(t *testing.T) test {
_jws, err := signer.Sign([]byte("{}"))
assert.FatalError(t, err)
_raw, err := _jws.CompactSerialize()
assert.FatalError(t, err)
_parsed, err := jose.ParseJWS(_raw)
assert.FatalError(t, err)
ctx := context.WithValue(context.Background(), acme.JwsContextKey, _parsed)
ctx = context.WithValue(ctx, acme.JwkContextKey, pub)
return test{
ctx: ctx,
statusCode: 200,
next: func(w http.ResponseWriter, r *http.Request) {
p, err := payloadFromContext(r.Context())
assert.FatalError(t, err)
if assert.NotNil(t, p) {
assert.Equals(t, p.value, []byte("{}"))
assert.False(t, p.isPostAsGet)
assert.True(t, p.isEmptyJSON)
}
w.Write(testBody)
},
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
h := New(nil).(*Handler)
req := httptest.NewRequest("GET", url, nil)
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
h.verifyAndExtractJWSPayload(tc.next)(w, req)
res := w.Result()
assert.Equals(t, res.StatusCode, tc.statusCode)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
var ae acme.AError
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
prob := tc.problem.ToACME()
assert.Equals(t, ae.Type, prob.Type)
assert.Equals(t, ae.Detail, prob.Detail)
assert.Equals(t, ae.Identifier, prob.Identifier)
assert.Equals(t, ae.Subproblems, prob.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.Equals(t, bytes.TrimSpace(body), testBody)
}
})
}
}
func TestHandlerLookupJWK(t *testing.T) {
prov := newProv()
provName := url.PathEscape(prov.GetName())
baseURL := &url.URL{Scheme: "https", Host: "test.ca.smallstep.com"}
url := fmt.Sprintf("%s/acme/%s/account/1234",
baseURL, provName)
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
accID := "account-id"
prefix := fmt.Sprintf("%s/acme/%s/account/",
baseURL, provName)
so := new(jose.SignerOptions)
so.WithHeader("kid", fmt.Sprintf("%s%s", prefix, accID))
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
Key: jwk.Key,
}, so)
assert.FatalError(t, err)
jws, err := signer.Sign([]byte("baz"))
assert.FatalError(t, err)
raw, err := jws.CompactSerialize()
assert.FatalError(t, err)
parsedJWS, err := jose.ParseJWS(raw)
assert.FatalError(t, err)
type test struct {
auth acme.Interface
ctx context.Context
next func(http.ResponseWriter, *http.Request)
problem *acme.Error
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/no-jws": func(t *testing.T) test {
return test{
ctx: context.WithValue(context.Background(), acme.ProvisionerContextKey, prov),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jws expected in request context")),
}
},
"fail/nil-jws": func(t *testing.T) test {
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, nil)
return test{
ctx: ctx,
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jws expected in request context")),
}
},
"fail/no-kid": func(t *testing.T) test {
_signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
Key: jwk.Key,
}, new(jose.SignerOptions))
assert.FatalError(t, err)
_jws, err := _signer.Sign([]byte("baz"))
assert.FatalError(t, err)
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, _jws)
ctx = context.WithValue(ctx, acme.BaseURLContextKey, baseURL)
return test{
auth: &mockAcmeAuthority{
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.AccountLink)
assert.True(t, abs)
assert.Equals(t, in, []string{""})
return prefix
},
},
ctx: ctx,
statusCode: 400,
problem: acme.MalformedErr(errors.Errorf("kid does not have required prefix; expected %s, but got ", prefix)),
}
},
"fail/bad-kid-prefix": func(t *testing.T) test {
_so := new(jose.SignerOptions)
_so.WithHeader("kid", "foo")
_signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
Key: jwk.Key,
}, _so)
assert.FatalError(t, err)
_jws, err := _signer.Sign([]byte("baz"))
assert.FatalError(t, err)
_raw, err := _jws.CompactSerialize()
assert.FatalError(t, err)
_parsed, err := jose.ParseJWS(_raw)
assert.FatalError(t, err)
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, _parsed)
ctx = context.WithValue(ctx, acme.BaseURLContextKey, baseURL)
return test{
auth: &mockAcmeAuthority{
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.AccountLink)
assert.True(t, abs)
assert.Equals(t, in, []string{""})
return fmt.Sprintf("%s/acme/%s/account/", baseURL.String(), provName)
},
},
ctx: ctx,
statusCode: 400,
problem: acme.MalformedErr(errors.Errorf("kid does not have required prefix; expected %s, but got foo", prefix)),
}
},
"fail/account-not-found": func(t *testing.T) test {
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, parsedJWS)
ctx = context.WithValue(ctx, acme.BaseURLContextKey, baseURL)
return test{
auth: &mockAcmeAuthority{
getAccount: func(ctx context.Context, _accID string) (*acme.Account, error) {
p, err := acme.ProvisionerFromContext(ctx)
assert.FatalError(t, err)
assert.Equals(t, p, prov)
assert.Equals(t, accID, accID)
return nil, database.ErrNotFound
},
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.AccountLink)
assert.True(t, abs)
assert.Equals(t, in, []string{""})
return fmt.Sprintf("%s/acme/%s/account/", baseURL.String(), provName)
},
},
ctx: ctx,
statusCode: 400,
problem: acme.AccountDoesNotExistErr(nil),
}
},
"fail/GetAccount-error": func(t *testing.T) test {
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, parsedJWS)
ctx = context.WithValue(ctx, acme.BaseURLContextKey, baseURL)
return test{
auth: &mockAcmeAuthority{
getAccount: func(ctx context.Context, _accID string) (*acme.Account, error) {
p, err := acme.ProvisionerFromContext(ctx)
assert.FatalError(t, err)
assert.Equals(t, p, prov)
assert.Equals(t, accID, accID)
return nil, acme.ServerInternalErr(errors.New("force"))
},
getLink: func(ctx context.Context, typ acme.Link, abs bool, in ...string) string {
assert.Equals(t, typ, acme.AccountLink)
assert.True(t, abs)
assert.Equals(t, in, []string{""})
return fmt.Sprintf("%s/acme/%s/account/", baseURL.String(), provName)
},
},
ctx: ctx,
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("force")),
}
},
"fail/account-not-valid": func(t *testing.T) test {
acc := &acme.Account{Status: "deactivated"}
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, parsedJWS)
ctx = context.WithValue(ctx, acme.BaseURLContextKey, baseURL)
return test{
auth: &mockAcmeAuthority{
getAccount: func(ctx context.Context, _accID string) (*acme.Account, error) {
p, err := acme.ProvisionerFromContext(ctx)
assert.FatalError(t, err)
assert.Equals(t, p, prov)
assert.Equals(t, accID, accID)
return acc, 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.Equals(t, in, []string{""})
return fmt.Sprintf("%s/acme/%s/account/", baseURL.String(), provName)
},
},
ctx: ctx,
statusCode: 401,
problem: acme.UnauthorizedErr(errors.New("account is not active")),
}
},
"ok": func(t *testing.T) test {
acc := &acme.Account{Status: "valid", Key: jwk}
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, parsedJWS)
ctx = context.WithValue(ctx, acme.BaseURLContextKey, baseURL)
return test{
auth: &mockAcmeAuthority{
getAccount: func(ctx context.Context, _accID string) (*acme.Account, error) {
p, err := acme.ProvisionerFromContext(ctx)
assert.FatalError(t, err)
assert.Equals(t, p, prov)
assert.Equals(t, accID, accID)
return acc, 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.Equals(t, in, []string{""})
return fmt.Sprintf("%s/acme/%s/account/", baseURL.String(), provName)
},
},
ctx: ctx,
next: func(w http.ResponseWriter, r *http.Request) {
_acc, err := acme.AccountFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _acc, acc)
_jwk, err := acme.JwkFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _jwk, jwk)
w.Write(testBody)
},
statusCode: 200,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
h := New(tc.auth).(*Handler)
req := httptest.NewRequest("GET", url, nil)
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
h.lookupJWK(tc.next)(w, req)
res := w.Result()
assert.Equals(t, res.StatusCode, tc.statusCode)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
var ae acme.AError
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
prob := tc.problem.ToACME()
assert.Equals(t, ae.Type, prob.Type)
assert.Equals(t, ae.Detail, prob.Detail)
assert.Equals(t, ae.Identifier, prob.Identifier)
assert.Equals(t, ae.Subproblems, prob.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.Equals(t, bytes.TrimSpace(body), testBody)
}
})
}
}
func TestHandlerExtractJWK(t *testing.T) {
prov := newProv()
provName := url.PathEscape(prov.GetName())
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
kid, err := jwk.Thumbprint(crypto.SHA256)
assert.FatalError(t, err)
pub := jwk.Public()
pub.KeyID = base64.RawURLEncoding.EncodeToString(kid)
so := new(jose.SignerOptions)
so.WithHeader("jwk", pub)
signer, err := jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(jwk.Algorithm),
Key: jwk.Key,
}, so)
assert.FatalError(t, err)
jws, err := signer.Sign([]byte("baz"))
assert.FatalError(t, err)
raw, err := jws.CompactSerialize()
assert.FatalError(t, err)
parsedJWS, err := jose.ParseJWS(raw)
assert.FatalError(t, err)
url := fmt.Sprintf("https://ca.smallstep.com/acme/%s/account/1234",
provName)
type test struct {
auth acme.Interface
ctx context.Context
next func(http.ResponseWriter, *http.Request)
problem *acme.Error
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/no-jws": func(t *testing.T) test {
return test{
ctx: context.WithValue(context.Background(), acme.ProvisionerContextKey, prov),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jws expected in request context")),
}
},
"fail/nil-jws": func(t *testing.T) test {
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, nil)
return test{
ctx: ctx,
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jws expected in request context")),
}
},
"fail/nil-jwk": func(t *testing.T) test {
_jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
JSONWebKey: nil,
},
},
},
}
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, _jws)
return test{
ctx: ctx,
statusCode: 400,
problem: acme.MalformedErr(errors.New("jwk expected in protected header")),
}
},
"fail/invalid-jwk": func(t *testing.T) test {
_jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
JSONWebKey: &jose.JSONWebKey{Key: "foo"},
},
},
},
}
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, _jws)
return test{
ctx: ctx,
statusCode: 400,
problem: acme.MalformedErr(errors.New("invalid jwk in protected header")),
}
},
"fail/GetAccountByKey-error": func(t *testing.T) test {
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, parsedJWS)
return test{
ctx: ctx,
auth: &mockAcmeAuthority{
getAccountByKey: func(ctx context.Context, jwk *jose.JSONWebKey) (*acme.Account, error) {
p, err := acme.ProvisionerFromContext(ctx)
assert.FatalError(t, err)
assert.Equals(t, p, prov)
assert.Equals(t, jwk.KeyID, pub.KeyID)
return nil, acme.ServerInternalErr(errors.New("force"))
},
},
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("force")),
}
},
"fail/account-not-valid": func(t *testing.T) test {
acc := &acme.Account{Status: "deactivated"}
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, parsedJWS)
return test{
ctx: ctx,
auth: &mockAcmeAuthority{
getAccountByKey: func(ctx context.Context, jwk *jose.JSONWebKey) (*acme.Account, error) {
p, err := acme.ProvisionerFromContext(ctx)
assert.FatalError(t, err)
assert.Equals(t, p, prov)
assert.Equals(t, jwk.KeyID, pub.KeyID)
return acc, nil
},
},
statusCode: 401,
problem: acme.UnauthorizedErr(errors.New("account is not active")),
}
},
"ok": func(t *testing.T) test {
acc := &acme.Account{Status: "valid"}
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, parsedJWS)
return test{
ctx: ctx,
auth: &mockAcmeAuthority{
getAccountByKey: func(ctx context.Context, jwk *jose.JSONWebKey) (*acme.Account, error) {
p, err := acme.ProvisionerFromContext(ctx)
assert.FatalError(t, err)
assert.Equals(t, p, prov)
assert.Equals(t, jwk.KeyID, pub.KeyID)
return acc, nil
},
},
next: func(w http.ResponseWriter, r *http.Request) {
_acc, err := acme.AccountFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _acc, acc)
_jwk, err := acme.JwkFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _jwk.KeyID, pub.KeyID)
w.Write(testBody)
},
statusCode: 200,
}
},
"ok/no-account": func(t *testing.T) test {
ctx := context.WithValue(context.Background(), acme.ProvisionerContextKey, prov)
ctx = context.WithValue(ctx, acme.JwsContextKey, parsedJWS)
return test{
ctx: ctx,
auth: &mockAcmeAuthority{
getAccountByKey: func(ctx context.Context, jwk *jose.JSONWebKey) (*acme.Account, error) {
p, err := acme.ProvisionerFromContext(ctx)
assert.FatalError(t, err)
assert.Equals(t, p, prov)
assert.Equals(t, jwk.KeyID, pub.KeyID)
return nil, database.ErrNotFound
},
},
next: func(w http.ResponseWriter, r *http.Request) {
_acc, err := acme.AccountFromContext(r.Context())
assert.NotNil(t, err)
assert.Nil(t, _acc)
_jwk, err := acme.JwkFromContext(r.Context())
assert.FatalError(t, err)
assert.Equals(t, _jwk.KeyID, pub.KeyID)
w.Write(testBody)
},
statusCode: 200,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
h := New(tc.auth).(*Handler)
req := httptest.NewRequest("GET", url, nil)
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
h.extractJWK(tc.next)(w, req)
res := w.Result()
assert.Equals(t, res.StatusCode, tc.statusCode)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
var ae acme.AError
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
prob := tc.problem.ToACME()
assert.Equals(t, ae.Type, prob.Type)
assert.Equals(t, ae.Detail, prob.Detail)
assert.Equals(t, ae.Identifier, prob.Identifier)
assert.Equals(t, ae.Subproblems, prob.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.Equals(t, bytes.TrimSpace(body), testBody)
}
})
}
}
func TestHandlerValidateJWS(t *testing.T) {
url := "https://ca.smallstep.com/acme/account/1234"
type test struct {
auth acme.Interface
ctx context.Context
next func(http.ResponseWriter, *http.Request)
problem *acme.Error
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/no-jws": func(t *testing.T) test {
return test{
ctx: context.Background(),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jws expected in request context")),
}
},
"fail/nil-jws": func(t *testing.T) test {
return test{
ctx: context.WithValue(context.Background(), acme.JwsContextKey, nil),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("jws expected in request context")),
}
},
"fail/no-signature": func(t *testing.T) test {
return test{
ctx: context.WithValue(context.Background(), acme.JwsContextKey, &jose.JSONWebSignature{}),
statusCode: 400,
problem: acme.MalformedErr(errors.New("request body does not contain a signature")),
}
},
"fail/more-than-one-signature": func(t *testing.T) test {
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{},
{},
},
}
return test{
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.New("request body contains more than one signature")),
}
},
"fail/unprotected-header-not-empty": func(t *testing.T) test {
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{Unprotected: jose.Header{Nonce: "abc"}},
},
}
return test{
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.New("unprotected header must not be used")),
}
},
"fail/unsuitable-algorithm-none": func(t *testing.T) test {
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{Protected: jose.Header{Algorithm: "none"}},
},
}
return test{
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.New("unsuitable algorithm: none")),
}
},
"fail/unsuitable-algorithm-mac": func(t *testing.T) test {
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{Protected: jose.Header{Algorithm: jose.HS256}},
},
}
return test{
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.Errorf("unsuitable algorithm: %s", jose.HS256)),
}
},
"fail/rsa-key-&-alg-mismatch": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
pub := jwk.Public()
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
Algorithm: jose.RS256,
JSONWebKey: &pub,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
},
},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return nil
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.Errorf("jws key type and algorithm do not match")),
}
},
"fail/rsa-key-too-small": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("RSA", "", "", "sig", "", 1024)
assert.FatalError(t, err)
pub := jwk.Public()
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
Algorithm: jose.RS256,
JSONWebKey: &pub,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
},
},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return nil
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.Errorf("rsa keys must be at least 2048 bits (256 bytes) in size")),
}
},
"fail/UseNonce-error": func(t *testing.T) test {
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{Protected: jose.Header{Algorithm: jose.ES256}},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return acme.ServerInternalErr(errors.New("force"))
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 500,
problem: acme.ServerInternalErr(errors.New("force")),
}
},
"fail/no-url-header": func(t *testing.T) test {
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{Protected: jose.Header{Algorithm: jose.ES256}},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return nil
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.New("jws missing url protected header")),
}
},
"fail/url-mismatch": func(t *testing.T) test {
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
Algorithm: jose.ES256,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": "foo",
},
},
},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return nil
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.Errorf("url header in JWS (foo) does not match request url (%s)", url)),
}
},
"fail/both-jwk-kid": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
pub := jwk.Public()
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
Algorithm: jose.ES256,
KeyID: "bar",
JSONWebKey: &pub,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
},
},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return nil
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.Errorf("jwk and kid are mutually exclusive")),
}
},
"fail/no-jwk-kid": func(t *testing.T) test {
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
Algorithm: jose.ES256,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
},
},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return nil
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
statusCode: 400,
problem: acme.MalformedErr(errors.Errorf("either jwk or kid must be defined in jws protected header")),
}
},
"ok/kid": func(t *testing.T) test {
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
Algorithm: jose.ES256,
KeyID: "bar",
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
},
},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return nil
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(testBody)
},
statusCode: 200,
}
},
"ok/jwk/ecdsa": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
assert.FatalError(t, err)
pub := jwk.Public()
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
Algorithm: jose.ES256,
JSONWebKey: &pub,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
},
},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return nil
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(testBody)
},
statusCode: 200,
}
},
"ok/jwk/rsa": func(t *testing.T) test {
jwk, err := jose.GenerateJWK("RSA", "", "", "sig", "", 2048)
assert.FatalError(t, err)
pub := jwk.Public()
jws := &jose.JSONWebSignature{
Signatures: []jose.Signature{
{
Protected: jose.Header{
Algorithm: jose.RS256,
JSONWebKey: &pub,
ExtraHeaders: map[jose.HeaderKey]interface{}{
"url": url,
},
},
},
},
}
return test{
auth: &mockAcmeAuthority{
useNonce: func(n string) error {
return nil
},
},
ctx: context.WithValue(context.Background(), acme.JwsContextKey, jws),
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(testBody)
},
statusCode: 200,
}
},
}
for name, run := range tests {
tc := run(t)
t.Run(name, func(t *testing.T) {
h := New(tc.auth).(*Handler)
req := httptest.NewRequest("GET", url, nil)
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
h.validateJWS(tc.next)(w, req)
res := w.Result()
assert.Equals(t, res.StatusCode, tc.statusCode)
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 && assert.NotNil(t, tc.problem) {
var ae acme.AError
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &ae))
prob := tc.problem.ToACME()
assert.Equals(t, ae.Type, prob.Type)
assert.Equals(t, ae.Detail, prob.Detail)
assert.Equals(t, ae.Identifier, prob.Identifier)
assert.Equals(t, ae.Subproblems, prob.Subproblems)
assert.Equals(t, res.Header["Content-Type"], []string{"application/problem+json"})
} else {
assert.Equals(t, bytes.TrimSpace(body), testBody)
}
})
}
}