2019-05-27 00:41:10 +00:00
package api
import (
"bytes"
"context"
2021-08-09 08:26:31 +00:00
"crypto"
"crypto/ecdsa"
"crypto/hmac"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
2019-05-27 00:41:10 +00:00
"encoding/json"
"fmt"
"io/ioutil"
2021-08-09 08:26:31 +00:00
"math/big"
2019-05-27 00:41:10 +00:00
"net/http/httptest"
2020-05-07 03:18:12 +00:00
"net/url"
2019-05-27 00:41:10 +00:00
"testing"
"time"
"github.com/go-chi/chi"
2021-08-10 10:39:11 +00:00
"github.com/pkg/errors"
2019-05-27 00:41:10 +00:00
"github.com/smallstep/assert"
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/authority/provisioner"
2020-08-24 21:44:11 +00:00
"go.step.sm/crypto/jose"
2021-08-09 08:26:31 +00:00
squarejose "gopkg.in/square/go-jose.v2"
2019-05-27 00:41:10 +00:00
)
var (
defaultDisableRenewal = false
globalProvisionerClaims = provisioner . Claims {
MinTLSDur : & provisioner . Duration { Duration : 5 * time . Minute } ,
MaxTLSDur : & provisioner . Duration { Duration : 24 * time . Hour } ,
DefaultTLSDur : & provisioner . Duration { Duration : 24 * time . Hour } ,
DisableRenewal : & defaultDisableRenewal ,
}
)
2021-03-25 07:23:57 +00:00
func newProv ( ) acme . Provisioner {
2019-05-27 00:41:10 +00:00
// Initialize provisioners
p := & provisioner . ACME {
Type : "ACME" ,
2021-04-13 02:06:07 +00:00
Name : "test@acme-<test>provisioner.com" ,
2019-05-27 00:41:10 +00:00
}
if err := p . Init ( provisioner . Config { Claims : globalProvisionerClaims } ) ; err != nil {
fmt . Printf ( "%v" , err )
}
return p
}
2021-08-09 08:26:31 +00:00
func newACMEProv ( t * testing . T ) * provisioner . ACME {
p := newProv ( )
a , ok := p . ( * provisioner . ACME )
if ! ok {
t . Fatal ( "not a valid ACME provisioner" )
}
return a
}
var errUnsupportedKey = fmt . Errorf ( "unknown key type; only RSA and ECDSA are supported" )
// keyID is the account identity provided by a CA during registration.
type keyID string
// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID.
// See jwsEncodeJSON for details.
const noKeyID = keyID ( "" )
// jwsEncodeEAB creates a JWS payload for External Account Binding according to RFC 8555 §7.3.4.
// Implementation taken from github.com/mholt/acmez
func jwsEncodeEAB ( accountKey crypto . PublicKey , hmacKey [ ] byte , kid keyID , url string ) ( [ ] byte , error ) {
// §7.3.4: "The 'alg' field MUST indicate a MAC-based algorithm"
alg , sha := "HS256" , crypto . SHA256
// §7.3.4: "The 'nonce' field MUST NOT be present"
phead , err := jwsHead ( alg , "" , url , kid , nil )
if err != nil {
return nil , err
}
encodedKey , err := jwkEncode ( accountKey )
if err != nil {
return nil , err
}
payload := base64 . RawURLEncoding . EncodeToString ( [ ] byte ( encodedKey ) )
payloadToSign := [ ] byte ( phead + "." + payload )
h := hmac . New ( sha256 . New , hmacKey )
h . Write ( payloadToSign )
sig := h . Sum ( nil )
return jwsFinal ( sha , sig , phead , payload )
}
// jwsHead constructs the protected JWS header for the given fields.
// Since jwk and kid are mutually-exclusive, the jwk will be encoded
// only if kid is empty. If nonce is empty, it will not be encoded.
// Implementation taken from github.com/mholt/acmez
func jwsHead ( alg , nonce , url string , kid keyID , key crypto . Signer ) ( string , error ) {
phead := fmt . Sprintf ( ` { "alg":%q ` , alg )
if kid == noKeyID {
jwk , err := jwkEncode ( key . Public ( ) )
if err != nil {
return "" , err
}
phead += fmt . Sprintf ( ` ,"jwk":%s ` , jwk )
} else {
phead += fmt . Sprintf ( ` ,"kid":%q ` , kid )
}
if nonce != "" {
phead += fmt . Sprintf ( ` ,"nonce":%q ` , nonce )
}
phead += fmt . Sprintf ( ` ,"url":%q} ` , url )
phead = base64 . RawURLEncoding . EncodeToString ( [ ] byte ( phead ) )
return phead , nil
}
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
// The result is also suitable for creating a JWK thumbprint.
// https://tools.ietf.org/html/rfc7517
// Implementation taken from github.com/mholt/acmez
func jwkEncode ( pub crypto . PublicKey ) ( string , error ) {
switch pub := pub . ( type ) {
case * rsa . PublicKey :
// https://tools.ietf.org/html/rfc7518#section-6.3.1
n := pub . N
e := big . NewInt ( int64 ( pub . E ) )
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt . Sprintf ( ` { "e":"%s","kty":"RSA","n":"%s"} ` ,
base64 . RawURLEncoding . EncodeToString ( e . Bytes ( ) ) ,
base64 . RawURLEncoding . EncodeToString ( n . Bytes ( ) ) ,
) , nil
case * ecdsa . PublicKey :
// https://tools.ietf.org/html/rfc7518#section-6.2.1
p := pub . Curve . Params ( )
n := p . BitSize / 8
if p . BitSize % 8 != 0 {
n ++
}
x := pub . X . Bytes ( )
if n > len ( x ) {
x = append ( make ( [ ] byte , n - len ( x ) ) , x ... )
}
y := pub . Y . Bytes ( )
if n > len ( y ) {
y = append ( make ( [ ] byte , n - len ( y ) ) , y ... )
}
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt . Sprintf ( ` { "crv":"%s","kty":"EC","x":"%s","y":"%s"} ` ,
p . Name ,
base64 . RawURLEncoding . EncodeToString ( x ) ,
base64 . RawURLEncoding . EncodeToString ( y ) ,
) , nil
}
return "" , errUnsupportedKey
}
// jwsFinal constructs the final JWS object.
// Implementation taken from github.com/mholt/acmez
func jwsFinal ( sha crypto . Hash , sig [ ] byte , phead , payload string ) ( [ ] byte , error ) {
enc := struct {
Protected string ` json:"protected" `
Payload string ` json:"payload" `
Sig string ` json:"signature" `
} {
Protected : phead ,
Payload : payload ,
Sig : base64 . RawURLEncoding . EncodeToString ( sig ) ,
}
result , err := json . Marshal ( & enc )
if err != nil {
return nil , err
}
return result , nil
}
2021-03-11 07:05:46 +00:00
func TestNewAccountRequest_Validate ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
type test struct {
nar * NewAccountRequest
err * acme . Error
}
var tests = map [ string ] func ( t * testing . T ) test {
"fail/incompatible-input" : func ( t * testing . T ) test {
return test {
nar : & NewAccountRequest {
OnlyReturnExisting : true ,
Contact : [ ] string { "foo" , "bar" } ,
} ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "incompatible input; onlyReturnExisting must be alone" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/bad-contact" : func ( t * testing . T ) test {
return test {
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "" } ,
} ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "contact cannot be empty string" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"ok" : func ( t * testing . T ) test {
return test {
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
} ,
}
} ,
"ok/onlyReturnExisting" : func ( t * testing . T ) test {
return test {
nar : & NewAccountRequest {
OnlyReturnExisting : true ,
} ,
}
} ,
}
for name , run := range tests {
tc := run ( t )
t . Run ( name , func ( t * testing . T ) {
if err := tc . nar . Validate ( ) ; err != nil {
if assert . NotNil ( t , err ) {
ae , ok := err . ( * acme . Error )
assert . True ( t , ok )
assert . HasPrefix ( t , ae . Error ( ) , tc . err . Error ( ) )
assert . Equals ( t , ae . StatusCode ( ) , tc . err . StatusCode ( ) )
assert . Equals ( t , ae . Type , tc . err . Type )
}
} else {
assert . Nil ( t , tc . err )
}
} )
}
}
2021-03-11 07:05:46 +00:00
func TestUpdateAccountRequest_Validate ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
type test struct {
uar * UpdateAccountRequest
err * acme . Error
}
var tests = map [ string ] func ( t * testing . T ) test {
"fail/incompatible-input" : func ( t * testing . T ) test {
return test {
uar : & UpdateAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
Status : "foo" ,
} ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "incompatible input; " +
"contact and status updates are mutually exclusive" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/bad-contact" : func ( t * testing . T ) test {
return test {
uar : & UpdateAccountRequest {
Contact : [ ] string { "foo" , "" } ,
} ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "contact cannot be empty string" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/bad-status" : func ( t * testing . T ) test {
return test {
uar : & UpdateAccountRequest {
Status : "foo" ,
} ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorMalformedType , "cannot update account " +
"status to foo, only deactivated" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"ok/contact" : func ( t * testing . T ) test {
return test {
uar : & UpdateAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
} ,
}
} ,
"ok/status" : func ( t * testing . T ) test {
return test {
uar : & UpdateAccountRequest {
Status : "deactivated" ,
} ,
}
} ,
2020-05-08 18:52:30 +00:00
"ok/accept-empty" : func ( t * testing . T ) test {
return test {
uar : & UpdateAccountRequest { } ,
}
} ,
2019-05-27 00:41:10 +00:00
}
for name , run := range tests {
tc := run ( t )
t . Run ( name , func ( t * testing . T ) {
if err := tc . uar . Validate ( ) ; err != nil {
if assert . NotNil ( t , err ) {
ae , ok := err . ( * acme . Error )
assert . True ( t , ok )
assert . HasPrefix ( t , ae . Error ( ) , tc . err . Error ( ) )
assert . Equals ( t , ae . StatusCode ( ) , tc . err . StatusCode ( ) )
assert . Equals ( t , ae . Type , tc . err . Type )
}
} else {
assert . Nil ( t , tc . err )
}
} )
}
}
2021-03-09 06:35:57 +00:00
func TestHandler_GetOrdersByAccountID ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
accID := "account-id"
// Request with chi context
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "accID" , accID )
2021-03-12 08:16:48 +00:00
prov := newProv ( )
provName := url . PathEscape ( prov . GetName ( ) )
baseURL := & url . URL { Scheme : "https" , Host : "test.ca.smallstep.com" }
2021-10-08 18:59:57 +00:00
u := fmt . Sprintf ( "http://ca.smallstep.com/acme/%s/account/%s/orders" , provName , accID )
2019-05-27 00:41:10 +00:00
2021-04-13 02:06:07 +00:00
oids := [ ] string { "foo" , "bar" }
oidURLs := [ ] string {
fmt . Sprintf ( "%s/acme/%s/order/foo" , baseURL . String ( ) , provName ) ,
fmt . Sprintf ( "%s/acme/%s/order/bar" , baseURL . String ( ) , 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
statusCode int
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 {
"fail/no-account" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB { } ,
2021-03-12 08:16:48 +00:00
ctx : context . Background ( ) ,
2020-02-02 01:35:41 +00:00
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorAccountDoesNotExistType , "account does not exist" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-account" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB { } ,
2021-03-12 08:16:48 +00:00
ctx : context . WithValue ( context . Background ( ) , accContextKey , nil ) ,
2020-02-02 01:35:41 +00:00
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorAccountDoesNotExistType , "account does not exist" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/account-id-mismatch" : func ( t * testing . T ) test {
acc := & acme . Account { ID : "foo" }
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , accContextKey , acc )
2019-05-27 00:41:10 +00:00
ctx = context . WithValue ( ctx , chi . RouteCtxKey , chiCtx )
return test {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB { } ,
2019-05-27 00:41:10 +00:00
ctx : ctx ,
statusCode : 401 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorUnauthorizedType , "account ID does not match url param" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
2021-03-12 08:16:48 +00:00
"fail/db.GetOrdersByAccountID-error" : func ( t * testing . T ) test {
2019-05-27 00:41:10 +00:00
acc := & acme . Account { ID : accID }
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , accContextKey , acc )
2019-05-27 00:41:10 +00:00
ctx = context . WithValue ( ctx , chi . RouteCtxKey , chiCtx )
return test {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
MockError : 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
}
} ,
"ok" : func ( t * testing . T ) test {
acc := & acme . Account { ID : accID }
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , accContextKey , acc )
2019-05-27 00:41:10 +00:00
ctx = context . WithValue ( ctx , chi . RouteCtxKey , chiCtx )
2021-03-12 08:16:48 +00:00
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
2019-05-27 00:41:10 +00:00
return test {
2021-03-09 06:35:57 +00:00
db : & acme . MockDB {
MockGetOrdersByAccountID : func ( ctx context . Context , id string ) ( [ ] string , error ) {
2019-05-27 00:41:10 +00:00
assert . Equals ( t , id , acc . ID )
return oids , nil
} ,
} ,
ctx : ctx ,
statusCode : 200 ,
}
} ,
}
for name , run := range tests {
tc := run ( t )
t . Run ( name , func ( t * testing . T ) {
2021-03-12 08:16:48 +00:00
h := & Handler { db : tc . db , linker : NewLinker ( "dns" , "acme" ) }
2021-10-08 18:59:57 +00:00
req := httptest . NewRequest ( "GET" , u , nil )
2019-05-27 00:41:10 +00:00
req = req . WithContext ( tc . ctx )
w := httptest . NewRecorder ( )
2021-03-09 06:35:57 +00:00
h . GetOrdersByAccountID ( w , req )
2019-05-27 00:41:10 +00:00
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 {
2021-03-12 08:16:48 +00:00
expB , err := json . Marshal ( oidURLs )
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , err )
assert . Equals ( t , bytes . TrimSpace ( body ) , expB )
assert . Equals ( t , res . Header [ "Content-Type" ] , [ ] string { "application/json" } )
}
} )
}
}
2021-03-09 06:35:57 +00:00
func TestHandler_NewAccount ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
prov := newProv ( )
2021-04-13 02:06:07 +00:00
escProvName := url . PathEscape ( prov . GetName ( ) )
2020-05-07 03:18:12 +00:00
baseURL := & url . URL { Scheme : "https" , Host : "test.ca.smallstep.com" }
2019-05-27 00:41:10 +00:00
type test struct {
2021-03-09 06:35:57 +00:00
db acme . DB
2021-03-11 21:10:14 +00:00
acc * acme . Account
2019-05-27 00:41:10 +00:00
ctx context . Context
statusCode int
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 {
"fail/no-payload" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . Background ( ) ,
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/nil-payload" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , 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 ( "payload expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/unmarshal-payload-error" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { } )
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 , "failed to " +
"unmarshal new-account request payload: unexpected end of JSON input" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/malformed-payload-error" : func ( t * testing . T ) test {
nar := & NewAccountRequest {
Contact : [ ] string { "foo" , "" } ,
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { value : b } )
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 , "contact cannot be empty string" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/no-existing-account" : func ( t * testing . T ) test {
nar := & NewAccountRequest {
OnlyReturnExisting : true ,
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { value : b } )
2021-07-17 17:02:47 +00:00
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
2020-02-02 01:35:41 +00:00
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorAccountDoesNotExistType , "account does not exist" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/no-jwk" : func ( t * testing . T ) test {
nar := & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { value : b } )
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 ( "jwk expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-jwk" : func ( t * testing . T ) test {
nar := & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { value : b } )
ctx = context . WithValue ( ctx , jwkContextKey , 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 ( "jwk expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
2021-08-09 08:26:31 +00:00
"fail/new-account-no-eab-provided" : func ( t * testing . T ) test {
nar := & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : nil ,
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { value : b } )
ctx = context . WithValue ( ctx , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
ctx : ctx ,
statusCode : 400 ,
err : acme . NewError ( acme . ErrorExternalAccountRequiredType , "no external account binding provided" ) ,
}
} ,
2021-03-11 21:10:14 +00:00
"fail/db.CreateAccount-error" : func ( t * testing . T ) test {
2019-05-27 00:41:10 +00:00
nar := & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { value : b } )
ctx = context . WithValue ( ctx , jwkContextKey , jwk )
return test {
db : & acme . MockDB {
MockCreateAccount : func ( ctx context . Context , acc * acme . Account ) error {
assert . Equals ( t , acc . Contact , nar . Contact )
assert . Equals ( t , acc . Key , jwk )
return 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
}
} ,
"ok/new-account" : func ( t * testing . T ) test {
nar := & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { value : b } )
ctx = context . WithValue ( ctx , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB {
MockCreateAccount : func ( ctx context . Context , acc * acme . Account ) error {
2021-03-11 21:10:14 +00:00
acc . ID = "accountID"
2021-03-09 06:35:57 +00:00
assert . Equals ( t , acc . Contact , nar . Contact )
assert . Equals ( t , acc . Key , jwk )
return nil
2019-05-27 00:41:10 +00:00
} ,
2021-03-11 21:10:14 +00:00
} ,
acc : & acme . Account {
2021-03-15 17:30:12 +00:00
ID : "accountID" ,
Key : jwk ,
Status : acme . StatusValid ,
Contact : [ ] string { "foo" , "bar" } ,
2021-04-13 02:06:07 +00:00
OrdersURL : fmt . Sprintf ( "%s/acme/%s/account/accountID/orders" , baseURL . String ( ) , escProvName ) ,
2019-05-27 00:41:10 +00:00
} ,
ctx : ctx ,
statusCode : 201 ,
}
} ,
"ok/return-existing" : func ( t * testing . T ) test {
nar := & NewAccountRequest {
OnlyReturnExisting : true ,
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
2021-03-11 21:10:14 +00:00
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
acc := & acme . Account {
ID : "accountID" ,
Key : jwk ,
Status : acme . StatusValid ,
Contact : [ ] string { "foo" , "bar" } ,
}
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , payloadContextKey , & payloadInfo { value : b } )
2021-03-11 21:10:14 +00:00
ctx = context . WithValue ( ctx , accContextKey , acc )
2021-03-09 06:35:57 +00:00
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
return test {
2019-05-27 00:41:10 +00:00
ctx : ctx ,
2021-03-11 21:10:14 +00:00
acc : acc ,
2019-05-27 00:41:10 +00:00
statusCode : 200 ,
}
} ,
2021-08-09 08:26:31 +00:00
"ok/new-account-no-eab-required" : func ( t * testing . T ) test {
2021-08-10 10:39:11 +00:00
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
2021-08-09 08:26:31 +00:00
nar := & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
2021-08-10 10:39:11 +00:00
ExternalAccountBinding : eab ,
2021-08-09 08:26:31 +00:00
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = false
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { value : b } )
ctx = context . WithValue ( ctx , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB {
MockCreateAccount : func ( ctx context . Context , acc * acme . Account ) error {
acc . ID = "accountID"
assert . Equals ( t , acc . Contact , nar . Contact )
assert . Equals ( t , acc . Key , jwk )
return nil
} ,
} ,
acc : & acme . Account {
ID : "accountID" ,
Key : jwk ,
Status : acme . StatusValid ,
Contact : [ ] string { "foo" , "bar" } ,
OrdersURL : fmt . Sprintf ( "%s/acme/%s/account/accountID/orders" , baseURL . String ( ) , escProvName ) ,
} ,
ctx : ctx ,
statusCode : 201 ,
}
} ,
"ok/new-account-with-eab" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
2021-08-10 10:39:11 +00:00
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
2021-08-09 08:26:31 +00:00
assert . FatalError ( t , err )
nar := & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
2021-08-10 10:39:11 +00:00
ExternalAccountBinding : eab ,
2021-08-09 08:26:31 +00:00
}
b , err := json . Marshal ( nar )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , payloadContextKey , & payloadInfo { value : b } )
ctx = context . WithValue ( ctx , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB {
MockCreateAccount : func ( ctx context . Context , acc * acme . Account ) error {
acc . ID = "accountID"
assert . Equals ( t , acc . Contact , nar . Contact )
assert . Equals ( t , acc . Key , jwk )
return nil
} ,
MockGetExternalAccountKey : func ( ctx context . Context , provisionerName string , keyID string ) ( * acme . ExternalAccountKey , error ) {
return & acme . ExternalAccountKey {
2021-09-16 21:09:24 +00:00
ID : "eakID" ,
Provisioner : escProvName ,
Reference : "testeak" ,
KeyBytes : [ ] byte { 1 , 3 , 3 , 7 } ,
CreatedAt : time . Now ( ) ,
2021-08-09 08:26:31 +00:00
} , nil
} ,
MockUpdateExternalAccountKey : func ( ctx context . Context , provisionerName string , eak * acme . ExternalAccountKey ) error {
return nil
} ,
} ,
acc : & acme . Account {
ID : "accountID" ,
Key : jwk ,
Status : acme . StatusValid ,
Contact : [ ] string { "foo" , "bar" } ,
OrdersURL : fmt . Sprintf ( "%s/acme/%s/account/accountID/orders" , baseURL . String ( ) , escProvName ) ,
2021-08-10 10:39:11 +00:00
ExternalAccountBinding : eab ,
2021-08-09 08:26:31 +00:00
} ,
ctx : ctx ,
statusCode : 201 ,
}
} ,
2019-05-27 00:41:10 +00:00
}
for name , run := range tests {
tc := run ( t )
t . Run ( name , func ( t * testing . T ) {
2021-03-11 21:10:14 +00:00
h := & Handler { db : tc . db , linker : NewLinker ( "dns" , "acme" ) }
2020-05-07 03:18:12 +00:00
req := httptest . NewRequest ( "GET" , "/foo/bar" , nil )
2019-05-27 00:41:10 +00:00
req = req . WithContext ( tc . ctx )
w := httptest . NewRecorder ( )
h . NewAccount ( 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 {
2021-03-11 21:10:14 +00:00
expB , err := json . Marshal ( tc . acc )
2019-05-27 00:41:10 +00:00
assert . FatalError ( t , err )
assert . Equals ( t , bytes . TrimSpace ( body ) , expB )
assert . Equals ( t , res . Header [ "Location" ] ,
2020-05-07 03:18:12 +00:00
[ ] string { fmt . Sprintf ( "%s/acme/%s/account/%s" , baseURL . String ( ) ,
2021-04-13 02:06:07 +00:00
escProvName , "accountID" ) } )
2019-05-27 00:41:10 +00:00
assert . Equals ( t , res . Header [ "Content-Type" ] , [ ] string { "application/json" } )
}
} )
}
}
2021-04-13 02:06:07 +00:00
func TestHandler_GetOrUpdateAccount ( t * testing . T ) {
2019-05-27 00:41:10 +00:00
accID := "accountID"
acc := acme . Account {
2021-03-15 17:30:12 +00:00
ID : accID ,
Status : "valid" ,
OrdersURL : fmt . Sprintf ( "https://ca.smallstep.com/acme/account/%s/orders" , accID ) ,
2019-05-27 00:41:10 +00:00
}
prov := newProv ( )
2021-04-13 02:06:07 +00:00
escProvName := url . PathEscape ( prov . GetName ( ) )
2020-05-07 03:18:12 +00:00
baseURL := & url . URL { Scheme : "https" , Host : "test.ca.smallstep.com" }
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
statusCode int
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 {
"fail/no-account" : func ( t * testing . T ) test {
return test {
2021-03-09 06:35:57 +00:00
ctx : context . Background ( ) ,
2020-02-02 01:35:41 +00:00
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorAccountDoesNotExistType , "account does not exist" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-account" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , accContextKey , nil )
2019-05-27 00:41:10 +00:00
return test {
ctx : ctx ,
2020-02-02 01:35:41 +00:00
statusCode : 400 ,
2021-03-09 06:35:57 +00:00
err : acme . NewError ( acme . ErrorAccountDoesNotExistType , "account does not exist" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/no-payload" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , accContextKey , & acc )
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 ( "payload expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/nil-payload" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , accContextKey , & acc )
ctx = context . WithValue ( ctx , payloadContextKey , 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 ( "payload expected in request context" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/unmarshal-payload-error" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , accContextKey , & acc )
ctx = context . WithValue ( ctx , payloadContextKey , & payloadInfo { } )
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 , "failed to unmarshal new-account request payload: unexpected end of JSON input" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
"fail/malformed-payload-error" : func ( t * testing . T ) test {
uar := & UpdateAccountRequest {
Contact : [ ] string { "foo" , "" } ,
}
b , err := json . Marshal ( uar )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , accContextKey , & acc )
ctx = context . WithValue ( ctx , payloadContextKey , & payloadInfo { value : b } )
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 , "contact cannot be empty string" ) ,
2019-05-27 00:41:10 +00:00
}
} ,
2021-03-12 08:16:48 +00:00
"fail/db.UpdateAccount-error" : func ( t * testing . T ) test {
2019-05-27 00:41:10 +00:00
uar := & UpdateAccountRequest {
Status : "deactivated" ,
}
b , err := json . Marshal ( uar )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , accContextKey , & acc )
ctx = context . WithValue ( ctx , payloadContextKey , & payloadInfo { value : b } )
return test {
db : & acme . MockDB {
MockUpdateAccount : func ( ctx context . Context , upd * acme . Account ) error {
assert . Equals ( t , upd . Status , acme . StatusDeactivated )
assert . Equals ( t , upd . ID , acc . ID )
return 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
}
} ,
"ok/deactivate" : func ( t * testing . T ) test {
uar := & UpdateAccountRequest {
Status : "deactivated" ,
}
b , err := json . Marshal ( uar )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , accContextKey , & acc )
ctx = context . WithValue ( ctx , payloadContextKey , & payloadInfo { value : b } )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
return test {
db : & acme . MockDB {
MockUpdateAccount : func ( ctx context . Context , upd * acme . Account ) error {
assert . Equals ( t , upd . Status , acme . StatusDeactivated )
assert . Equals ( t , upd . ID , acc . ID )
return nil
2019-05-27 00:41:10 +00:00
} ,
} ,
ctx : ctx ,
statusCode : 200 ,
}
} ,
2020-05-08 18:52:30 +00:00
"ok/update-empty" : func ( t * testing . T ) test {
uar := & UpdateAccountRequest { }
b , err := json . Marshal ( uar )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , accContextKey , & acc )
ctx = context . WithValue ( ctx , payloadContextKey , & payloadInfo { value : b } )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
return test {
2020-05-08 18:52:30 +00:00
ctx : ctx ,
statusCode : 200 ,
}
} ,
"ok/update-contacts" : func ( t * testing . T ) test {
2019-05-27 00:41:10 +00:00
uar := & UpdateAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
}
b , err := json . Marshal ( uar )
assert . FatalError ( t , err )
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , accContextKey , & acc )
ctx = context . WithValue ( ctx , payloadContextKey , & payloadInfo { value : b } )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
return test {
db : & acme . MockDB {
MockUpdateAccount : func ( ctx context . Context , upd * acme . Account ) error {
assert . Equals ( t , upd . Contact , uar . Contact )
assert . Equals ( t , upd . ID , acc . ID )
return nil
2019-05-27 00:41:10 +00:00
} ,
} ,
ctx : ctx ,
statusCode : 200 ,
}
} ,
"ok/post-as-get" : func ( t * testing . T ) test {
2021-03-09 06:35:57 +00:00
ctx := context . WithValue ( context . Background ( ) , provisionerContextKey , prov )
ctx = context . WithValue ( ctx , accContextKey , & acc )
ctx = context . WithValue ( ctx , payloadContextKey , & payloadInfo { isPostAsGet : true } )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
return test {
2019-05-27 00:41:10 +00:00
ctx : ctx ,
statusCode : 200 ,
}
} ,
}
for name , run := range tests {
tc := run ( t )
t . Run ( name , func ( t * testing . T ) {
2021-03-11 21:10:14 +00:00
h := & Handler { db : tc . db , linker : NewLinker ( "dns" , "acme" ) }
2020-05-07 03:18:12 +00:00
req := httptest . NewRequest ( "GET" , "/foo/bar" , nil )
2019-05-27 00:41:10 +00:00
req = req . WithContext ( tc . ctx )
w := httptest . NewRecorder ( )
2021-04-13 02:06:07 +00:00
h . GetOrUpdateAccount ( w , req )
2019-05-27 00:41:10 +00:00
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 {
expB , err := json . Marshal ( acc )
assert . FatalError ( t , err )
assert . Equals ( t , bytes . TrimSpace ( body ) , expB )
assert . Equals ( t , res . Header [ "Location" ] ,
2020-05-07 03:18:12 +00:00
[ ] string { fmt . Sprintf ( "%s/acme/%s/account/%s" , baseURL . String ( ) ,
2021-04-13 02:06:07 +00:00
escProvName , accID ) } )
2019-05-27 00:41:10 +00:00
assert . Equals ( t , res . Header [ "Content-Type" ] , [ ] string { "application/json" } )
}
} )
}
}
2021-08-09 08:26:31 +00:00
func Test_keysAreEqual ( t * testing . T ) {
jwkX , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
jwkY , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
2021-08-10 10:39:11 +00:00
wrongJWK , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
wrongJWK . Key = struct { } { }
2021-08-09 08:26:31 +00:00
type args struct {
x * squarejose . JSONWebKey
y * squarejose . JSONWebKey
}
tests := [ ] struct {
name string
args args
want bool
} {
{
name : "ok/nil" ,
args : args {
x : jwkX ,
y : nil ,
} ,
want : false ,
} ,
{
name : "ok/equal" ,
args : args {
x : jwkX ,
y : jwkX ,
} ,
want : true ,
} ,
{
name : "ok/not-equal" ,
args : args {
x : jwkX ,
y : jwkY ,
} ,
want : false ,
} ,
2021-08-10 10:39:11 +00:00
{
name : "ok/wrong-key-type" ,
args : args {
x : wrongJWK ,
y : jwkY ,
} ,
want : false ,
} ,
2021-08-09 08:26:31 +00:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
if got := keysAreEqual ( tt . args . x , tt . args . y ) ; got != tt . want {
t . Errorf ( "keysAreEqual() = %v, want %v" , got , tt . want )
}
} )
}
}
func TestHandler_validateExternalAccountBinding ( t * testing . T ) {
2021-08-10 10:39:11 +00:00
acmeProv := newACMEProv ( t )
escProvName := url . PathEscape ( acmeProv . GetName ( ) )
baseURL := & url . URL { Scheme : "https" , Host : "test.ca.smallstep.com" }
type test struct {
db acme . DB
2021-08-09 08:26:31 +00:00
ctx context . Context
nar * NewAccountRequest
2021-08-10 10:39:11 +00:00
eak * acme . ExternalAccountKey
err * acme . Error
2021-08-09 08:26:31 +00:00
}
2021-08-10 10:39:11 +00:00
var tests = map [ string ] func ( t * testing . T ) test {
"ok/no-eab-required-but-provided" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
ctx := context . WithValue ( context . Background ( ) , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB { } ,
ctx : ctx ,
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : eab ,
} ,
eak : nil ,
err : nil ,
}
} ,
"ok/eab" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
2021-10-08 11:18:23 +00:00
createdAt := time . Now ( )
2021-08-10 10:39:11 +00:00
return test {
db : & acme . MockDB {
MockGetExternalAccountKey : func ( ctx context . Context , provisionerName string , keyID string ) ( * acme . ExternalAccountKey , error ) {
return & acme . ExternalAccountKey {
2021-09-16 21:09:24 +00:00
ID : "eakID" ,
Provisioner : escProvName ,
Reference : "testeak" ,
KeyBytes : [ ] byte { 1 , 3 , 3 , 7 } ,
2021-10-08 11:18:23 +00:00
CreatedAt : createdAt ,
2021-08-10 10:39:11 +00:00
} , nil
} ,
} ,
ctx : ctx ,
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : eab ,
} ,
2021-10-08 11:18:23 +00:00
eak : & acme . ExternalAccountKey {
ID : "eakID" ,
Provisioner : escProvName ,
Reference : "testeak" ,
KeyBytes : [ ] byte { 1 , 3 , 3 , 7 } ,
CreatedAt : createdAt ,
} ,
2021-08-10 10:39:11 +00:00
err : nil ,
}
} ,
"fail/parse-eab-jose" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
eab . Payload = eab . Payload + "{}"
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB { } ,
ctx : ctx ,
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : eab ,
} ,
eak : nil ,
err : acme . NewErrorISE ( "error parsing externalAccountBinding jws" ) ,
}
} ,
"fail/retrieve-eab-key-db-failure" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB {
MockError : errors . New ( "db failure" ) ,
} ,
ctx : ctx ,
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : eab ,
} ,
eak : nil ,
err : acme . NewErrorISE ( "error retrieving external account key" ) ,
}
} ,
"fail/retrieve-eab-key-not-found" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "unknown-key-id" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB {
MockGetExternalAccountKey : func ( ctx context . Context , provisionerName string , keyID string ) ( * acme . ExternalAccountKey , error ) {
return nil , acme . NewErrorISE ( "error retrieving external account key" )
} ,
} ,
ctx : ctx ,
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : eab ,
} ,
eak : nil ,
err : acme . NewErrorISE ( "error loading external account key unknown-key-id" ) ,
}
} ,
"fail/retrieve-eab-wrong-provisioner" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB {
MockError : acme . NewError ( acme . ErrorUnauthorizedType , "name of provisioner does not match provisioner for which the EAB key was created" ) ,
} ,
ctx : ctx ,
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : eab ,
} ,
eak : nil ,
err : acme . NewError ( acme . ErrorUnauthorizedType , "name of provisioner does not match provisioner for which the EAB key was created" ) ,
}
} ,
"fail/eab-already-bound" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
createdAt := time . Now ( )
boundAt := time . Now ( ) . Add ( 1 * time . Second )
return test {
db : & acme . MockDB {
MockGetExternalAccountKey : func ( ctx context . Context , provisionerName string , keyID string ) ( * acme . ExternalAccountKey , error ) {
return & acme . ExternalAccountKey {
2021-09-16 21:09:24 +00:00
ID : "eakID" ,
Provisioner : escProvName ,
Reference : "testeak" ,
CreatedAt : createdAt ,
AccountID : "some-account-id" ,
BoundAt : boundAt ,
2021-08-10 10:39:11 +00:00
} , nil
} ,
} ,
ctx : ctx ,
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : eab ,
} ,
eak : nil ,
err : acme . NewError ( acme . ErrorUnauthorizedType , "external account binding key with id '%s' was already bound to account '%s' on %s" , "eakID" , "some-account-id" , boundAt ) ,
}
} ,
"fail/eab-verify" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( jwk . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB {
MockGetExternalAccountKey : func ( ctx context . Context , provisionerName string , keyID string ) ( * acme . ExternalAccountKey , error ) {
return & acme . ExternalAccountKey {
2021-09-16 21:09:24 +00:00
ID : "eakID" ,
Provisioner : escProvName ,
Reference : "testeak" ,
KeyBytes : [ ] byte { 1 , 2 , 3 , 4 } ,
CreatedAt : time . Now ( ) ,
2021-08-10 10:39:11 +00:00
} , nil
} ,
} ,
ctx : ctx ,
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : eab ,
} ,
eak : nil ,
err : acme . NewErrorISE ( "error verifying externalAccountBinding signature" ) ,
}
} ,
"fail/eab-non-matching-keys" : func ( t * testing . T ) test {
jwk , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
differentJWK , err := jose . GenerateJWK ( "EC" , "P-256" , "ES256" , "sig" , "" , 0 )
assert . FatalError ( t , err )
eabJWS , err := jwsEncodeEAB ( differentJWK . Public ( ) . Key , [ ] byte { 1 , 3 , 3 , 7 } , "eakID" , fmt . Sprintf ( "%s/acme/%s/account/new-account" , baseURL . String ( ) , escProvName ) )
assert . FatalError ( t , err )
eab := & ExternalAccountBinding { }
err = json . Unmarshal ( eabJWS , & eab )
assert . FatalError ( t , err )
prov := newACMEProv ( t )
prov . RequireEAB = true
ctx := context . WithValue ( context . Background ( ) , jwkContextKey , jwk )
ctx = context . WithValue ( ctx , baseURLContextKey , baseURL )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
return test {
db : & acme . MockDB {
MockGetExternalAccountKey : func ( ctx context . Context , provisionerName string , keyID string ) ( * acme . ExternalAccountKey , error ) {
return & acme . ExternalAccountKey {
2021-09-16 21:09:24 +00:00
ID : "eakID" ,
Provisioner : escProvName ,
Reference : "testeak" ,
KeyBytes : [ ] byte { 1 , 3 , 3 , 7 } ,
CreatedAt : time . Now ( ) ,
2021-08-10 10:39:11 +00:00
} , nil
} ,
} ,
ctx : ctx ,
nar : & NewAccountRequest {
Contact : [ ] string { "foo" , "bar" } ,
ExternalAccountBinding : eab ,
} ,
eak : nil ,
err : acme . NewError ( acme . ErrorMalformedType , "keys in jws and eab payload do not match" ) ,
}
} ,
2021-08-09 08:26:31 +00:00
}
2021-08-10 10:39:11 +00:00
for name , run := range tests {
tc := run ( t )
t . Run ( name , func ( t * testing . T ) {
2021-08-09 08:26:31 +00:00
h := & Handler {
2021-08-10 10:39:11 +00:00
db : tc . db ,
}
got , err := h . validateExternalAccountBinding ( tc . ctx , tc . nar )
wantErr := tc . err != nil
gotErr := err != nil
if wantErr != gotErr {
t . Errorf ( "Handler.validateExternalAccountBinding() error = %v, want %v" , err , tc . err )
}
if wantErr {
assert . NotNil ( t , err )
assert . Type ( t , & acme . Error { } , err )
ae , _ := err . ( * acme . Error )
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 )
} else {
if got == nil {
assert . Nil ( t , tc . eak )
} else {
assert . NotNil ( t , tc . eak )
2021-10-08 11:18:23 +00:00
assert . Equals ( t , got . ID , tc . eak . ID )
assert . Equals ( t , got . KeyBytes , tc . eak . KeyBytes )
assert . Equals ( t , got . Provisioner , tc . eak . Provisioner )
assert . Equals ( t , got . Reference , tc . eak . Reference )
assert . Equals ( t , got . CreatedAt , tc . eak . CreatedAt )
assert . Equals ( t , got . AccountID , tc . eak . AccountID )
assert . Equals ( t , got . BoundAt , tc . eak . BoundAt )
2021-08-10 10:39:11 +00:00
}
2021-08-09 08:26:31 +00:00
}
} )
}
}