2019-05-27 00:41:10 +00:00
package api
import (
"bytes"
"context"
"crypto"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
2020-05-07 03:18:12 +00:00
"net/url"
2019-05-27 00:41:10 +00:00
"strings"
"testing"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/acme"
"github.com/smallstep/nosql/database"
2020-08-24 21:44:11 +00:00
"go.step.sm/crypto/jose"
2019-05-27 00:41:10 +00:00
)
var testBody = [ ] byte ( "foo" )
func testNext ( w http . ResponseWriter , r * http . Request ) {
w . Write ( testBody )
}
2020-05-07 03:18:12 +00:00
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 ( ) )
}
} )
}
}
2021-03-11 07:05:46 +00:00
func TestHandler_baseURLFromRequest ( t * testing . T ) {
2021-03-09 06:35:57 +00:00
h := & Handler { }
2020-05-07 03:18:12 +00:00
req := httptest . NewRequest ( "GET" , "/foo" , nil )
req . Host = "test.ca.smallstep.com:8080"
w := httptest . NewRecorder ( )
next := func ( w http . ResponseWriter , r * http . Request ) {
2021-03-09 06:35:57 +00:00
bu := baseURLFromContext ( r . Context ( ) )
2020-05-07 03:18:12 +00:00
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 ) {
2021-03-09 06:35:57 +00:00
assert . Equals ( t , baseURLFromContext ( r . Context ( ) ) , nil )
2020-05-07 03:18:12 +00:00
}
h . baseURLFromRequest ( next ) ( w , req )
}
2021-03-11 07:05:46 +00:00
func TestHandler_addNonce ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
url := "https://ca.smallstep.com/acme/new-nonce"
type test struct {
2021-03-09 06:35:57 +00:00
db acme . DB
err * acme . Error
2019-05-27 00:41:10 +00:00
statusCode int
}
var tests = map [ string ] func ( t * testing . T ) test {
"fail/AddNonce-error" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
MockCreateNonce : func ( ctx context . Context ) ( acme . Nonce , error ) {
return acme . Nonce ( "" ) , acme . NewErrorISE ( "force" )
2019-05-27 00:41:10 +00:00
} ,
} ,
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "force" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"ok" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
MockCreateNonce : func ( ctx context . Context ) ( acme . Nonce , error ) {
2019-05-27 00:41:10 +00:00
return "bar" , nil
} ,
} ,
statusCode : 200 ,
}
} ,
}
for name , run := range tests {
tc := run ( t )
t . Run ( name , func ( t * testing . T ) {
2021-03-09 06:35:57 +00:00
h := & Handler { db : tc . db }
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
if res . StatusCode >= 400 && assert . NotNil ( t , tc . err ) {
var ae acme . Error
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & ae ) )
2021-03-09 06:35:57 +00:00
assert . Equals ( t , ae . Type , tc . err . Type )
assert . Equals ( t , ae . Detail , tc . err . Detail )
assert . Equals ( t , ae . Identifier , tc . err . Identifier )
assert . Equals ( t , ae . Subproblems , tc . err . Subproblems )
2019-05-27 00:41:10 +00:00
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 )
}
} )
}
}
2021-03-10 18:50:51 +00:00
func TestHandler_addDirLink ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
prov := newProv ( )
2020-05-07 03:18:12 +00:00
provName := url . PathEscape ( prov . GetName ( ) )
baseURL := & url . URL { Scheme : "https" , Host : "test.ca.smallstep.com" }
2019-05-27 00:41:10 +00:00
type test struct {
link string
2021-03-10 18:50:51 +00:00
linker Linker
2019-05-27 00:41:10 +00:00
statusCode int
ctx context . Context
2021-03-09 06:35:57 +00:00
err * acme . Error
2019-05-27 00:41:10 +00:00
}
var tests = map [ string ] func ( t * testing . T ) test {
"ok" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
2019-05-27 00:41:10 +00:00
return test {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2020-05-07 03:18:12 +00:00
ctx : ctx ,
link : fmt . Sprintf ( "%s/acme/%s/directory" , baseURL . String ( ) , provName ) ,
2019-05-27 00:41:10 +00:00
statusCode : 200 ,
}
} ,
}
for name , run := range tests {
tc := run ( t )
t . Run ( name , func ( t * testing . T ) {
2021-03-10 18:50:51 +00:00
h := & Handler { linker : tc . linker }
2020-05-07 03:18:12 +00:00
req := httptest . NewRequest ( "GET" , "/foo" , nil )
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
if res . StatusCode >= 400 && assert . NotNil ( t , tc . err ) {
var ae acme . Error
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & ae ) )
2021-03-09 06:35:57 +00:00
assert . Equals ( t , ae . Type , tc . err . Type )
assert . Equals ( t , ae . Detail , tc . err . Detail )
assert . Equals ( t , ae . Identifier , tc . err . Identifier )
assert . Equals ( t , ae . Subproblems , tc . err . Subproblems )
2019-05-27 00:41:10 +00:00
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 )
}
} )
}
}
2021-03-11 07:05:46 +00:00
func TestHandler_verifyContentType ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
prov := newProv ( )
2020-05-07 03:18:12 +00:00
provName := prov . GetName ( )
baseURL := & url . URL { Scheme : "https" , Host : "test.ca.smallstep.com" }
url := fmt . Sprintf ( "%s/acme/%s/certificate/abc123" , baseURL . String ( ) , provName )
2019-05-27 00:41:10 +00:00
type test struct {
h Handler
ctx context . Context
contentType string
2021-03-09 06:35:57 +00:00
err * acme . Error
2019-05-27 00:41:10 +00:00
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 {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2019-05-27 00:41:10 +00:00
} ,
2020-05-07 03:18:12 +00:00
url : fmt . Sprintf ( "%s/acme/%s/new-account" , baseURL . String ( ) , provName ) ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , prov ) ,
2019-05-27 00:41:10 +00:00
contentType : "foo" ,
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "expected content-type to be in [application/jose+json], but got foo" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/certificate-bad-content-type" : func ( t * testing . T ) test {
return test {
h : Handler {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2019-05-27 00:41:10 +00:00
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , prov ) ,
2019-05-27 00:41:10 +00:00
contentType : "foo" ,
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "expected content-type to be in [application/jose+json application/pkix-cert application/pkcs7-mime], but got foo" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"ok" : func ( t * testing . T ) test {
return test {
h : Handler {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2019-05-27 00:41:10 +00:00
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , prov ) ,
2019-05-27 00:41:10 +00:00
contentType : "application/jose+json" ,
statusCode : 200 ,
}
} ,
"ok/certificate/pkix-cert" : func ( t * testing . T ) test {
return test {
h : Handler {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2019-05-27 00:41:10 +00:00
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , prov ) ,
2019-05-27 00:41:10 +00:00
contentType : "application/pkix-cert" ,
statusCode : 200 ,
}
} ,
"ok/certificate/jose+json" : func ( t * testing . T ) test {
return test {
h : Handler {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2019-05-27 00:41:10 +00:00
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , prov ) ,
2019-05-27 00:41:10 +00:00
contentType : "application/jose+json" ,
statusCode : 200 ,
}
} ,
"ok/certificate/pkcs7-mime" : func ( t * testing . T ) test {
return test {
h : Handler {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2019-05-27 00:41:10 +00:00
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , prov ) ,
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
if res . StatusCode >= 400 && assert . NotNil ( t , tc . err ) {
var ae acme . Error
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & ae ) )
2021-03-09 06:35:57 +00:00
assert . Equals ( t , ae . Type , tc . err . Type )
assert . Equals ( t , ae . Detail , tc . err . Detail )
assert . Equals ( t , ae . Identifier , tc . err . Identifier )
assert . Equals ( t , ae . Subproblems , tc . err . Subproblems )
2019-05-27 00:41:10 +00:00
assert . Equals ( t , res . Header [ "Content-Type" ] , [ ] string { "application/problem+json" } )
} else {
assert . Equals ( t , bytes . TrimSpace ( body ) , testBody )
}
} )
}
}
2021-03-11 07:05:46 +00:00
func TestHandler_isPostAsGet ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
url := "https://ca.smallstep.com/acme/new-account"
type test struct {
ctx context . Context
2021-03-09 06:35:57 +00:00
err * acme . Error
2019-05-27 00:41:10 +00:00
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 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "payload expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-payload" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , payloadContextKey , nil ) ,
2019-05-27 00:41:10 +00:00
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "payload expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/not-post-as-get" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { } ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "expected POST-as-GET" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"ok" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { isPostAsGet : true } ) ,
2019-05-27 00:41:10 +00:00
statusCode : 200 ,
}
} ,
}
for name , run := range tests {
tc := run ( t )
t . Run ( name , func ( t * testing . T ) {
2021-03-09 06:35:57 +00:00
h := & Handler { }
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
if res . StatusCode >= 400 && assert . NotNil ( t , tc . err ) {
var ae acme . Error
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & ae ) )
2021-03-09 06:35:57 +00:00
assert . Equals ( t , ae . Type , tc . err . Type )
assert . Equals ( t , ae . Detail , tc . err . Detail )
assert . Equals ( t , ae . Identifier , tc . err . Identifier )
assert . Equals ( t , ae . Subproblems , tc . err . Subproblems )
2019-05-27 00:41:10 +00:00
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
}
2021-03-11 07:05:46 +00:00
func TestHandler_parseJWS ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
url := "https://ca.smallstep.com/acme/new-account"
type test struct {
next nextHTTP
body io . Reader
2021-03-09 06:35:57 +00:00
err * acme . Error
2019-05-27 00:41:10 +00:00
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 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "failed to read request body: force" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/parse-jws-error" : func ( t * testing . T ) test {
return test {
body : strings . NewReader ( "foo" ) ,
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "failed to parse JWS from request body: square/go-jose: compact JWS format must have three parts" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 ) {
2021-03-09 06:35:57 +00:00
jws , err := jwsFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
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 ) {
2021-03-09 06:35:57 +00:00
h := & Handler { }
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
if res . StatusCode >= 400 && assert . NotNil ( t , tc . err ) {
var ae acme . Error
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & ae ) )
2021-03-09 06:35:57 +00:00
assert . Equals ( t , ae . Type , tc . err . Type )
assert . Equals ( t , ae . Detail , tc . err . Detail )
assert . Equals ( t , ae . Identifier , tc . err . Identifier )
assert . Equals ( t , ae . Subproblems , tc . err . Subproblems )
2019-05-27 00:41:10 +00:00
assert . Equals ( t , res . Header [ "Content-Type" ] , [ ] string { "application/problem+json" } )
} else {
assert . Equals ( t , bytes . TrimSpace ( body ) , testBody )
}
} )
}
}
2021-03-11 07:05:46 +00:00
func TestHandler_verifyAndExtractJWSPayload ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
err * acme . Error
2019-05-27 00:41:10 +00:00
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 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jws expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-jws" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , nil ) ,
2019-05-27 00:41:10 +00:00
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jws expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/no-jwk" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jwk expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-jwk" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , jwsContextKey , parsedJWS )
2019-05-27 00:41:10 +00:00
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( ctx , jwsContextKey , nil ) ,
2019-05-27 00:41:10 +00:00
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jwk expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 ( )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , jwsContextKey , parsedJWS )
2021-03-11 07:05:46 +00:00
ctx = context . WithValue ( ctx , jwkContextKey , & _pub )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "error verifying jws: square/go-jose: error in cryptographic primitive" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/algorithm-mismatch" : func ( t * testing . T ) test {
_pub := * pub
clone := & _pub
clone . Algorithm = jose . HS256
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , jwsContextKey , parsedJWS )
2021-03-11 07:05:46 +00:00
ctx = context . WithValue ( ctx , jwkContextKey , clone )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "verifier and signature algorithm do not match" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"ok" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , jwsContextKey , parsedJWS )
2021-03-11 07:05:46 +00:00
ctx = context . WithValue ( ctx , jwkContextKey , pub )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 200 ,
next : func ( w http . ResponseWriter , r * http . Request ) {
2020-05-07 03:18:12 +00:00
p , err := payloadFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
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 = ""
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , jwsContextKey , parsedJWS )
2021-03-11 07:05:46 +00:00
ctx = context . WithValue ( ctx , jwkContextKey , pub )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 200 ,
next : func ( w http . ResponseWriter , r * http . Request ) {
2020-05-07 03:18:12 +00:00
p , err := payloadFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , jwsContextKey , _parsed )
2021-03-11 07:05:46 +00:00
ctx = context . WithValue ( ctx , jwkContextKey , pub )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 200 ,
next : func ( w http . ResponseWriter , r * http . Request ) {
2020-05-07 03:18:12 +00:00
p , err := payloadFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , jwsContextKey , _parsed )
2021-03-11 07:05:46 +00:00
ctx = context . WithValue ( ctx , jwkContextKey , pub )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 200 ,
next : func ( w http . ResponseWriter , r * http . Request ) {
2020-05-07 03:18:12 +00:00
p , err := payloadFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
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 ) {
2021-03-09 06:35:57 +00:00
h := & Handler { }
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
if res . StatusCode >= 400 && assert . NotNil ( t , tc . err ) {
var ae acme . Error
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & ae ) )
2021-03-09 06:35:57 +00:00
assert . Equals ( t , ae . Type , tc . err . Type )
assert . Equals ( t , ae . Detail , tc . err . Detail )
assert . Equals ( t , ae . Identifier , tc . err . Identifier )
assert . Equals ( t , ae . Subproblems , tc . err . Subproblems )
2019-05-27 00:41:10 +00:00
assert . Equals ( t , res . Header [ "Content-Type" ] , [ ] string { "application/problem+json" } )
} else {
assert . Equals ( t , bytes . TrimSpace ( body ) , testBody )
}
} )
}
}
2021-03-11 07:05:46 +00:00
func TestHandler_lookupJWK ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
prov := newProv ( )
2020-05-07 03:18:12 +00:00
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 )
2019-05-27 00:41:10 +00:00
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
accID := "account-id"
2020-05-07 03:18:12 +00:00
prefix := fmt . Sprintf ( "%s/acme/%s/account/" ,
baseURL , provName )
2019-05-27 00:41:10 +00:00
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 {
2021-03-10 18:50:51 +00:00
linker Linker
2021-03-09 06:35:57 +00:00
db acme . DB
2019-05-27 00:41:10 +00:00
ctx context . Context
next func ( http . ResponseWriter , * http . Request )
2021-03-09 06:35:57 +00:00
err * acme . Error
2019-05-27 00:41:10 +00:00
statusCode int
}
var tests = map [ string ] func ( t * testing . T ) test {
"fail/no-jws" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , prov ) ,
2019-05-27 00:41:10 +00:00
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jws expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-jws" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , nil )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jws expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , _jws )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
2019-05-27 00:41:10 +00:00
return test {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2019-05-27 00:41:10 +00:00
ctx : ctx ,
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "kid does not have required prefix; expected %s, but got " , prefix ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , _parsed )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
2019-05-27 00:41:10 +00:00
return test {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2019-05-27 00:41:10 +00:00
ctx : ctx ,
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "kid does not have required prefix; expected %s, but got foo" , prefix ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/account-not-found" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , parsedJWS )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
2019-05-27 00:41:10 +00:00
return test {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockGetAccount : func ( ctx context . Context , accID string ) ( * acme . Account , error ) {
2019-05-27 00:41:10 +00:00
assert . Equals ( t , accID , accID )
return nil , database . ErrNotFound
} ,
} ,
ctx : ctx ,
2020-02-02 01:35:41 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorAccountDoesNotExistType , "account does not exist" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/GetAccount-error" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , parsedJWS )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
2019-05-27 00:41:10 +00:00
return test {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockGetAccount : func ( ctx context . Context , id string ) ( * acme . Account , error ) {
assert . Equals ( t , id , accID )
2021-03-09 06:35:57 +00:00
return nil , acme . NewErrorISE ( "force" )
2019-05-27 00:41:10 +00:00
} ,
} ,
ctx : ctx ,
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "force" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/account-not-valid" : func ( t * testing . T ) test {
acc := & acme . Account { Status : "deactivated" }
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , parsedJWS )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
2019-05-27 00:41:10 +00:00
return test {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockGetAccount : func ( ctx context . Context , id string ) ( * acme . Account , error ) {
assert . Equals ( t , id , accID )
2019-05-27 00:41:10 +00:00
return acc , nil
} ,
} ,
ctx : ctx ,
statusCode : 401 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorUnauthorizedType , "account is not active" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"ok" : func ( t * testing . T ) test {
acc := & acme . Account { Status : "valid" , Key : jwk }
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , parsedJWS )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
2019-05-27 00:41:10 +00:00
return test {
2021-03-10 18:50:51 +00:00
linker : NewLinker ( "dns" , "acme" ) ,
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockGetAccount : func ( ctx context . Context , id string ) ( * acme . Account , error ) {
assert . Equals ( t , id , accID )
2019-05-27 00:41:10 +00:00
return acc , nil
} ,
} ,
ctx : ctx ,
next : func ( w http . ResponseWriter , r * http . Request ) {
2021-03-10 18:50:51 +00:00
_acc , err := accountFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , err )
assert . Equals ( t , _acc , acc )
2021-03-10 18:50:51 +00:00
_jwk , err := jwkFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
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 ) {
2021-03-10 18:50:51 +00:00
h := & Handler { db : tc . db , linker : tc . linker }
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
if res . StatusCode >= 400 && assert . NotNil ( t , tc . err ) {
2021-03-10 18:50:51 +00:00
var ae acme . Error
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & ae ) )
2021-03-09 06:35:57 +00:00
assert . Equals ( t , ae . Type , tc . err . Type )
assert . Equals ( t , ae . Detail , tc . err . Detail )
assert . Equals ( t , ae . Identifier , tc . err . Identifier )
assert . Equals ( t , ae . Subproblems , tc . err . Subproblems )
2019-05-27 00:41:10 +00:00
assert . Equals ( t , res . Header [ "Content-Type" ] , [ ] string { "application/problem+json" } )
} else {
assert . Equals ( t , bytes . TrimSpace ( body ) , testBody )
}
} )
}
}
2021-03-11 07:05:46 +00:00
func TestHandler_extractJWK ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
prov := newProv ( )
2020-05-07 03:18:12 +00:00
provName := url . PathEscape ( prov . GetName ( ) )
2019-05-27 00:41:10 +00:00
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" ,
2020-05-07 03:18:12 +00:00
provName )
2019-05-27 00:41:10 +00:00
type test struct {
2021-03-09 06:35:57 +00:00
db acme . DB
2019-05-27 00:41:10 +00:00
ctx context . Context
next func ( http . ResponseWriter , * http . Request )
2021-03-09 06:35:57 +00:00
err * acme . Error
2019-05-27 00:41:10 +00:00
statusCode int
}
var tests = map [ string ] func ( t * testing . T ) test {
"fail/no-jws" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , prov ) ,
2019-05-27 00:41:10 +00:00
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jws expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-jws" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , nil )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jws expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-jwk" : func ( t * testing . T ) test {
_jws := & jose . JSONWebSignature {
Signatures : [ ] jose . Signature {
{
Protected : jose . Header {
JSONWebKey : nil ,
} ,
} ,
} ,
}
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , _jws )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "jwk expected in protected header" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/invalid-jwk" : func ( t * testing . T ) test {
_jws := & jose . JSONWebSignature {
Signatures : [ ] jose . Signature {
{
Protected : jose . Header {
JSONWebKey : & jose . JSONWebKey { Key : "foo" } ,
} ,
} ,
} ,
}
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , _jws )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "invalid jwk in protected header" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/GetAccountByKey-error" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , parsedJWS )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockGetAccountByKeyID : func ( ctx context . Context , kid string ) ( * acme . Account , error ) {
assert . Equals ( t , kid , pub . KeyID )
2021-03-09 06:35:57 +00:00
return nil , acme . NewErrorISE ( "force" )
2019-05-27 00:41:10 +00:00
} ,
} ,
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "force" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/account-not-valid" : func ( t * testing . T ) test {
acc := & acme . Account { Status : "deactivated" }
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , parsedJWS )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockGetAccountByKeyID : func ( ctx context . Context , kid string ) ( * acme . Account , error ) {
assert . Equals ( t , kid , pub . KeyID )
2019-05-27 00:41:10 +00:00
return acc , nil
} ,
} ,
statusCode : 401 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorUnauthorizedType , "account is not active" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"ok" : func ( t * testing . T ) test {
acc := & acme . Account { Status : "valid" }
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , parsedJWS )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockGetAccountByKeyID : func ( ctx context . Context , kid string ) ( * acme . Account , error ) {
assert . Equals ( t , kid , pub . KeyID )
2019-05-27 00:41:10 +00:00
return acc , nil
} ,
} ,
next : func ( w http . ResponseWriter , r * http . Request ) {
2021-03-10 18:50:51 +00:00
_acc , err := accountFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , err )
assert . Equals ( t , _acc , acc )
2021-03-10 18:50:51 +00:00
_jwk , err := jwkFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , err )
assert . Equals ( t , _jwk . KeyID , pub . KeyID )
w . Write ( testBody )
} ,
statusCode : 200 ,
}
} ,
"ok/no-account" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , jwsContextKey , parsedJWS )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockGetAccountByKeyID : func ( ctx context . Context , kid string ) ( * acme . Account , error ) {
assert . Equals ( t , kid , pub . KeyID )
2019-05-27 00:41:10 +00:00
return nil , database . ErrNotFound
} ,
} ,
next : func ( w http . ResponseWriter , r * http . Request ) {
2021-03-10 18:50:51 +00:00
_acc , err := accountFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
assert . NotNil ( t , err )
assert . Nil ( t , _acc )
2021-03-10 18:50:51 +00:00
_jwk , err := jwkFromContext ( r . Context ( ) )
2019-05-27 00:41:10 +00:00
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 ) {
2021-03-10 18:50:51 +00:00
h := & Handler { db : tc . db }
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
if res . StatusCode >= 400 && assert . NotNil ( t , tc . err ) {
2021-03-10 18:50:51 +00:00
var ae acme . Error
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & ae ) )
2021-03-09 06:35:57 +00:00
assert . Equals ( t , ae . Type , tc . err . Type )
assert . Equals ( t , ae . Detail , tc . err . Detail )
assert . Equals ( t , ae . Identifier , tc . err . Identifier )
assert . Equals ( t , ae . Subproblems , tc . err . Subproblems )
2019-05-27 00:41:10 +00:00
assert . Equals ( t , res . Header [ "Content-Type" ] , [ ] string { "application/problem+json" } )
} else {
assert . Equals ( t , bytes . TrimSpace ( body ) , testBody )
}
} )
}
}
2021-03-11 07:05:46 +00:00
func TestHandler_validateJWS ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
url := "https://ca.smallstep.com/acme/account/1234"
type test struct {
2021-03-09 06:35:57 +00:00
db acme . DB
2019-05-27 00:41:10 +00:00
ctx context . Context
next func ( http . ResponseWriter , * http . Request )
2021-03-09 06:35:57 +00:00
err * acme . Error
2019-05-27 00:41:10 +00:00
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 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jws expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-jws" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , nil ) ,
2019-05-27 00:41:10 +00:00
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "jws expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/no-signature" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , & jose . JSONWebSignature { } ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "request body does not contain a signature" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/more-than-one-signature" : func ( t * testing . T ) test {
jws := & jose . JSONWebSignature {
Signatures : [ ] jose . Signature {
{ } ,
{ } ,
} ,
}
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "request body contains more than one signature" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/unprotected-header-not-empty" : func ( t * testing . T ) test {
jws := & jose . JSONWebSignature {
Signatures : [ ] jose . Signature {
{ Unprotected : jose . Header { Nonce : "abc" } } ,
} ,
}
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "unprotected header must not be used" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/unsuitable-algorithm-none" : func ( t * testing . T ) test {
jws := & jose . JSONWebSignature {
Signatures : [ ] jose . Signature {
{ Protected : jose . Header { Algorithm : "none" } } ,
} ,
}
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "unsuitable algorithm: none" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/unsuitable-algorithm-mac" : func ( t * testing . T ) test {
jws := & jose . JSONWebSignature {
Signatures : [ ] jose . Signature {
{ Protected : jose . Header { Algorithm : jose . HS256 } } ,
} ,
}
return test {
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "unsuitable algorithm: %s" , jose . HS256 ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2019-05-27 00:41:10 +00:00
return nil
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "jws key type and algorithm do not match" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2019-05-27 00:41:10 +00:00
return nil
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "rsa keys must be at least 2048 bits (256 bytes) in size" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/UseNonce-error" : func ( t * testing . T ) test {
jws := & jose . JSONWebSignature {
Signatures : [ ] jose . Signature {
{ Protected : jose . Header { Algorithm : jose . ES256 } } ,
} ,
}
return test {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2021-03-09 06:35:57 +00:00
return acme . NewErrorISE ( "force" )
2019-05-27 00:41:10 +00:00
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 500 ,
2021-03-09 06:35:57 +00:00
err : acme . NewErrorISE ( "force" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/no-url-header" : func ( t * testing . T ) test {
jws := & jose . JSONWebSignature {
Signatures : [ ] jose . Signature {
{ Protected : jose . Header { Algorithm : jose . ES256 } } ,
} ,
}
return test {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2019-05-27 00:41:10 +00:00
return nil
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "jws missing url protected header" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2019-05-27 00:41:10 +00:00
return nil
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "url header in JWS (foo) does not match request url (%s)" , url ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2019-05-27 00:41:10 +00:00
return nil
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "jwk and kid are mutually exclusive" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2019-05-27 00:41:10 +00:00
return nil
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
statusCode : 400 ,
2021-03-10 18:50:51 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "either jwk or kid must be defined in jws protected header" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"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 {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2019-05-27 00:41:10 +00:00
return nil
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
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 {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2019-05-27 00:41:10 +00:00
return nil
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
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 {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
2021-03-10 18:50:51 +00:00
MockDeleteNonce : func ( ctx context . Context , n acme . Nonce ) error {
2019-05-27 00:41:10 +00:00
return nil
} ,
} ,
2021-03-09 06:35:57 +00:00
ctx : context . WithValue ( context . Background ( ) , jwsContextKey , jws ) ,
2019-05-27 00:41:10 +00:00
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 ) {
2021-03-10 18:50:51 +00:00
h := & Handler { db : tc . db }
2019-05-27 00:41:10 +00:00
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 )
2021-03-09 06:35:57 +00:00
if res . StatusCode >= 400 && assert . NotNil ( t , tc . err ) {
2021-03-10 18:50:51 +00:00
var ae acme . Error
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & ae ) )
2021-03-09 06:35:57 +00:00
assert . Equals ( t , ae . Type , tc . err . Type )
assert . Equals ( t , ae . Detail , tc . err . Detail )
assert . Equals ( t , ae . Identifier , tc . err . Identifier )
assert . Equals ( t , ae . Subproblems , tc . err . Subproblems )
2019-05-27 00:41:10 +00:00
assert . Equals ( t , res . Header [ "Content-Type" ] , [ ] string { "application/problem+json" } )
} else {
assert . Equals ( t , bytes . TrimSpace ( body ) , testBody )
}
} )
}
}