2018-10-05 21:48:36 +00:00
package authority
import (
2019-07-29 19:34:27 +00:00
"context"
2018-10-05 21:48:36 +00:00
"crypto/rand"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
2018-10-26 06:49:23 +00:00
"encoding/asn1"
2019-03-05 08:07:13 +00:00
"encoding/base64"
2019-08-27 00:52:49 +00:00
"encoding/pem"
2018-10-05 21:48:36 +00:00
"fmt"
"net/http"
2019-02-15 00:44:36 +00:00
"reflect"
2018-10-05 21:48:36 +00:00
"testing"
"time"
"github.com/pkg/errors"
"github.com/smallstep/assert"
2019-03-08 03:30:17 +00:00
"github.com/smallstep/certificates/authority/provisioner"
2019-03-05 08:07:13 +00:00
"github.com/smallstep/certificates/db"
2018-10-05 21:48:36 +00:00
"github.com/smallstep/cli/crypto/keys"
2019-03-05 08:07:13 +00:00
"github.com/smallstep/cli/crypto/pemutil"
2018-10-05 21:48:36 +00:00
"github.com/smallstep/cli/crypto/tlsutil"
"github.com/smallstep/cli/crypto/x509util"
2019-03-12 01:15:24 +00:00
"github.com/smallstep/cli/jose"
2019-03-05 08:07:13 +00:00
"gopkg.in/square/go-jose.v2/jwt"
2018-10-05 21:48:36 +00:00
)
2019-03-12 01:47:57 +00:00
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
}
}
2019-02-07 00:26:25 +00:00
func getCSR ( t * testing . T , priv interface { } , opts ... func ( * x509 . CertificateRequest ) ) * x509 . CertificateRequest {
2018-10-05 21:48:36 +00:00
_csr := & x509 . CertificateRequest {
2019-02-07 00:26:25 +00:00
Subject : pkix . Name { CommonName : "smallstep test" } ,
2018-10-05 21:48:36 +00:00
DNSNames : [ ] string { "test.smallstep.com" } ,
}
2019-02-07 00:26:25 +00:00
for _ , opt := range opts {
opt ( _csr )
}
2018-10-05 21:48:36 +00:00
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" ,
2018-10-19 05:26:39 +00:00
CommonName : "test.smallstep.com" ,
2018-10-05 21:48:36 +00:00
}
2018-10-19 05:26:39 +00:00
nb := time . Now ( )
2019-03-08 03:30:17 +00:00
signOpts := provisioner . Options {
2019-03-25 19:35:21 +00:00
NotBefore : provisioner . NewTimeDuration ( nb ) ,
NotAfter : provisioner . NewTimeDuration ( nb . Add ( time . Minute * 5 ) ) ,
2018-10-19 05:26:39 +00:00
}
2018-10-05 21:48:36 +00:00
2019-03-12 01:15:24 +00:00
// Create a token to get test extra opts.
2019-03-08 03:30:17 +00:00
p := a . config . AuthorityConfig . Provisioners [ 1 ] . ( * provisioner . JWK )
2019-03-12 01:15:24 +00:00
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 )
2019-07-29 19:34:27 +00:00
ctx := provisioner . NewContextWithMethod ( context . Background ( ) , provisioner . SignMethod )
extraOpts , err := a . Authorize ( ctx , token )
2019-03-12 01:15:24 +00:00
assert . FatalError ( t , err )
2018-10-26 06:49:23 +00:00
2018-10-05 21:48:36 +00:00
type signTest struct {
2018-10-19 05:26:39 +00:00
auth * Authority
csr * x509 . CertificateRequest
2019-03-08 03:30:17 +00:00
signOpts provisioner . Options
extraOpts [ ] provisioner . SignOption
2018-10-19 05:26:39 +00:00
err * apiError
2018-10-05 21:48:36 +00:00
}
tests := map [ string ] func ( * testing . T ) * signTest {
2019-03-21 01:11:45 +00:00
"fail invalid signature" : func ( t * testing . T ) * signTest {
2018-10-05 21:48:36 +00:00
csr := getCSR ( t , priv )
2019-03-21 01:11:45 +00:00
csr . Signature = [ ] byte ( "foo" )
2018-10-05 21:48:36 +00:00
return & signTest {
2018-10-26 06:49:23 +00:00
auth : a ,
csr : csr ,
2019-03-21 01:11:45 +00:00
extraOpts : extraOpts ,
2018-10-26 06:49:23 +00:00
signOpts : signOpts ,
2019-03-21 01:11:45 +00:00
err : & apiError { errors . New ( "sign: invalid certificate request" ) ,
http . StatusBadRequest ,
2019-07-29 19:34:27 +00:00
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
2018-10-05 21:48:36 +00:00
} ,
}
} ,
2018-10-19 05:26:39 +00:00
"fail invalid extra option" : func ( t * testing . T ) * signTest {
2018-10-05 21:48:36 +00:00
csr := getCSR ( t , priv )
csr . Raw = [ ] byte ( "foo" )
return & signTest {
2018-10-26 06:49:23 +00:00
auth : a ,
csr : csr ,
2019-02-07 00:26:25 +00:00
extraOpts : append ( extraOpts , "42" ) ,
2018-10-26 06:49:23 +00:00
signOpts : signOpts ,
2018-10-19 05:26:39 +00:00
err : & apiError { errors . New ( "sign: invalid extra option type string" ) ,
http . StatusInternalServerError ,
2019-07-29 19:34:27 +00:00
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
2018-10-05 21:48:36 +00:00
} ,
}
} ,
2018-10-19 05:26:39 +00:00
"fail merge default ASN1DN" : func ( t * testing . T ) * signTest {
2018-10-05 21:48:36 +00:00
_a := testAuthority ( t )
_a . config . AuthorityConfig . Template = nil
csr := getCSR ( t , priv )
return & signTest {
2018-10-26 06:49:23 +00:00
auth : _a ,
csr : csr ,
2019-02-07 00:26:25 +00:00
extraOpts : extraOpts ,
2018-10-26 06:49:23 +00:00
signOpts : signOpts ,
2018-10-19 05:26:39 +00:00
err : & apiError { errors . New ( "sign: default ASN1DN template cannot be nil" ) ,
http . StatusInternalServerError ,
2019-07-29 19:34:27 +00:00
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
2018-10-05 21:48:36 +00:00
} ,
}
} ,
2018-10-19 05:26:39 +00:00
"fail create cert" : func ( t * testing . T ) * signTest {
2018-10-05 21:48:36 +00:00
_a := testAuthority ( t )
_a . intermediateIdentity . Key = nil
csr := getCSR ( t , priv )
return & signTest {
2018-10-26 06:49:23 +00:00
auth : _a ,
csr : csr ,
2019-03-12 01:15:24 +00:00
extraOpts : extraOpts ,
2018-10-26 06:49:23 +00:00
signOpts : signOpts ,
2018-10-19 05:26:39 +00:00
err : & apiError { errors . New ( "sign: error creating new leaf certificate" ) ,
http . StatusInternalServerError ,
2019-07-29 19:34:27 +00:00
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
2018-10-19 05:26:39 +00:00
} ,
}
} ,
"fail provisioner duration claim" : func ( t * testing . T ) * signTest {
csr := getCSR ( t , priv )
2019-03-08 03:30:17 +00:00
_signOpts := provisioner . Options {
2019-03-25 19:35:21 +00:00
NotBefore : provisioner . NewTimeDuration ( nb ) ,
NotAfter : provisioner . NewTimeDuration ( nb . Add ( time . Hour * 25 ) ) ,
2018-10-19 05:26:39 +00:00
}
return & signTest {
2018-10-26 06:49:23 +00:00
auth : a ,
csr : csr ,
2019-02-07 00:26:25 +00:00
extraOpts : extraOpts ,
2018-10-26 06:49:23 +00:00
signOpts : _signOpts ,
2018-10-19 05:26:39 +00:00
err : & apiError { errors . New ( "sign: requested duration of 25h0m0s is more than the authorized maximum certificate duration of 24h0m0s" ) ,
http . StatusUnauthorized ,
2019-07-29 19:34:27 +00:00
apiCtx { "csr" : csr , "signOptions" : _signOpts } ,
2018-10-05 21:48:36 +00:00
} ,
}
} ,
2019-02-07 00:26:25 +00:00
"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 ,
2019-03-12 01:15:24 +00:00
err : & apiError { errors . New ( "sign: certificate request does not contain the valid DNS names - got [test.smallstep.com smallstep test], want [test.smallstep.com]" ) ,
2019-02-07 00:26:25 +00:00
http . StatusUnauthorized ,
2019-07-29 19:34:27 +00:00
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
2019-02-07 00:26:25 +00:00
} ,
}
} ,
2019-08-27 00:52:49 +00:00
"fail rsa key too short" : func ( t * testing . T ) * signTest {
shortRSAKeyPEM := ` -- -- - BEGIN CERTIFICATE REQUEST -- -- -
2019-09-05 01:31:09 +00:00
MIIBhDCB7gIBADAZMRcwFQYDVQQDEw5zbWFsbHN0ZXAgdGVzdDCBnzANBgkqhkiG
9 w0BAQEFAAOBjQAwgYkCgYEA5JlgH99HvHHsCD6XTqqYj3bXU2oIlnYGoLVs7IJ4
k205rv5 / YWky2gjdpIv0Tnaf3o57IJ891lB7GiyO5iHIEUv5N9dVzrdUboyzk2uZ
7 JMMNB43CSLB2oNuwJjLeAM / yBzlhRnvpKjrNSfSV + cH54FXdnbFbcTFMStnjqKG
MeECAwEAAaAsMCoGCSqGSIb3DQEJDjEdMBswGQYDVR0RBBIwEIIOc21hbGxzdGVw
IHRlc3QwDQYJKoZIhvcNAQELBQADgYEAKwsbr8Zfcq05DgOoJ //cXMFK1SP8ktRU
N2 ++ E8Ww0Tet9oyNRArqxxS / UyVio63D3wynzRAB25PFGpYG1cN4b81Gv / foFUT6
W5kR63lNVHBHgQmv5mA8YFsfrJHstaz5k727v2LMHEYIf5 / 3i 16 d5zhuxUoaPTYr
ZYtQ9Ot36qc =
2019-08-27 00:52:49 +00:00
-- -- - 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 ,
2019-09-05 21:06:01 +00:00
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
2019-08-27 00:52:49 +00:00
} ,
}
} ,
2019-03-05 08:07:13 +00:00
"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 ,
2019-07-29 19:34:27 +00:00
apiCtx { "csr" : csr , "signOptions" : signOpts } }
2019-03-05 08:07:13 +00:00
} ,
}
return & signTest {
auth : _a ,
csr : csr ,
extraOpts : extraOpts ,
signOpts : signOpts ,
err : & apiError { errors . New ( "sign: error storing certificate in db: force" ) ,
http . StatusInternalServerError ,
2019-07-29 19:34:27 +00:00
apiCtx { "csr" : csr , "signOptions" : signOpts } ,
2019-03-05 08:07:13 +00:00
} ,
}
} ,
2018-10-19 05:26:39 +00:00
"ok" : func ( t * testing . T ) * signTest {
2018-10-05 21:48:36 +00:00
csr := getCSR ( t , priv )
2019-03-05 08:07:13 +00:00
_a := testAuthority ( t )
_a . db = & MockAuthDB {
storeCertificate : func ( crt * x509 . Certificate ) error {
assert . Equals ( t , crt . Subject . CommonName , "smallstep test" )
return nil
} ,
}
2018-10-05 21:48:36 +00:00
return & signTest {
2018-10-26 06:49:23 +00:00
auth : a ,
csr : csr ,
2019-02-07 00:26:25 +00:00
extraOpts : extraOpts ,
2018-10-26 06:49:23 +00:00
signOpts : signOpts ,
2018-10-05 21:48:36 +00:00
}
} ,
}
for name , genTestCase := range tests {
t . Run ( name , func ( t * testing . T ) {
tc := genTestCase ( t )
2019-10-09 19:57:12 +00:00
certChain , err := tc . auth . Sign ( tc . csr , tc . signOpts , tc . extraOpts ... )
2018-10-05 21:48:36 +00:00
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 {
2019-10-09 19:57:12 +00:00
leaf := certChain [ 0 ]
intermediate := certChain [ 1 ]
2018-10-05 21:48:36 +00:00
if assert . Nil ( t , tc . err ) {
2019-03-25 19:35:21 +00:00
assert . Equals ( t , leaf . NotBefore , signOpts . NotBefore . Time ( ) . Truncate ( time . Second ) )
assert . Equals ( t , leaf . NotAfter , signOpts . NotAfter . Time ( ) . Truncate ( time . Second ) )
2018-10-05 21:48:36 +00:00
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 } ,
2019-02-07 00:26:25 +00:00
CommonName : "smallstep test" ,
2018-10-05 21:48:36 +00:00
} ) )
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 } )
2018-10-19 05:26:39 +00:00
assert . Equals ( t , leaf . DNSNames , [ ] string { "test.smallstep.com" } )
2018-10-05 21:48:36 +00:00
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 )
2018-10-26 06:49:23 +00:00
// Verify Provisioner OID
found := 0
for _ , ext := range leaf . Extensions {
id := ext . Id . String ( )
2018-10-26 21:24:16 +00:00
if id != stepOIDProvisioner . String ( ) {
2018-10-26 06:49:23 +00:00
continue
}
found ++
2018-10-26 21:24:16 +00:00
val := stepProvisionerASN1 { }
_ , err := asn1 . Unmarshal ( ext . Value , & val )
2018-10-26 06:49:23 +00:00
assert . FatalError ( t , err )
2018-10-26 21:24:16 +00:00
assert . Equals ( t , val . Type , provisionerTypeJWK )
2018-10-30 01:00:30 +00:00
assert . Equals ( t , val . Name , [ ] byte ( p . Name ) )
2018-10-26 21:24:16 +00:00
assert . Equals ( t , val . CredentialID , [ ] byte ( p . Key . KeyID ) )
2018-10-26 06:49:23 +00:00
}
2018-10-26 21:24:16 +00:00
assert . Equals ( t , found , 1 )
2018-10-26 06:49:23 +00:00
2018-10-05 21:48:36 +00:00
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
2019-03-08 03:30:17 +00:00
so := & provisioner . Options {
2019-03-25 19:35:21 +00:00
NotBefore : provisioner . NewTimeDuration ( nb1 ) ,
NotAfter : provisioner . NewTimeDuration ( na1 ) ,
2018-10-05 21:48:36 +00:00
}
leaf , err := x509util . NewLeafProfile ( "renew" , a . intermediateIdentity . Crt ,
a . intermediateIdentity . Key ,
2019-03-25 19:35:21 +00:00
x509util . WithNotBeforeAfterDuration ( so . NotBefore . Time ( ) , so . NotAfter . Time ( ) , 0 ) ,
2018-10-05 21:48:36 +00:00
withDefaultASN1DN ( a . config . AuthorityConfig . Template ) ,
2019-02-15 00:44:36 +00:00
x509util . WithPublicKey ( pub ) , x509util . WithHosts ( "test.smallstep.com,test" ) ,
2019-03-08 03:30:17 +00:00
withProvisionerOID ( "Max" , a . config . AuthorityConfig . Provisioners [ 0 ] . ( * provisioner . JWK ) . Key . KeyID ) )
2018-10-05 21:48:36 +00:00
assert . FatalError ( t , err )
crtBytes , err := leaf . CreateCertificate ( )
assert . FatalError ( t , err )
crt , err := x509 . ParseCertificate ( crtBytes )
assert . FatalError ( t , err )
2018-11-01 22:43:24 +00:00
leafNoRenew , err := x509util . NewLeafProfile ( "norenew" , a . intermediateIdentity . Crt ,
a . intermediateIdentity . Key ,
2019-03-25 19:35:21 +00:00
x509util . WithNotBeforeAfterDuration ( so . NotBefore . Time ( ) , so . NotAfter . Time ( ) , 0 ) ,
2018-11-01 22:43:24 +00:00
withDefaultASN1DN ( a . config . AuthorityConfig . Template ) ,
x509util . WithPublicKey ( pub ) , x509util . WithHosts ( "test.smallstep.com,test" ) ,
2019-03-08 03:30:17 +00:00
withProvisionerOID ( "dev" , a . config . AuthorityConfig . Provisioners [ 2 ] . ( * provisioner . JWK ) . Key . KeyID ) ,
2018-11-01 22:43:24 +00:00
)
assert . FatalError ( t , err )
crtBytesNoRenew , err := leafNoRenew . CreateCertificate ( )
assert . FatalError ( t , err )
crtNoRenew , err := x509 . ParseCertificate ( crtBytesNoRenew )
assert . FatalError ( t , err )
2018-10-05 21:48:36 +00:00
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" ) ,
2019-07-29 19:34:27 +00:00
http . StatusInternalServerError , apiCtx { } } ,
2018-10-05 21:48:36 +00:00
} , nil
} ,
2018-11-01 22:43:24 +00:00
"fail-unauthorized" : func ( ) ( * renewTest , error ) {
ctx := map [ string ] interface { } {
"serialNumber" : crtNoRenew . SerialNumber . String ( ) ,
}
return & renewTest {
crt : crtNoRenew ,
2019-03-05 08:07:13 +00:00
err : & apiError { errors . New ( "renew: renew is disabled for provisioner dev:IMi94WBNI6gP5cNHXlZYNUzvMjGdHyBRmFoo-lCEaqk" ) ,
2018-11-01 22:43:24 +00:00
http . StatusUnauthorized , ctx } ,
} , nil
} ,
2018-10-05 21:48:36 +00:00
"success" : func ( ) ( * renewTest , error ) {
return & renewTest {
2019-02-15 03:17:42 +00:00
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 )
2019-03-21 00:41:37 +00:00
newRootCrt , err := x509 . ParseCertificate ( newRootBytes )
2019-02-15 03:17:42 +00:00
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 )
2019-03-21 00:41:37 +00:00
newIntermediateCrt , err := x509 . ParseCertificate ( newIntermediateBytes )
2019-02-15 03:17:42 +00:00
assert . FatalError ( t , err )
_a := testAuthority ( t )
_a . intermediateIdentity . Key = newIntermediateProfile . SubjectPrivateKey ( )
_a . intermediateIdentity . Crt = newIntermediateCrt
return & renewTest {
auth : _a ,
crt : crt ,
2018-10-05 21:48:36 +00:00
} , nil
} ,
}
for name , genTestCase := range tests {
t . Run ( name , func ( t * testing . T ) {
tc , err := genTestCase ( )
assert . FatalError ( t , err )
2019-10-09 19:57:12 +00:00
var certChain [ ] * x509 . Certificate
2018-10-05 21:48:36 +00:00
if tc . auth != nil {
2019-10-09 19:57:12 +00:00
certChain , err = tc . auth . Renew ( tc . crt )
2018-10-05 21:48:36 +00:00
} else {
2019-10-09 19:57:12 +00:00
certChain , err = a . Renew ( tc . crt )
2018-10-05 21:48:36 +00:00
}
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 {
2019-10-09 19:57:12 +00:00
leaf := certChain [ 0 ]
intermediate := certChain [ 1 ]
2018-10-05 21:48:36 +00:00
if assert . Nil ( t , tc . err ) {
2019-02-15 03:17:42 +00:00
assert . Equals ( t , leaf . NotAfter . Sub ( leaf . NotBefore ) , tc . crt . NotAfter . Sub ( crt . NotBefore ) )
2018-10-05 21:48:36 +00:00
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 [ : ] )
2019-02-15 03:17:42 +00:00
// 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 ( ) )
2019-02-15 00:44:36 +00:00
}
}
2019-02-15 03:17:42 +00:00
} 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 ( ) )
}
}
2019-02-15 00:44:36 +00:00
}
}
2019-02-15 03:17:42 +00:00
realIntermediate , err := x509 . ParseCertificate ( tc . auth . intermediateIdentity . Crt . Raw )
assert . FatalError ( t , err )
assert . Equals ( t , intermediate , realIntermediate )
2018-10-05 21:48:36 +00:00
}
}
} )
}
}
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 )
} )
}
}
2019-03-05 08:07:13 +00:00
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 { } {
2019-07-29 19:34:27 +00:00
return apiCtx {
2019-03-05 08:07:13 +00:00
"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 )
2019-05-02 22:26:18 +00:00
a . db = & MockAuthDB {
useToken : func ( id , tok string ) ( bool , error ) {
return true , nil
} ,
err : errors . New ( "force" ) ,
}
2019-03-05 08:07:13 +00:00
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 )
2019-05-02 22:26:18 +00:00
a . db = & MockAuthDB {
useToken : func ( id , tok string ) ( bool , error ) {
return true , nil
} ,
err : db . ErrAlreadyExists ,
}
2019-03-05 08:07:13 +00:00
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 )
2019-05-02 22:26:18 +00:00
a . db = & MockAuthDB {
useToken : func ( id , tok string ) ( bool , error ) {
return true , nil
} ,
}
2019-03-05 08:07:13 +00:00
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 ) {
2019-11-15 02:07:16 +00:00
if err := tc . a . Revoke ( context . TODO ( ) , tc . opts ) ; err != nil {
2019-03-05 08:07:13 +00:00
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 )
}
} )
}
}