@@ -13,6 +13,7 @@ import (
13
13
"crypto/tls"
14
14
"crypto/x509"
15
15
"crypto/x509/pkix"
16
+ "encoding/base64"
16
17
"encoding/json"
17
18
"encoding/pem"
18
19
"fmt"
@@ -34,6 +35,7 @@ import (
34
35
"github.com/smallstep/certificates/logging"
35
36
"github.com/smallstep/certificates/templates"
36
37
"go.step.sm/crypto/jose"
38
+ "go.step.sm/crypto/x509util"
37
39
"golang.org/x/crypto/ssh"
38
40
)
39
41
@@ -171,6 +173,7 @@ type mockAuthority struct {
171
173
ret1 , ret2 interface {}
172
174
err error
173
175
authorizeSign func (ott string ) ([]provisioner.SignOption , error )
176
+ authorizeRenewToken func (ctx context.Context , ott string ) (* x509.Certificate , error )
174
177
getTLSOptions func () * authority.TLSOptions
175
178
root func (shasum string ) (* x509.Certificate , error )
176
179
sign func (cr * x509.CertificateRequest , opts provisioner.SignOptions , signOpts ... provisioner.SignOption ) ([]* x509.Certificate , error )
@@ -208,6 +211,13 @@ func (m *mockAuthority) AuthorizeSign(ott string) ([]provisioner.SignOption, err
208
211
return m .ret1 .([]provisioner.SignOption ), m .err
209
212
}
210
213
214
+ func (m * mockAuthority ) AuthorizeRenewToken (ctx context.Context , ott string ) (* x509.Certificate , error ) {
215
+ if m .authorizeRenewToken != nil {
216
+ return m .authorizeRenewToken (ctx , ott )
217
+ }
218
+ return m .ret1 .(* x509.Certificate ), m .err
219
+ }
220
+
211
221
func (m * mockAuthority ) GetTLSOptions () * authority.TLSOptions {
212
222
if m .getTLSOptions != nil {
213
223
return m .getTLSOptions ()
@@ -920,48 +930,141 @@ func Test_caHandler_Renew(t *testing.T) {
920
930
cs := & tls.ConnectionState {
921
931
PeerCertificates : []* x509.Certificate {parseCertificate (certPEM )},
922
932
}
933
+
934
+ // Prepare root and leaf for renew after expiry test.
935
+ now := time .Now ()
936
+ rootPub , rootPriv , err := ed25519 .GenerateKey (rand .Reader )
937
+ if err != nil {
938
+ t .Fatal (err )
939
+ }
940
+ leafPub , leafPriv , err := ed25519 .GenerateKey (rand .Reader )
941
+ if err != nil {
942
+ t .Fatal (err )
943
+ }
944
+ root := & x509.Certificate {
945
+ Subject : pkix.Name {CommonName : "Test Root CA" },
946
+ PublicKey : rootPub ,
947
+ KeyUsage : x509 .KeyUsageCertSign ,
948
+ BasicConstraintsValid : true ,
949
+ IsCA : true ,
950
+ NotBefore : now .Add (- 2 * time .Hour ),
951
+ NotAfter : now .Add (time .Hour ),
952
+ }
953
+ root , err = x509util .CreateCertificate (root , root , rootPub , rootPriv )
954
+ if err != nil {
955
+ t .Fatal (err )
956
+ }
957
+ expiredLeaf := & x509.Certificate {
958
+ Subject : pkix.Name {CommonName : "Leaf certificate" },
959
+ PublicKey : leafPub ,
960
+ KeyUsage : x509 .KeyUsageDigitalSignature ,
961
+ ExtKeyUsage : []x509.ExtKeyUsage {x509 .ExtKeyUsageServerAuth , x509 .ExtKeyUsageClientAuth },
962
+ NotBefore : now .Add (- time .Hour ),
963
+ NotAfter : now .Add (- time .Minute ),
964
+ EmailAddresses : []string {"test@example.org" },
965
+ }
966
+ expiredLeaf , err = x509util .CreateCertificate (expiredLeaf , root , leafPub , rootPriv )
967
+ if err != nil {
968
+ t .Fatal (err )
969
+ }
970
+
971
+ // Generate renew after expiry token
972
+ so := new (jose.SignerOptions )
973
+ so .WithType ("JWT" )
974
+ so .WithHeader ("x5cInsecure" , []string {base64 .StdEncoding .EncodeToString (expiredLeaf .Raw )})
975
+ sig , err := jose .NewSigner (jose.SigningKey {Algorithm : jose .EdDSA , Key : leafPriv }, so )
976
+ if err != nil {
977
+ t .Fatal (err )
978
+ }
979
+ generateX5cToken := func (claims jose.Claims ) string {
980
+ s , err := jose .Signed (sig ).Claims (claims ).CompactSerialize ()
981
+ if err != nil {
982
+ t .Fatal (err )
983
+ }
984
+ return s
985
+ }
986
+
923
987
tests := []struct {
924
988
name string
925
989
tls * tls.ConnectionState
990
+ header http.Header
926
991
cert * x509.Certificate
927
992
root * x509.Certificate
928
993
err error
929
994
statusCode int
930
995
}{
931
- {"ok" , cs , parseCertificate (certPEM ), parseCertificate (rootPEM ), nil , http .StatusCreated },
932
- {"no tls" , nil , nil , nil , nil , http .StatusBadRequest },
933
- {"no peer certificates" , & tls.ConnectionState {}, nil , nil , nil , http .StatusBadRequest },
934
- {"renew error" , cs , nil , nil , errs .Forbidden ("an error" ), http .StatusForbidden },
996
+ {"ok" , cs , nil , parseCertificate (certPEM ), parseCertificate (rootPEM ), nil , http .StatusCreated },
997
+ {"ok renew after expiry" , & tls.ConnectionState {}, http.Header {
998
+ "Authorization" : []string {"Bearer " + generateX5cToken (jose.Claims {
999
+ NotBefore : jose .NewNumericDate (now ), Expiry : jose .NewNumericDate (now .Add (5 * time .Minute )),
1000
+ })},
1001
+ }, expiredLeaf , root , nil , http .StatusCreated },
1002
+ {"no tls" , nil , nil , nil , nil , nil , http .StatusBadRequest },
1003
+ {"no peer certificates" , & tls.ConnectionState {}, nil , nil , nil , nil , http .StatusBadRequest },
1004
+ {"renew error" , cs , nil , nil , nil , errs .Forbidden ("an error" ), http .StatusForbidden },
1005
+ {"fail expired token" , & tls.ConnectionState {}, http.Header {
1006
+ "Authorization" : []string {"Bearer " + generateX5cToken (jose.Claims {
1007
+ NotBefore : jose .NewNumericDate (now .Add (- time .Hour )), Expiry : jose .NewNumericDate (now .Add (- time .Minute )),
1008
+ })},
1009
+ }, expiredLeaf , root , errs .Forbidden ("an error" ), http .StatusUnauthorized },
1010
+ {"fail invalid root" , & tls.ConnectionState {}, http.Header {
1011
+ "Authorization" : []string {"Bearer " + generateX5cToken (jose.Claims {
1012
+ NotBefore : jose .NewNumericDate (now .Add (- time .Hour )), Expiry : jose .NewNumericDate (now .Add (- time .Minute )),
1013
+ })},
1014
+ }, expiredLeaf , parseCertificate (rootPEM ), errs .Forbidden ("an error" ), http .StatusUnauthorized },
935
1015
}
936
1016
937
- expected := []byte (`{"crt":"` + strings .ReplaceAll (certPEM , "\n " , `\n` ) + `\n","ca":"` + strings .ReplaceAll (rootPEM , "\n " , `\n` ) + `\n","certChain":["` + strings .ReplaceAll (certPEM , "\n " , `\n` ) + `\n","` + strings .ReplaceAll (rootPEM , "\n " , `\n` ) + `\n"]}` )
938
-
939
1017
for _ , tt := range tests {
940
1018
t .Run (tt .name , func (t * testing.T ) {
941
1019
h := New (& mockAuthority {
942
1020
ret1 : tt .cert , ret2 : tt .root , err : tt .err ,
1021
+ authorizeRenewToken : func (ctx context.Context , ott string ) (* x509.Certificate , error ) {
1022
+ jwt , chain , err := jose .ParseX5cInsecure (ott , []* x509.Certificate {tt .root })
1023
+ if err != nil {
1024
+ return nil , errs .Unauthorized (err .Error ())
1025
+ }
1026
+ var claims jose.Claims
1027
+ if err := jwt .Claims (chain [0 ][0 ].PublicKey , & claims ); err != nil {
1028
+ return nil , errs .Unauthorized (err .Error ())
1029
+ }
1030
+ if err := claims .ValidateWithLeeway (jose.Expected {
1031
+ Time : now ,
1032
+ }, time .Minute ); err != nil {
1033
+ return nil , errs .Unauthorized (err .Error ())
1034
+ }
1035
+ return chain [0 ][0 ], nil
1036
+ },
943
1037
getTLSOptions : func () * authority.TLSOptions {
944
1038
return nil
945
1039
},
946
1040
}).(* caHandler )
947
1041
req := httptest .NewRequest ("POST" , "http://example.com/renew" , nil )
948
1042
req .TLS = tt .tls
1043
+ req .Header = tt .header
949
1044
w := httptest .NewRecorder ()
950
1045
h .Renew (logging .NewResponseLogger (w ), req )
951
- res := w .Result ()
952
1046
953
- if res .StatusCode != tt .statusCode {
954
- t .Errorf ("caHandler.Renew StatusCode = %d, wants %d" , res .StatusCode , tt .statusCode )
955
- }
1047
+ res := w .Result ()
1048
+ defer res .Body .Close ()
956
1049
957
1050
body , err := io .ReadAll (res .Body )
958
- res .Body .Close ()
959
1051
if err != nil {
960
1052
t .Errorf ("caHandler.Renew unexpected error = %v" , err )
961
1053
}
1054
+ if res .StatusCode != tt .statusCode {
1055
+ t .Errorf ("caHandler.Renew StatusCode = %d, wants %d" , res .StatusCode , tt .statusCode )
1056
+ t .Errorf ("%s" , body )
1057
+ }
1058
+
962
1059
if tt .statusCode < http .StatusBadRequest {
1060
+ expected := []byte (`{"crt":"` + strings .ReplaceAll (string (pem .EncodeToMemory (& pem.Block {Type : "CERTIFICATE" , Bytes : tt .cert .Raw })), "\n " , `\n` ) + `",` +
1061
+ `"ca":"` + strings .ReplaceAll (string (pem .EncodeToMemory (& pem.Block {Type : "CERTIFICATE" , Bytes : tt .root .Raw })), "\n " , `\n` ) + `",` +
1062
+ `"certChain":["` +
1063
+ strings .ReplaceAll (string (pem .EncodeToMemory (& pem.Block {Type : "CERTIFICATE" , Bytes : tt .cert .Raw })), "\n " , `\n` ) + `","` +
1064
+ strings .ReplaceAll (string (pem .EncodeToMemory (& pem.Block {Type : "CERTIFICATE" , Bytes : tt .root .Raw })), "\n " , `\n` ) + `"]}` )
1065
+
963
1066
if ! bytes .Equal (bytes .TrimSpace (body ), expected ) {
964
- t .Errorf ("caHandler.Root Body = %s, wants %s" , body , expected )
1067
+ t .Errorf ("caHandler.Root Body = \n %s, wants \n %s" , body , expected )
965
1068
}
966
1069
}
967
1070
})
0 commit comments