package authority
import (
"context"
"crypto/rand"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
"reflect"
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority/provisioner"
"github.com/smallstep/certificates/db"
"github.com/smallstep/cli/crypto/keys"
"github.com/smallstep/cli/crypto/pemutil"
"github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util"
"github.com/smallstep/cli/jose"
"gopkg.in/square/go-jose.v2/jwt"
)
var (
stepOIDRoot = asn1 . ObjectIdentifier { 1 , 3 , 6 , 1 , 4 , 1 , 37476 , 9000 , 64 }
stepOIDProvisioner = append ( asn1 . ObjectIdentifier ( nil ) , append ( stepOIDRoot , 1 ) ... )
)
const provisionerTypeJWK = 1
type stepProvisionerASN1 struct {
Type int
Name [ ] byte
CredentialID [ ] byte
}
func withProvisionerOID ( name , kid string ) x509util . WithOption {
return func ( p x509util . Profile ) error {
crt := p . Subject ( )
b , err := asn1 . Marshal ( stepProvisionerASN1 {
Type : provisionerTypeJWK ,
Name : [ ] byte ( name ) ,
CredentialID : [ ] byte ( kid ) ,
} )
if err != nil {
return err
}
crt . ExtraExtensions = append ( crt . ExtraExtensions , pkix . Extension {
Id : stepOIDProvisioner ,
Critical : false ,
Value : b ,
} )
return nil
}
}
func getCSR ( t * testing . T , priv interface { } , opts ... func ( * x509 . CertificateRequest ) ) * x509 . CertificateRequest {
_csr := & x509 . CertificateRequest {
Subject : pkix . Name { CommonName : "smallstep test" } ,
DNSNames : [ ] string { "test.smallstep.com" } ,
}
for _ , opt := range opts {
opt ( _csr )
}
csrBytes , err := x509 . CreateCertificateRequest ( rand . Reader , _csr , priv )
assert . FatalError ( t , err )
csr , err := x509 . ParseCertificateRequest ( csrBytes )
assert . FatalError ( t , err )
return csr
}
func TestSign ( t * testing . T ) {
pub , priv , err := keys . GenerateDefaultKeyPair ( )
assert . FatalError ( t , err )
a := testAuthority ( t )
assert . FatalError ( t , err )
a . config . AuthorityConfig . Template = & x509util . ASN1DN {
Country : "Tazmania" ,
Organization : "Acme Co" ,
Locality : "Landscapes" ,
Province : "Sudden Cliffs" ,
StreetAddress : "TNT" ,
CommonName : "test.smallstep.com" ,
}
nb := time . Now ( )
signOpts := provisioner . Options {
NotBefore : provisioner . NewTimeDuration ( nb ) ,
NotAfter : provisioner . NewTimeDuration ( nb . Add ( time . Minute * 5 ) ) ,
}
// Create a token to get test extra opts.
p := a . config . AuthorityConfig . Provisioners [ 1 ] . ( * provisioner . JWK )
key , err := jose . ParseKey ( "testdata/secrets/step_cli_key_priv.jwk" , jose . WithPassword ( [ ] byte ( "pass" ) ) )
assert . FatalError ( t , err )
token , err := generateToken ( "smallstep test" , "step-cli" , "https://test.ca.smallstep.com/sign" , [ ] string { "test.smallstep.com" } , time . Now ( ) , key )
assert . FatalError ( t , err )
ctx := provisioner . NewContextWithMethod ( context . Background ( ) , provisioner . SignMethod )
extraOpts , err := a . Authorize ( ctx , token )
assert . FatalError ( t , err )
type signTest struct {
auth * Authority
csr * x509 . CertificateRequest
signOpts provisioner . Options
extraOpts [ ] provisioner . SignOption
err * apiError
}
tests := map [ string ] func ( * testing . T ) * signTest {
"fail invalid signature" : func ( t * testing . T ) * signTest {
csr := getCSR ( t , priv )
csr . Signature = [ ] byte ( "foo" )
return & signTest {
auth : a ,
csr : csr ,
extraOpts : extraOpts ,
signOpts : signOpts ,
err : & apiError { errors . New ( "sign: invalid certificate request" ) ,
http . StatusBadRequest ,
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
} ,
}
} ,
"fail invalid extra option" : func ( t * testing . T ) * signTest {
csr := getCSR ( t , priv )
csr . Raw = [ ] byte ( "foo" )
return & signTest {
auth : a ,
csr : csr ,
extraOpts : append ( extraOpts , "42" ) ,
signOpts : signOpts ,
err : & apiError { errors . New ( "sign: invalid extra option type string" ) ,
http . StatusInternalServerError ,
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
} ,
}
} ,
"fail merge default ASN1DN" : func ( t * testing . T ) * signTest {
_a := testAuthority ( t )
_a . config . AuthorityConfig . Template = nil
csr := getCSR ( t , priv )
return & signTest {
auth : _a ,
csr : csr ,
extraOpts : extraOpts ,
signOpts : signOpts ,
err : & apiError { errors . New ( "sign: default ASN1DN template cannot be nil" ) ,
http . StatusInternalServerError ,
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
} ,
}
} ,
"fail create cert" : func ( t * testing . T ) * signTest {
_a := testAuthority ( t )
_a . intermediateIdentity . Key = nil
csr := getCSR ( t , priv )
return & signTest {
auth : _a ,
csr : csr ,
extraOpts : extraOpts ,
signOpts : signOpts ,
err : & apiError { errors . New ( "sign: error creating new leaf certificate" ) ,
http . StatusInternalServerError ,
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
} ,
}
} ,
"fail provisioner duration claim" : func ( t * testing . T ) * signTest {
csr := getCSR ( t , priv )
_signOpts := provisioner . Options {
NotBefore : provisioner . NewTimeDuration ( nb ) ,
NotAfter : provisioner . NewTimeDuration ( nb . Add ( time . Hour * 25 ) ) ,
}
return & signTest {
auth : a ,
csr : csr ,
extraOpts : extraOpts ,
signOpts : _signOpts ,
err : & apiError { errors . New ( "sign: requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h0m0s" ) ,
http . StatusUnauthorized ,
apiCtx { "csr" : csr , "signOptions" : _signOpts } ,
} ,
}
} ,
"fail validate sans when adding common name not in claims" : func ( t * testing . T ) * signTest {
csr := getCSR ( t , priv , func ( csr * x509 . CertificateRequest ) {
csr . DNSNames = append ( csr . DNSNames , csr . Subject . CommonName )
} )
return & signTest {
auth : a ,
csr : csr ,
extraOpts : extraOpts ,
signOpts : signOpts ,
err : & apiError { errors . New ( "sign: certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]" ) ,
http . StatusUnauthorized ,
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
} ,
}
} ,
"fail rsa key too short" : func ( t * testing . T ) * signTest {
shortRSAKeyPEM := ` -- -- - BEGIN CERTIFICATE REQUEST -- -- -
MIIBhDCB7gIBADAZMRcwFQYDVQQDEw5zbWFsbHN0ZXAgdGVzdDCBnzANBgkqhkiG
9 w0BAQEFAAOBjQAwgYkCgYEA5JlgH99HvHHsCD6XTqqYj3bXU2oIlnYGoLVs7IJ4
k205rv5 / YWky2gjdpIv0Tnaf3o57IJ891lB7GiyO5iHIEUv5N9dVzrdUboyzk2uZ
7 JMMNB43CSLB2oNuwJjLeAM / yBzlhRnvpKjrNSfSV + cH54FXdnbFbcTFMStnjqKG
MeECAwEAAaAsMCoGCSqGSIb3DQEJDjEdMBswGQYDVR0RBBIwEIIOc21hbGxzdGVw
IHRlc3QwDQYJKoZIhvcNAQELBQADgYEAKwsbr8Zfcq05DgOoJ //cXMFK1SP8ktRU
N2 ++ E8Ww0Tet9oyNRArqxxS / UyVio63D3wynzRAB25PFGpYG1cN4b81Gv / foFUT6
W5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5 / 3i 16 d5zhuxUoaPTYr
ZYtQ9Ot36qc =
-- -- - END CERTIFICATE REQUEST -- -- - `
block , _ := pem . Decode ( [ ] byte ( shortRSAKeyPEM ) )
assert . FatalError ( t , err )
csr , err := x509 . ParseCertificateRequest ( block . Bytes )
assert . FatalError ( t , err )
return & signTest {
auth : a ,
csr : csr ,
extraOpts : extraOpts ,
signOpts : signOpts ,
err : & apiError { errors . New ( "sign: rsa key in CSR must be at least 2048 bits (256 bytes)" ) ,
http . StatusUnauthorized ,
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
} ,
}
} ,
"fail store cert in db" : func ( t * testing . T ) * signTest {
csr := getCSR ( t , priv )
_a := testAuthority ( t )
_a . db = & MockAuthDB {
storeCertificate : func ( crt * x509 . Certificate ) error {
return & apiError { errors . New ( "force" ) ,
http . StatusInternalServerError ,
apiCtx { "csr" : csr , "signOptions" : signOpts } }
} ,
}
return & signTest {
auth : _a ,
csr : csr ,
extraOpts : extraOpts ,
signOpts : signOpts ,
err : & apiError { errors . New ( "sign: error storing certificate in db: force" ) ,
http . StatusInternalServerError ,
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
} ,
}
} ,
"ok" : func ( t * testing . T ) * signTest {
csr := getCSR ( t , priv )
_a := testAuthority ( t )
_a . db = & MockAuthDB {
storeCertificate : func ( crt * x509 . Certificate ) error {
assert . Equals ( t , crt . Subject . CommonName , "smallstep test" )
return nil
} ,
}
return & signTest {
auth : a ,
csr : csr ,
extraOpts : extraOpts ,
signOpts : signOpts ,
}
} ,
}
for name , genTestCase := range tests {
t . Run ( name , func ( t * testing . T ) {
tc := genTestCase ( t )
certChain , err := tc . auth . Sign ( tc . csr , tc . signOpts , tc . extraOpts ... )
if err != nil {
if assert . NotNil ( t , tc . err ) {
switch v := err . ( type ) {
case * apiError :
assert . HasPrefix ( t , v . err . Error ( ) , tc . err . Error ( ) )
assert . Equals ( t , v . code , tc . err . code )
assert . Equals ( t , v . context , tc . err . context )
default :
t . Errorf ( "unexpected error type: %T" , v )
}
}
} else {
leaf := certChain [ 0 ]
intermediate := certChain [ 1 ]
if assert . Nil ( t , tc . err ) {
assert . Equals ( t , leaf . NotBefore , signOpts . NotBefore . Time ( ) . Truncate ( time . Second ) )
assert . Equals ( t , leaf . NotAfter , signOpts . NotAfter . Time ( ) . Truncate ( time . Second ) )
tmplt := a . config . AuthorityConfig . Template
assert . Equals ( t , fmt . Sprintf ( "%v" , leaf . Subject ) ,
fmt . Sprintf ( "%v" , & pkix . Name {
Country : [ ] string { tmplt . Country } ,
Organization : [ ] string { tmplt . Organization } ,
Locality : [ ] string { tmplt . Locality } ,
StreetAddress : [ ] string { tmplt . StreetAddress } ,
Province : [ ] string { tmplt . Province } ,
CommonName : "smallstep test" ,
} ) )
assert . Equals ( t , leaf . Issuer , intermediate . Subject )
assert . Equals ( t , leaf . SignatureAlgorithm , x509 . ECDSAWithSHA256 )
assert . Equals ( t , leaf . PublicKeyAlgorithm , x509 . ECDSA )
assert . Equals ( t , leaf . ExtKeyUsage ,
[ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageServerAuth , x509 . ExtKeyUsageClientAuth } )
assert . Equals ( t , leaf . DNSNames , [ ] string { "test.smallstep.com" } )
pubBytes , err := x509 . MarshalPKIXPublicKey ( pub )
assert . FatalError ( t , err )
hash := sha1 . Sum ( pubBytes )
assert . Equals ( t , leaf . SubjectKeyId , hash [ : ] )
assert . Equals ( t , leaf . AuthorityKeyId , a . intermediateIdentity . Crt . SubjectKeyId )
// Verify Provisioner OID
found := 0
for _ , ext := range leaf . Extensions {
id := ext . Id . String ( )
if id != stepOIDProvisioner . String ( ) {
continue
}
found ++
val := stepProvisionerASN1 { }
_ , err := asn1 . Unmarshal ( ext . Value , & val )
assert . FatalError ( t , err )
assert . Equals ( t , val . Type , provisionerTypeJWK )
assert . Equals ( t , val . Name , [ ] byte ( p . Name ) )
assert . Equals ( t , val . CredentialID , [ ] byte ( p . Key . KeyID ) )
}
assert . Equals ( t , found , 1 )
realIntermediate , err := x509 . ParseCertificate ( a . intermediateIdentity . Crt . Raw )
assert . FatalError ( t , err )
assert . Equals ( t , intermediate , realIntermediate )
}
}
} )
}
}
func TestRenew ( t * testing . T ) {
pub , _ , err := keys . GenerateDefaultKeyPair ( )
assert . FatalError ( t , err )
a := testAuthority ( t )
a . config . AuthorityConfig . Template = & x509util . ASN1DN {
Country : "Tazmania" ,
Organization : "Acme Co" ,
Locality : "Landscapes" ,
Province : "Sudden Cliffs" ,
StreetAddress : "TNT" ,
CommonName : "renew" ,
}
now := time . Now ( ) . UTC ( )
nb1 := now . Add ( - time . Minute * 7 )
na1 := now
so := & provisioner . Options {
NotBefore : provisioner . NewTimeDuration ( nb1 ) ,
NotAfter : provisioner . NewTimeDuration ( na1 ) ,
}
leaf , err := x509util . NewLeafProfile ( "renew" , a . intermediateIdentity . Crt ,
a . intermediateIdentity . Key ,
x509util . WithNotBeforeAfterDuration ( so . NotBefore . Time ( ) , so . NotAfter . Time ( ) , 0 ) ,
withDefaultASN1DN ( a . config . AuthorityConfig . Template ) ,
x509util . WithPublicKey ( pub ) , x509util . WithHosts ( "test.smallstep.com,test" ) ,
withProvisionerOID ( "Max" , a . config . AuthorityConfig . Provisioners [ 0 ] . ( * provisioner . JWK ) . Key . KeyID ) )
assert . FatalError ( t , err )
crtBytes , err := leaf . CreateCertificate ( )
assert . FatalError ( t , err )
crt , err := x509 . ParseCertificate ( crtBytes )
assert . FatalError ( t , err )
leafNoRenew , err := x509util . NewLeafProfile ( "norenew" , a . intermediateIdentity . Crt ,
a . intermediateIdentity . Key ,
x509util . WithNotBeforeAfterDuration ( so . NotBefore . Time ( ) , so . NotAfter . Time ( ) , 0 ) ,
withDefaultASN1DN ( a . config . AuthorityConfig . Template ) ,
x509util . WithPublicKey ( pub ) , x509util . WithHosts ( "test.smallstep.com,test" ) ,
withProvisionerOID ( "dev" , a . config . AuthorityConfig . Provisioners [ 2 ] . ( * provisioner . JWK ) . Key . KeyID ) ,
)
assert . FatalError ( t , err )
crtBytesNoRenew , err := leafNoRenew . CreateCertificate ( )
assert . FatalError ( t , err )
crtNoRenew , err := x509 . ParseCertificate ( crtBytesNoRenew )
assert . FatalError ( t , err )
type renewTest struct {
auth * Authority
crt * x509 . Certificate
err * apiError
}
tests := map [ string ] func ( ) ( * renewTest , error ) {
"fail-create-cert" : func ( ) ( * renewTest , error ) {
_a := testAuthority ( t )
_a . intermediateIdentity . Key = nil
return & renewTest {
auth : _a ,
crt : crt ,
err : & apiError { errors . New ( "error renewing certificate from existing server certificate" ) ,
http . StatusInternalServerError , apiCtx { } } ,
} , nil
} ,
"fail-unauthorized" : func ( ) ( * renewTest , error ) {
ctx := map [ string ] interface { } {
"serialNumber" : crtNoRenew . SerialNumber . String ( ) ,
}
return & renewTest {
crt : crtNoRenew ,
err : & apiError { errors . New ( "renew: renew is disabled for provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk" ) ,
http . StatusUnauthorized , ctx } ,
} , nil
} ,
"success" : func ( ) ( * renewTest , error ) {
return & renewTest {
auth : a ,
crt : crt ,
} , nil
} ,
"success-new-intermediate" : func ( ) ( * renewTest , error ) {
newRootProfile , err := x509util . NewRootProfile ( "new-root" )
assert . FatalError ( t , err )
newRootBytes , err := newRootProfile . CreateCertificate ( )
assert . FatalError ( t , err )
newRootCrt , err := x509 . ParseCertificate ( newRootBytes )
assert . FatalError ( t , err )
newIntermediateProfile , err := x509util . NewIntermediateProfile ( "new-intermediate" ,
newRootCrt , newRootProfile . SubjectPrivateKey ( ) )
assert . FatalError ( t , err )
newIntermediateBytes , err := newIntermediateProfile . CreateCertificate ( )
assert . FatalError ( t , err )
newIntermediateCrt , err := x509 . ParseCertificate ( newIntermediateBytes )
assert . FatalError ( t , err )
_a := testAuthority ( t )
_a . intermediateIdentity . Key = newIntermediateProfile . SubjectPrivateKey ( )
_a . intermediateIdentity . Crt = newIntermediateCrt
return & renewTest {
auth : _a ,
crt : crt ,
} , nil
} ,
}
for name , genTestCase := range tests {
t . Run ( name , func ( t * testing . T ) {
tc , err := genTestCase ( )
assert . FatalError ( t , err )
var certChain [ ] * x509 . Certificate
if tc . auth != nil {
certChain , err = tc . auth . Renew ( tc . crt )
} else {
certChain , err = a . Renew ( tc . crt )
}
if err != nil {
if assert . NotNil ( t , tc . err ) {
switch v := err . ( type ) {
case * apiError :
assert . HasPrefix ( t , v . err . Error ( ) , tc . err . Error ( ) )
assert . Equals ( t , v . code , tc . err . code )
assert . Equals ( t , v . context , tc . err . context )
default :
t . Errorf ( "unexpected error type: %T" , v )
}
}
} else {
leaf := certChain [ 0 ]
intermediate := certChain [ 1 ]
if assert . Nil ( t , tc . err ) {
assert . Equals ( t , leaf . NotAfter . Sub ( leaf . NotBefore ) , tc . crt . NotAfter . Sub ( crt . NotBefore ) )
assert . True ( t , leaf . NotBefore . After ( now . Add ( - time . Minute ) ) )
assert . True ( t , leaf . NotBefore . Before ( now . Add ( time . Minute ) ) )
expiry := now . Add ( time . Minute * 7 )
assert . True ( t , leaf . NotAfter . After ( expiry . Add ( - time . Minute ) ) )
assert . True ( t , leaf . NotAfter . Before ( expiry . Add ( time . Minute ) ) )
tmplt := a . config . AuthorityConfig . Template
assert . Equals ( t , fmt . Sprintf ( "%v" , leaf . Subject ) ,
fmt . Sprintf ( "%v" , & pkix . Name {
Country : [ ] string { tmplt . Country } ,
Organization : [ ] string { tmplt . Organization } ,
Locality : [ ] string { tmplt . Locality } ,
StreetAddress : [ ] string { tmplt . StreetAddress } ,
Province : [ ] string { tmplt . Province } ,
CommonName : tmplt . CommonName ,
} ) )
assert . Equals ( t , leaf . Issuer , intermediate . Subject )
assert . Equals ( t , leaf . SignatureAlgorithm , x509 . ECDSAWithSHA256 )
assert . Equals ( t , leaf . PublicKeyAlgorithm , x509 . ECDSA )
assert . Equals ( t , leaf . ExtKeyUsage ,
[ ] x509 . ExtKeyUsage { x509 . ExtKeyUsageServerAuth , x509 . ExtKeyUsageClientAuth } )
assert . Equals ( t , leaf . DNSNames , [ ] string { "test.smallstep.com" , "test" } )
pubBytes , err := x509 . MarshalPKIXPublicKey ( pub )
assert . FatalError ( t , err )
hash := sha1 . Sum ( pubBytes )
assert . Equals ( t , leaf . SubjectKeyId , hash [ : ] )
// We did not change the intermediate before renewing.
if a . intermediateIdentity . Crt . SerialNumber == tc . auth . intermediateIdentity . Crt . SerialNumber {
assert . Equals ( t , leaf . AuthorityKeyId , a . intermediateIdentity . Crt . SubjectKeyId )
// Compare extensions: they can be in a different order
for _ , ext1 := range tc . crt . Extensions {
found := false
for _ , ext2 := range leaf . Extensions {
if reflect . DeepEqual ( ext1 , ext2 ) {
found = true
break
}
}
if ! found {
t . Errorf ( "x509 extension %s not found in renewed certificate" , ext1 . Id . String ( ) )
}
}
} else {
// We did change the intermediate before renewing.
assert . Equals ( t , leaf . AuthorityKeyId , tc . auth . intermediateIdentity . Crt . SubjectKeyId )
// Compare extensions: they can be in a different order
for _ , ext1 := range tc . crt . Extensions {
// The authority key id extension should be different b/c the intermediates are different.
if ext1 . Id . Equal ( oidAuthorityKeyIdentifier ) {
for _ , ext2 := range leaf . Extensions {
assert . False ( t , reflect . DeepEqual ( ext1 , ext2 ) )
}
continue
} else {
found := false
for _ , ext2 := range leaf . Extensions {
if reflect . DeepEqual ( ext1 , ext2 ) {
found = true
break
}
}
if ! found {
t . Errorf ( "x509 extension %s not found in renewed certificate" , ext1 . Id . String ( ) )
}
}
}
}
realIntermediate , err := x509 . ParseCertificate ( tc . auth . intermediateIdentity . Crt . Raw )
assert . FatalError ( t , err )
assert . Equals ( t , intermediate , realIntermediate )
}
}
} )
}
}
func TestGetTLSOptions ( t * testing . T ) {
type renewTest struct {
auth * Authority
opts * tlsutil . TLSOptions
}
tests := map [ string ] func ( ) ( * renewTest , error ) {
"default" : func ( ) ( * renewTest , error ) {
a := testAuthority ( t )
return & renewTest { auth : a , opts : & DefaultTLSOptions } , nil
} ,
"non-default" : func ( ) ( * renewTest , error ) {
a := testAuthority ( t )
a . config . TLS = & tlsutil . TLSOptions {
CipherSuites : x509util . CipherSuites {
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305" ,
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" ,
} ,
MinVersion : 1.0 ,
MaxVersion : 1.1 ,
Renegotiation : true ,
}
return & renewTest { auth : a , opts : a . config . TLS } , nil
} ,
}
for name , genTestCase := range tests {
t . Run ( name , func ( t * testing . T ) {
tc , err := genTestCase ( )
assert . FatalError ( t , err )
opts := tc . auth . GetTLSOptions ( )
assert . Equals ( t , opts , tc . opts )
} )
}
}
func TestRevoke ( t * testing . T ) {
reasonCode := 2
reason := "bob was let go"
validIssuer := "step-cli"
validAudience := [ ] string { "https://test.ca.smallstep.com/revoke" }
now := time . Now ( ) . UTC ( )
getCtx := func ( ) map [ string ] interface { } {
return apiCtx {
"serialNumber" : "sn" ,
"reasonCode" : reasonCode ,
"reason" : reason ,
"mTLS" : false ,
"passiveOnly" : false ,
}
}
jwk , err := jose . ParseKey ( "testdata/secrets/step_cli_key_priv.jwk" , jose . WithPassword ( [ ] byte ( "pass" ) ) )
assert . FatalError ( t , err )
sig , err := jose . NewSigner ( jose . SigningKey { Algorithm : jose . ES256 , Key : jwk . Key } ,
( & jose . SignerOptions { } ) . WithType ( "JWT" ) . WithHeader ( "kid" , jwk . KeyID ) )
assert . FatalError ( t , err )
type test struct {
a * Authority
opts * RevokeOptions
err * apiError
}
tests := map [ string ] func ( ) test {
"error/token/authorizeRevoke error" : func ( ) test {
a := testAuthority ( t )
ctx := getCtx ( )
ctx [ "ott" ] = "foo"
return test {
a : a ,
opts : & RevokeOptions {
OTT : "foo" ,
Serial : "sn" ,
ReasonCode : reasonCode ,
Reason : reason ,
} ,
err : & apiError { errors . New ( "revoke: authorizeRevoke: authorizeToken: error parsing token" ) ,
http . StatusUnauthorized , ctx } ,
}
} ,
"error/nil-db" : func ( ) test {
a := testAuthority ( t )
cl := jwt . Claims {
Subject : "sn" ,
Issuer : validIssuer ,
NotBefore : jwt . NewNumericDate ( now ) ,
Expiry : jwt . NewNumericDate ( now . Add ( time . Minute ) ) ,
Audience : validAudience ,
ID : "44" ,
}
raw , err := jwt . Signed ( sig ) . Claims ( cl ) . CompactSerialize ( )
assert . FatalError ( t , err )
ctx := getCtx ( )
ctx [ "ott" ] = raw
ctx [ "tokenID" ] = "44"
ctx [ "provisionerID" ] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc"
return test {
a : a ,
opts : & RevokeOptions {
Serial : "sn" ,
ReasonCode : reasonCode ,
Reason : reason ,
OTT : raw ,
} ,
err : & apiError { errors . New ( "revoke: no persistence layer configured" ) ,
http . StatusNotImplemented , ctx } ,
}
} ,
"error/db-revoke" : func ( ) test {
a := testAuthority ( t )
a . db = & MockAuthDB {
useToken : func ( id , tok string ) ( bool , error ) {
return true , nil
} ,
err : errors . New ( "force" ) ,
}
cl := jwt . Claims {
Subject : "sn" ,
Issuer : validIssuer ,
NotBefore : jwt . NewNumericDate ( now ) ,
Expiry : jwt . NewNumericDate ( now . Add ( time . Minute ) ) ,
Audience : validAudience ,
ID : "44" ,
}
raw , err := jwt . Signed ( sig ) . Claims ( cl ) . CompactSerialize ( )
assert . FatalError ( t , err )
ctx := getCtx ( )
ctx [ "ott" ] = raw
ctx [ "tokenID" ] = "44"
ctx [ "provisionerID" ] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc"
return test {
a : a ,
opts : & RevokeOptions {
Serial : "sn" ,
ReasonCode : reasonCode ,
Reason : reason ,
OTT : raw ,
} ,
err : & apiError { errors . New ( "force" ) ,
http . StatusInternalServerError , ctx } ,
}
} ,
"error/already-revoked" : func ( ) test {
a := testAuthority ( t )
a . db = & MockAuthDB {
useToken : func ( id , tok string ) ( bool , error ) {
return true , nil
} ,
err : db . ErrAlreadyExists ,
}
cl := jwt . Claims {
Subject : "sn" ,
Issuer : validIssuer ,
NotBefore : jwt . NewNumericDate ( now ) ,
Expiry : jwt . NewNumericDate ( now . Add ( time . Minute ) ) ,
Audience : validAudience ,
ID : "44" ,
}
raw , err := jwt . Signed ( sig ) . Claims ( cl ) . CompactSerialize ( )
assert . FatalError ( t , err )
ctx := getCtx ( )
ctx [ "ott" ] = raw
ctx [ "tokenID" ] = "44"
ctx [ "provisionerID" ] = "step-cli:4UELJx8e0aS9m0CH3fZ0EB7D5aUPICb759zALHFejvc"
return test {
a : a ,
opts : & RevokeOptions {
Serial : "sn" ,
ReasonCode : reasonCode ,
Reason : reason ,
OTT : raw ,
} ,
err : & apiError { errors . New ( "revoke: certificate with serial number sn has already been revoked" ) ,
http . StatusBadRequest , ctx } ,
}
} ,
"ok/token" : func ( ) test {
a := testAuthority ( t )
a . db = & MockAuthDB {
useToken : func ( id , tok string ) ( bool , error ) {
return true , nil
} ,
}
cl := jwt . Claims {
Subject : "sn" ,
Issuer : validIssuer ,
NotBefore : jwt . NewNumericDate ( now ) ,
Expiry : jwt . NewNumericDate ( now . Add ( time . Minute ) ) ,
Audience : validAudience ,
ID : "44" ,
}
raw , err := jwt . Signed ( sig ) . Claims ( cl ) . CompactSerialize ( )
assert . FatalError ( t , err )
return test {
a : a ,
opts : & RevokeOptions {
Serial : "sn" ,
ReasonCode : reasonCode ,
Reason : reason ,
OTT : raw ,
} ,
}
} ,
"error/mTLS/authorizeRevoke" : func ( ) test {
a := testAuthority ( t )
a . db = & MockAuthDB { }
crt , err := pemutil . ReadCertificate ( "./testdata/certs/foo.crt" )
assert . FatalError ( t , err )
ctx := getCtx ( )
ctx [ "certificate" ] = base64 . StdEncoding . EncodeToString ( crt . Raw )
ctx [ "mTLS" ] = true
return test {
a : a ,
opts : & RevokeOptions {
Crt : crt ,
Serial : "sn" ,
ReasonCode : reasonCode ,
Reason : reason ,
MTLS : true ,
} ,
err : & apiError { errors . New ( "revoke: authorizeRevoke: serial number in certificate different than body" ) ,
http . StatusUnauthorized , ctx } ,
}
} ,
"ok/mTLS" : func ( ) test {
a := testAuthority ( t )
a . db = & MockAuthDB { }
crt , err := pemutil . ReadCertificate ( "./testdata/certs/foo.crt" )
assert . FatalError ( t , err )
return test {
a : a ,
opts : & RevokeOptions {
Crt : crt ,
Serial : "102012593071130646873265215610956555026" ,
ReasonCode : reasonCode ,
Reason : reason ,
MTLS : true ,
} ,
}
} ,
}
for name , f := range tests {
tc := f ( )
t . Run ( name , func ( t * testing . T ) {
if err := tc . a . Revoke ( context . TODO ( ) , tc . opts ) ; err != nil {
if assert . NotNil ( t , tc . err ) {
switch v := err . ( type ) {
case * apiError :
assert . HasPrefix ( t , v . err . Error ( ) , tc . err . Error ( ) )
assert . Equals ( t , v . code , tc . err . code )
assert . Equals ( t , v . context , tc . err . context )
default :
t . Errorf ( "unexpected error type: %T" , v )
}
}
} else {
assert . Nil ( t , tc . err )
}
} )
}
}