2019-08-06 01:35:00 +00:00
package authority
import (
2019-12-12 02:21:20 +00:00
"context"
2019-08-06 01:35:00 +00:00
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
2019-12-20 21:30:05 +00:00
"crypto/x509"
2019-10-15 00:10:47 +00:00
"encoding/base64"
2022-03-30 08:22:22 +00:00
"errors"
2019-08-06 01:35:00 +00:00
"fmt"
2019-12-20 21:30:05 +00:00
"net/http"
2019-10-15 00:10:47 +00:00
"reflect"
2019-08-06 01:35:00 +00:00
"testing"
"time"
2022-03-30 08:22:22 +00:00
"go.step.sm/crypto/jose"
"go.step.sm/crypto/sshutil"
"golang.org/x/crypto/ssh"
2019-08-06 01:35:00 +00:00
"github.com/smallstep/assert"
2022-03-30 08:22:22 +00:00
"github.com/smallstep/certificates/api/render"
2022-04-24 11:11:32 +00:00
"github.com/smallstep/certificates/authority/policy"
2019-08-06 01:35:00 +00:00
"github.com/smallstep/certificates/authority/provisioner"
2019-10-15 19:18:29 +00:00
"github.com/smallstep/certificates/db"
2019-10-15 00:10:47 +00:00
"github.com/smallstep/certificates/templates"
2019-08-06 01:35:00 +00:00
)
type sshTestModifier ssh . Certificate
2020-08-04 01:36:05 +00:00
func ( m sshTestModifier ) Modify ( cert * ssh . Certificate , _ provisioner . SignSSHOptions ) error {
2019-08-06 01:35:00 +00:00
if m . CertType != 0 {
cert . CertType = m . CertType
}
if m . KeyId != "" {
cert . KeyId = m . KeyId
}
if m . ValidAfter != 0 {
cert . ValidAfter = m . ValidAfter
}
if m . ValidBefore != 0 {
cert . ValidBefore = m . ValidBefore
}
if len ( m . ValidPrincipals ) != 0 {
cert . ValidPrincipals = m . ValidPrincipals
}
if m . Permissions . CriticalOptions != nil {
cert . Permissions . CriticalOptions = m . Permissions . CriticalOptions
}
if m . Permissions . Extensions != nil {
cert . Permissions . Extensions = m . Permissions . Extensions
}
return nil
}
type sshTestCertModifier string
2020-08-04 01:36:05 +00:00
func ( m sshTestCertModifier ) Modify ( cert * ssh . Certificate , opts provisioner . SignSSHOptions ) error {
2019-08-06 01:35:00 +00:00
if m == "" {
return nil
}
2022-02-03 20:23:02 +00:00
return errors . New ( string ( m ) )
2019-08-06 01:35:00 +00:00
}
type sshTestCertValidator string
2020-07-23 01:24:45 +00:00
func ( v sshTestCertValidator ) Valid ( crt * ssh . Certificate , opts provisioner . SignSSHOptions ) error {
2019-08-06 01:35:00 +00:00
if v == "" {
return nil
}
2022-02-03 20:23:02 +00:00
return errors . New ( string ( v ) )
2019-08-06 01:35:00 +00:00
}
type sshTestOptionsValidator string
2020-07-23 01:24:45 +00:00
func ( v sshTestOptionsValidator ) Valid ( opts provisioner . SignSSHOptions ) error {
2019-08-06 01:35:00 +00:00
if v == "" {
return nil
}
2022-02-03 20:23:02 +00:00
return errors . New ( string ( v ) )
2019-08-06 01:35:00 +00:00
}
type sshTestOptionsModifier string
2020-08-04 01:36:05 +00:00
func ( m sshTestOptionsModifier ) Modify ( cert * ssh . Certificate , opts provisioner . SignSSHOptions ) error {
if m == "" {
return nil
}
2022-02-03 20:23:02 +00:00
return errors . New ( string ( m ) )
2019-08-06 01:35:00 +00:00
}
2021-09-28 22:07:09 +00:00
func TestAuthority_initHostOnly ( t * testing . T ) {
auth := testAuthority ( t , func ( a * Authority ) error {
a . config . SSH . UserKey = ""
return nil
} )
// Check keys
keys , err := auth . GetSSHRoots ( context . Background ( ) )
assert . NoError ( t , err )
assert . Len ( t , 1 , keys . HostKeys )
assert . Len ( t , 0 , keys . UserKeys )
// Check templates, user templates should work fine.
_ , err = auth . GetSSHConfig ( context . Background ( ) , "user" , nil )
assert . NoError ( t , err )
_ , err = auth . GetSSHConfig ( context . Background ( ) , "host" , map [ string ] string {
"Certificate" : "ssh_host_ecdsa_key-cert.pub" ,
"Key" : "ssh_host_ecdsa_key" ,
} )
assert . Error ( t , err )
}
func TestAuthority_initUserOnly ( t * testing . T ) {
auth := testAuthority ( t , func ( a * Authority ) error {
a . config . SSH . HostKey = ""
return nil
} )
// Check keys
keys , err := auth . GetSSHRoots ( context . Background ( ) )
assert . NoError ( t , err )
assert . Len ( t , 0 , keys . HostKeys )
assert . Len ( t , 1 , keys . UserKeys )
// Check templates, host templates should work fine.
_ , err = auth . GetSSHConfig ( context . Background ( ) , "host" , map [ string ] string {
"Certificate" : "ssh_host_ecdsa_key-cert.pub" ,
"Key" : "ssh_host_ecdsa_key" ,
} )
assert . NoError ( t , err )
_ , err = auth . GetSSHConfig ( context . Background ( ) , "user" , nil )
assert . Error ( t , err )
}
2019-08-06 01:35:00 +00:00
func TestAuthority_SignSSH ( t * testing . T ) {
key , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
pub , err := ssh . NewPublicKey ( key . Public ( ) )
assert . FatalError ( t , err )
signKey , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
2019-09-28 02:05:53 +00:00
signer , err := ssh . NewSignerFromKey ( signKey )
assert . FatalError ( t , err )
2019-08-06 01:35:00 +00:00
userOptions := sshTestModifier {
CertType : ssh . UserCert ,
}
hostOptions := sshTestModifier {
CertType : ssh . HostCert ,
}
2020-08-04 01:36:05 +00:00
userTemplate , err := provisioner . TemplateSSHOptions ( nil , sshutil . CreateTemplateData ( sshutil . UserCert , "key-id" , nil ) )
assert . FatalError ( t , err )
hostTemplate , err := provisioner . TemplateSSHOptions ( nil , sshutil . CreateTemplateData ( sshutil . HostCert , "key-id" , nil ) )
assert . FatalError ( t , err )
userTemplateWithUser , err := provisioner . TemplateSSHOptions ( nil , sshutil . CreateTemplateData ( sshutil . UserCert , "key-id" , [ ] string { "user" } ) )
assert . FatalError ( t , err )
hostTemplateWithHosts , err := provisioner . TemplateSSHOptions ( nil , sshutil . CreateTemplateData ( sshutil . HostCert , "key-id" , [ ] string { "foo.test.com" , "bar.test.com" } ) )
assert . FatalError ( t , err )
2022-04-24 11:11:32 +00:00
userTemplateWithRoot , err := provisioner . TemplateSSHOptions ( nil , sshutil . CreateTemplateData ( sshutil . UserCert , "key-id" , [ ] string { "root" } ) )
assert . FatalError ( t , err )
hostTemplateWithExampleDotCom , err := provisioner . TemplateSSHOptions ( nil , sshutil . CreateTemplateData ( sshutil . HostCert , "key-id" , [ ] string { "example.com" } ) )
assert . FatalError ( t , err )
badUserTemplate , err := provisioner . TemplateSSHOptions ( nil , sshutil . CreateTemplateData ( sshutil . UserCert , "key-id" , [ ] string { "127.0.0.1" } ) )
assert . FatalError ( t , err )
badHostTemplate , err := provisioner . TemplateSSHOptions ( nil , sshutil . CreateTemplateData ( sshutil . HostCert , "key-id" , [ ] string { "host...local" } ) )
assert . FatalError ( t , err )
2020-08-04 01:51:47 +00:00
userCustomTemplate , err := provisioner . TemplateSSHOptions ( & provisioner . Options {
SSH : & provisioner . SSHOptions { Template : ` {
"type" : "{{ .Type }}" ,
"keyId" : "{{ .KeyID }}" ,
"principals" : { { append . Principals "admin" | toJson } } ,
"extensions" : { { set . Extensions "login@github.com" . Insecure . User . username | toJson } } ,
"criticalOptions" : { { toJson . CriticalOptions } }
} ` } ,
} , sshutil . CreateTemplateData ( sshutil . UserCert , "key-id" , [ ] string { "user" } ) )
assert . FatalError ( t , err )
2022-09-30 00:16:26 +00:00
enrichTemplateData := sshutil . CreateTemplateData ( sshutil . UserCert , "key-id" , [ ] string { "user" } )
enrichTemplate , err := provisioner . TemplateSSHOptions ( & provisioner . Options {
SSH : & provisioner . SSHOptions { Template : ` {
"type" : "{{ .Type }}" ,
"keyId" : "{{ .KeyID }}" ,
"principals" : { { toJson . Webhooks . people . role } } ,
"extensions" : { { set . Extensions "login@github.com" . Insecure . User . username | toJson } } ,
"criticalOptions" : { { toJson . CriticalOptions } }
} ` } ,
} , enrichTemplateData )
assert . FatalError ( t , err )
2020-08-04 01:51:47 +00:00
userFailTemplate , err := provisioner . TemplateSSHOptions ( & provisioner . Options {
SSH : & provisioner . SSHOptions { Template : ` {{ fail "an error" }} ` } ,
} , sshutil . CreateTemplateData ( sshutil . UserCert , "key-id" , [ ] string { "user" } ) )
assert . FatalError ( t , err )
2022-01-12 10:15:39 +00:00
userJSONSyntaxErrorTemplateFile , err := provisioner . TemplateSSHOptions ( & provisioner . Options {
SSH : & provisioner . SSHOptions { TemplateFile : "./testdata/templates/badjsonsyntax.tpl" } ,
} , sshutil . CreateTemplateData ( sshutil . UserCert , "key-id" , [ ] string { "user" } ) )
assert . FatalError ( t , err )
userJSONValueErrorTemplateFile , err := provisioner . TemplateSSHOptions ( & provisioner . Options {
SSH : & provisioner . SSHOptions { TemplateFile : "./testdata/templates/badjsonvalue.tpl" } ,
2022-01-10 14:49:37 +00:00
} , sshutil . CreateTemplateData ( sshutil . UserCert , "key-id" , [ ] string { "user" } ) )
assert . FatalError ( t , err )
2020-08-04 01:36:05 +00:00
2022-04-26 11:12:16 +00:00
userPolicyOptions := & policy . Options {
SSH : & policy . SSHPolicyOptions {
User : & policy . SSHUserCertificateOptions {
AllowedNames : & policy . SSHNameOptions {
Principals : [ ] string { "user" } ,
} ,
2022-04-24 11:11:32 +00:00
} ,
} ,
2022-04-26 11:12:16 +00:00
}
userPolicy , err := policy . New ( userPolicyOptions )
assert . FatalError ( t , err )
hostPolicyOptions := & policy . Options {
SSH : & policy . SSHPolicyOptions {
Host : & policy . SSHHostCertificateOptions {
AllowedNames : & policy . SSHNameOptions {
DNSDomains : [ ] string { "*.test.com" } ,
} ,
2022-04-24 11:11:32 +00:00
} ,
} ,
}
2022-04-26 11:12:16 +00:00
hostPolicy , err := policy . New ( hostPolicyOptions )
2022-04-24 11:11:32 +00:00
assert . FatalError ( t , err )
2019-08-06 01:35:00 +00:00
now := time . Now ( )
type fields struct {
2019-09-28 02:05:53 +00:00
sshCAUserCertSignKey ssh . Signer
sshCAHostCertSignKey ssh . Signer
2022-04-26 11:12:16 +00:00
policyEngine * policy . Engine
2019-08-06 01:35:00 +00:00
}
type args struct {
key ssh . PublicKey
2020-07-23 01:24:45 +00:00
opts provisioner . SignSSHOptions
2019-08-06 01:35:00 +00:00
signOpts [ ] provisioner . SignOption
}
type want struct {
CertType uint32
Principals [ ] string
ValidAfter uint64
ValidBefore uint64
}
tests := [ ] struct {
name string
fields fields
args args
want want
wantErr bool
} {
2022-04-26 11:12:16 +00:00
{ "ok-user" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions } } , want { CertType : ssh . UserCert } , false } ,
{ "ok-host" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { hostTemplate , hostOptions } } , want { CertType : ssh . HostCert } , false } ,
{ "ok-user-only" , fields { signer , nil , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions } } , want { CertType : ssh . UserCert } , false } ,
{ "ok-host-only" , fields { nil , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { hostTemplate , hostOptions } } , want { CertType : ssh . HostCert } , false } ,
{ "ok-opts-type-user" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { CertType : "user" } , [ ] provisioner . SignOption { userTemplate } } , want { CertType : ssh . UserCert } , false } ,
{ "ok-opts-type-host" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { CertType : "host" } , [ ] provisioner . SignOption { hostTemplate } } , want { CertType : ssh . HostCert } , false } ,
{ "ok-opts-principals" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { CertType : "user" , Principals : [ ] string { "user" } } , [ ] provisioner . SignOption { userTemplateWithUser } } , want { CertType : ssh . UserCert , Principals : [ ] string { "user" } } , false } ,
{ "ok-opts-principals" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { CertType : "host" , Principals : [ ] string { "foo.test.com" , "bar.test.com" } } , [ ] provisioner . SignOption { hostTemplateWithHosts } } , want { CertType : ssh . HostCert , Principals : [ ] string { "foo.test.com" , "bar.test.com" } } , false } ,
{ "ok-opts-valid-after" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { CertType : "user" , ValidAfter : provisioner . NewTimeDuration ( now ) } , [ ] provisioner . SignOption { userTemplate } } , want { CertType : ssh . UserCert , ValidAfter : uint64 ( now . Unix ( ) ) } , false } ,
{ "ok-opts-valid-before" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { CertType : "host" , ValidBefore : provisioner . NewTimeDuration ( now ) } , [ ] provisioner . SignOption { hostTemplate } } , want { CertType : ssh . HostCert , ValidBefore : uint64 ( now . Unix ( ) ) } , false } ,
{ "ok-cert-validator" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , sshTestCertValidator ( "" ) } } , want { CertType : ssh . UserCert } , false } ,
{ "ok-cert-modifier" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , sshTestCertModifier ( "" ) } } , want { CertType : ssh . UserCert } , false } ,
{ "ok-opts-validator" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , sshTestOptionsValidator ( "" ) } } , want { CertType : ssh . UserCert } , false } ,
{ "ok-opts-modifier" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , sshTestOptionsModifier ( "" ) } } , want { CertType : ssh . UserCert } , false } ,
{ "ok-custom-template" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userCustomTemplate , userOptions } } , want { CertType : ssh . UserCert , Principals : [ ] string { "user" , "admin" } } , false } ,
2022-09-30 00:16:26 +00:00
{ "ok-enrich-template" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { enrichTemplate , userOptions , & mockWebhookController { templateData : enrichTemplateData , respData : map [ string ] any { "people" : map [ string ] any { "role" : [ ] string { "user" , "eng" } } } } } } , want { CertType : ssh . UserCert , Principals : [ ] string { "user" , "eng" } } , false } ,
2022-04-26 11:12:16 +00:00
{ "ok-user-policy" , fields { signer , signer , userPolicy } , args { pub , provisioner . SignSSHOptions { CertType : "user" , Principals : [ ] string { "user" } } , [ ] provisioner . SignOption { userTemplateWithUser } } , want { CertType : ssh . UserCert , Principals : [ ] string { "user" } } , false } ,
{ "ok-host-policy" , fields { signer , signer , hostPolicy } , args { pub , provisioner . SignSSHOptions { CertType : "host" , Principals : [ ] string { "foo.test.com" , "bar.test.com" } } , [ ] provisioner . SignOption { hostTemplateWithHosts } } , want { CertType : ssh . HostCert , Principals : [ ] string { "foo.test.com" , "bar.test.com" } } , false } ,
{ "fail-opts-type" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { CertType : "foo" } , [ ] provisioner . SignOption { userTemplate } } , want { } , true } ,
{ "fail-cert-validator" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , sshTestCertValidator ( "an error" ) } } , want { } , true } ,
{ "fail-cert-modifier" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , sshTestCertModifier ( "an error" ) } } , want { } , true } ,
{ "fail-opts-validator" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , sshTestOptionsValidator ( "an error" ) } } , want { } , true } ,
{ "fail-opts-modifier" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , sshTestOptionsModifier ( "an error" ) } } , want { } , true } ,
{ "fail-bad-sign-options" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , "wrong type" } } , want { } , true } ,
{ "fail-no-user-key" , fields { nil , signer , nil } , args { pub , provisioner . SignSSHOptions { CertType : "user" } , [ ] provisioner . SignOption { userTemplate } } , want { } , true } ,
{ "fail-no-host-key" , fields { signer , nil , nil } , args { pub , provisioner . SignSSHOptions { CertType : "host" } , [ ] provisioner . SignOption { hostTemplate } } , want { } , true } ,
{ "fail-bad-type" , fields { signer , nil , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , sshTestModifier { CertType : 100 } } } , want { } , true } ,
{ "fail-custom-template" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userFailTemplate , userOptions } } , want { } , true } ,
{ "fail-custom-template-syntax-error-file" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userJSONSyntaxErrorTemplateFile , userOptions } } , want { } , true } ,
{ "fail-custom-template-syntax-value-file" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userJSONValueErrorTemplateFile , userOptions } } , want { } , true } ,
{ "fail-user-policy" , fields { signer , signer , userPolicy } , args { pub , provisioner . SignSSHOptions { CertType : "user" , Principals : [ ] string { "root" } } , [ ] provisioner . SignOption { userTemplateWithRoot } } , want { } , true } ,
{ "fail-user-policy-with-host-cert" , fields { signer , signer , userPolicy } , args { pub , provisioner . SignSSHOptions { CertType : "host" , Principals : [ ] string { "foo.test.com" } } , [ ] provisioner . SignOption { hostTemplateWithExampleDotCom } } , want { } , true } ,
{ "fail-user-policy-with-bad-user" , fields { signer , signer , userPolicy } , args { pub , provisioner . SignSSHOptions { CertType : "user" , Principals : [ ] string { "user" } } , [ ] provisioner . SignOption { badUserTemplate } } , want { } , true } ,
{ "fail-host-policy" , fields { signer , signer , hostPolicy } , args { pub , provisioner . SignSSHOptions { CertType : "host" , Principals : [ ] string { "example.com" } } , [ ] provisioner . SignOption { hostTemplateWithExampleDotCom } } , want { } , true } ,
{ "fail-host-policy-with-user-cert" , fields { signer , signer , hostPolicy } , args { pub , provisioner . SignSSHOptions { CertType : "user" , Principals : [ ] string { "user" } } , [ ] provisioner . SignOption { userTemplateWithUser } } , want { } , true } ,
{ "fail-host-policy-with-bad-host" , fields { signer , signer , hostPolicy } , args { pub , provisioner . SignSSHOptions { CertType : "host" , Principals : [ ] string { "example.com" } } , [ ] provisioner . SignOption { badHostTemplate } } , want { } , true } ,
2022-09-30 00:16:26 +00:00
{ "fail-enriching-webhooks" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , & mockWebhookController { enrichErr : provisioner . ErrWebhookDenied } } } , want { } , true } ,
{ "fail-authorizing-webhooks" , fields { signer , signer , nil } , args { pub , provisioner . SignSSHOptions { } , [ ] provisioner . SignOption { userTemplate , userOptions , & mockWebhookController { authorizeErr : provisioner . ErrWebhookDenied } } } , want { } , true } ,
2019-08-06 01:35:00 +00:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
a := testAuthority ( t )
a . sshCAUserCertSignKey = tt . fields . sshCAUserCertSignKey
a . sshCAHostCertSignKey = tt . fields . sshCAHostCertSignKey
2022-04-26 11:12:16 +00:00
a . policyEngine = tt . fields . policyEngine
2019-08-06 01:35:00 +00:00
2020-03-11 02:17:32 +00:00
got , err := a . SignSSH ( context . Background ( ) , tt . args . key , tt . args . opts , tt . args . signOpts ... )
2019-08-06 01:35:00 +00:00
if ( err != nil ) != tt . wantErr {
t . Errorf ( "Authority.SignSSH() error = %v, wantErr %v" , err , tt . wantErr )
return
}
if err == nil && assert . NotNil ( t , got ) {
assert . Equals ( t , tt . want . CertType , got . CertType )
assert . Equals ( t , tt . want . Principals , got . ValidPrincipals )
assert . Equals ( t , tt . want . ValidAfter , got . ValidAfter )
assert . Equals ( t , tt . want . ValidBefore , got . ValidBefore )
assert . NotNil ( t , got . Key )
assert . NotNil ( t , got . Nonce )
assert . NotEquals ( t , 0 , got . Serial )
assert . NotNil ( t , got . Signature )
assert . NotNil ( t , got . SignatureKey )
}
} )
}
}
func TestAuthority_SignSSHAddUser ( t * testing . T ) {
key , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
pub , err := ssh . NewPublicKey ( key . Public ( ) )
assert . FatalError ( t , err )
signKey , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
2019-09-28 02:05:53 +00:00
signer , err := ssh . NewSignerFromKey ( signKey )
assert . FatalError ( t , err )
2019-08-06 01:35:00 +00:00
type fields struct {
2019-09-28 02:05:53 +00:00
sshCAUserCertSignKey ssh . Signer
sshCAHostCertSignKey ssh . Signer
2019-08-06 01:35:00 +00:00
addUserPrincipal string
addUserCommand string
}
type args struct {
key ssh . PublicKey
subject * ssh . Certificate
}
type want struct {
CertType uint32
Principals [ ] string
ValidAfter uint64
ValidBefore uint64
ForceCommand string
}
now := time . Now ( )
validCert := & ssh . Certificate {
CertType : ssh . UserCert ,
ValidPrincipals : [ ] string { "user" } ,
ValidAfter : uint64 ( now . Unix ( ) ) ,
ValidBefore : uint64 ( now . Add ( time . Hour ) . Unix ( ) ) ,
}
validWant := want {
CertType : ssh . UserCert ,
Principals : [ ] string { "provisioner" } ,
ValidAfter : uint64 ( now . Unix ( ) ) ,
ValidBefore : uint64 ( now . Add ( time . Hour ) . Unix ( ) ) ,
ForceCommand : "sudo useradd -m user; nc -q0 localhost 22" ,
}
tests := [ ] struct {
name string
fields fields
args args
want want
wantErr bool
} {
2019-09-28 02:05:53 +00:00
{ "ok" , fields { signer , signer , "" , "" } , args { pub , validCert } , validWant , false } ,
{ "ok-no-host-key" , fields { signer , nil , "" , "" } , args { pub , validCert } , validWant , false } ,
{ "ok-custom-principal" , fields { signer , signer , "my-principal" , "" } , args { pub , & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { "user" } } } , want { CertType : ssh . UserCert , Principals : [ ] string { "my-principal" } , ForceCommand : "sudo useradd -m user; nc -q0 localhost 22" } , false } ,
{ "ok-custom-command" , fields { signer , signer , "" , "foo <principal> <principal>" } , args { pub , & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { "user" } } } , want { CertType : ssh . UserCert , Principals : [ ] string { "provisioner" } , ForceCommand : "foo user user" } , false } ,
{ "ok-custom-principal-and-command" , fields { signer , signer , "my-principal" , "foo <principal> <principal>" } , args { pub , & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { "user" } } } , want { CertType : ssh . UserCert , Principals : [ ] string { "my-principal" } , ForceCommand : "foo user user" } , false } ,
{ "fail-no-user-key" , fields { nil , signer , "" , "" } , args { pub , validCert } , want { } , true } ,
{ "fail-no-user-cert" , fields { signer , signer , "" , "" } , args { pub , & ssh . Certificate { CertType : ssh . HostCert , ValidPrincipals : [ ] string { "foo" } } } , want { } , true } ,
{ "fail-no-principals" , fields { signer , signer , "" , "" } , args { pub , & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { } } } , want { } , true } ,
{ "fail-many-principals" , fields { signer , signer , "" , "" } , args { pub , & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { "foo" , "bar" } } } , want { } , true } ,
2019-08-06 01:35:00 +00:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
a := testAuthority ( t )
a . sshCAUserCertSignKey = tt . fields . sshCAUserCertSignKey
a . sshCAHostCertSignKey = tt . fields . sshCAHostCertSignKey
a . config . SSH = & SSHConfig {
AddUserPrincipal : tt . fields . addUserPrincipal ,
AddUserCommand : tt . fields . addUserCommand ,
}
2020-03-11 02:17:32 +00:00
got , err := a . SignSSHAddUser ( context . Background ( ) , tt . args . key , tt . args . subject )
2019-08-06 01:35:00 +00:00
if ( err != nil ) != tt . wantErr {
t . Errorf ( "Authority.SignSSHAddUser() error = %v, wantErr %v" , err , tt . wantErr )
return
}
if err == nil && assert . NotNil ( t , got ) {
assert . Equals ( t , tt . want . CertType , got . CertType )
assert . Equals ( t , tt . want . Principals , got . ValidPrincipals )
assert . Equals ( t , tt . args . subject . ValidPrincipals [ 0 ] + "-" + tt . want . Principals [ 0 ] , got . KeyId )
assert . Equals ( t , tt . want . ValidAfter , got . ValidAfter )
assert . Equals ( t , tt . want . ValidBefore , got . ValidBefore )
assert . Equals ( t , map [ string ] string { "force-command" : tt . want . ForceCommand } , got . CriticalOptions )
assert . Equals ( t , nil , got . Extensions )
assert . NotNil ( t , got . Key )
assert . NotNil ( t , got . Nonce )
assert . NotEquals ( t , 0 , got . Serial )
assert . NotNil ( t , got . Signature )
assert . NotNil ( t , got . SignatureKey )
}
} )
}
}
2019-10-15 00:10:47 +00:00
func TestAuthority_GetSSHRoots ( t * testing . T ) {
key , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
user , err := ssh . NewPublicKey ( key . Public ( ) )
assert . FatalError ( t , err )
key , err = ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
host , err := ssh . NewPublicKey ( key . Public ( ) )
assert . FatalError ( t , err )
type fields struct {
sshCAUserCerts [ ] ssh . PublicKey
sshCAHostCerts [ ] ssh . PublicKey
}
tests := [ ] struct {
name string
fields fields
want * SSHKeys
wantErr bool
} {
{ "ok" , fields { [ ] ssh . PublicKey { user } , [ ] ssh . PublicKey { host } } , & SSHKeys { UserKeys : [ ] ssh . PublicKey { user } , HostKeys : [ ] ssh . PublicKey { host } } , false } ,
{ "nil" , fields { } , & SSHKeys { } , false } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
a := testAuthority ( t )
a . sshCAUserCerts = tt . fields . sshCAUserCerts
a . sshCAHostCerts = tt . fields . sshCAHostCerts
2020-03-11 02:17:32 +00:00
got , err := a . GetSSHRoots ( context . Background ( ) )
2019-10-15 00:10:47 +00:00
if ( err != nil ) != tt . wantErr {
t . Errorf ( "Authority.GetSSHRoots() error = %v, wantErr %v" , err , tt . wantErr )
return
}
if ! reflect . DeepEqual ( got , tt . want ) {
t . Errorf ( "Authority.GetSSHRoots() = %v, want %v" , got , tt . want )
}
} )
}
}
func TestAuthority_GetSSHFederation ( t * testing . T ) {
key , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
user , err := ssh . NewPublicKey ( key . Public ( ) )
assert . FatalError ( t , err )
key , err = ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
host , err := ssh . NewPublicKey ( key . Public ( ) )
assert . FatalError ( t , err )
type fields struct {
sshCAUserFederatedCerts [ ] ssh . PublicKey
sshCAHostFederatedCerts [ ] ssh . PublicKey
}
tests := [ ] struct {
name string
fields fields
want * SSHKeys
wantErr bool
} {
{ "ok" , fields { [ ] ssh . PublicKey { user } , [ ] ssh . PublicKey { host } } , & SSHKeys { UserKeys : [ ] ssh . PublicKey { user } , HostKeys : [ ] ssh . PublicKey { host } } , false } ,
{ "nil" , fields { } , & SSHKeys { } , false } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
a := testAuthority ( t )
a . sshCAUserFederatedCerts = tt . fields . sshCAUserFederatedCerts
a . sshCAHostFederatedCerts = tt . fields . sshCAHostFederatedCerts
2020-03-11 02:17:32 +00:00
got , err := a . GetSSHFederation ( context . Background ( ) )
2019-10-15 00:10:47 +00:00
if ( err != nil ) != tt . wantErr {
t . Errorf ( "Authority.GetSSHFederation() error = %v, wantErr %v" , err , tt . wantErr )
return
}
if ! reflect . DeepEqual ( got , tt . want ) {
t . Errorf ( "Authority.GetSSHFederation() = %v, want %v" , got , tt . want )
}
} )
}
}
func TestAuthority_GetSSHConfig ( t * testing . T ) {
key , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
user , err := ssh . NewPublicKey ( key . Public ( ) )
assert . FatalError ( t , err )
userSigner , err := ssh . NewSignerFromSigner ( key )
assert . FatalError ( t , err )
userB64 := base64 . StdEncoding . EncodeToString ( user . Marshal ( ) )
key , err = ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
host , err := ssh . NewPublicKey ( key . Public ( ) )
assert . FatalError ( t , err )
hostSigner , err := ssh . NewSignerFromSigner ( key )
assert . FatalError ( t , err )
hostB64 := base64 . StdEncoding . EncodeToString ( host . Marshal ( ) )
tmplConfig := & templates . Templates {
SSH : & templates . SSHTemplates {
User : [ ] templates . Template {
{ Name : "known_host.tpl" , Type : templates . File , TemplatePath : "./testdata/templates/known_hosts.tpl" , Path : "ssh/known_host" , Comment : "#" } ,
} ,
Host : [ ] templates . Template {
{ Name : "ca.tpl" , Type : templates . File , TemplatePath : "./testdata/templates/ca.tpl" , Path : "/etc/ssh/ca.pub" , Comment : "#" } ,
} ,
} ,
Data : map [ string ] interface { } {
"Step" : & templates . Step {
SSH : templates . StepSSH {
UserKey : user ,
HostKey : host ,
} ,
} ,
} ,
}
userOutput := [ ] templates . Output {
{ Name : "known_host.tpl" , Type : templates . File , Comment : "#" , Path : "ssh/known_host" , Content : [ ] byte ( fmt . Sprintf ( "@cert-authority * %s %s" , host . Type ( ) , hostB64 ) ) } ,
}
hostOutput := [ ] templates . Output {
{ Name : "ca.tpl" , Type : templates . File , Comment : "#" , Path : "/etc/ssh/ca.pub" , Content : [ ] byte ( user . Type ( ) + " " + userB64 ) } ,
}
2019-10-15 19:18:29 +00:00
tmplConfigWithUserData := & templates . Templates {
SSH : & templates . SSHTemplates {
User : [ ] templates . Template {
{ Name : "include.tpl" , Type : templates . File , TemplatePath : "./testdata/templates/include.tpl" , Path : "ssh/include" , Comment : "#" } ,
{ Name : "config.tpl" , Type : templates . File , TemplatePath : "./testdata/templates/config.tpl" , Path : "ssh/config" , Comment : "#" } ,
} ,
Host : [ ] templates . Template {
2020-06-17 01:21:44 +00:00
{
Name : "sshd_config.tpl" ,
Type : templates . File ,
TemplatePath : "./testdata/templates/sshd_config.tpl" ,
Path : "/etc/ssh/sshd_config" ,
Comment : "#" ,
RequiredData : [ ] string { "Certificate" , "Key" } ,
} ,
2019-10-15 19:18:29 +00:00
} ,
} ,
Data : map [ string ] interface { } {
"Step" : & templates . Step {
SSH : templates . StepSSH {
UserKey : user ,
HostKey : host ,
} ,
} ,
} ,
}
userOutputWithUserData := [ ] templates . Output {
{ Name : "include.tpl" , Type : templates . File , Comment : "#" , Path : "ssh/include" , Content : [ ] byte ( "Host *\n\tInclude /home/user/.step/ssh/config" ) } ,
2020-04-15 18:17:24 +00:00
{ Name : "config.tpl" , Type : templates . File , Comment : "#" , Path : "ssh/config" , Content : [ ] byte ( "Match exec \"step ssh check-host %h\"\n\tUserKnownHostsFile /home/user/.step/ssh/known_hosts\n\tProxyCommand step ssh proxycommand %r %h %p\n" ) } ,
2019-10-15 19:18:29 +00:00
}
hostOutputWithUserData := [ ] templates . Output {
2021-04-13 01:37:10 +00:00
{ Name : "sshd_config.tpl" , Type : templates . File , Comment : "#" , Path : "/etc/ssh/sshd_config" , Content : [ ] byte ( "Match all\n\tTrustedUserCAKeys /etc/ssh/ca.pub\n\tHostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub\n\tHostKey /etc/ssh/ssh_host_ecdsa_key" ) } ,
2019-10-15 19:18:29 +00:00
}
2021-11-15 23:32:07 +00:00
tmplConfigUserIncludes := & templates . Templates {
SSH : & templates . SSHTemplates {
User : [ ] templates . Template {
{ Name : "step_includes.tpl" , Type : templates . PrependLine , TemplatePath : "./testdata/templates/step_includes.tpl" , Path : "${STEPPATH}/ssh/includes" , Comment : "#" } ,
} ,
} ,
Data : map [ string ] interface { } {
"Step" : & templates . Step {
SSH : templates . StepSSH {
UserKey : user ,
HostKey : host ,
} ,
} ,
} ,
}
userOutputEmptyData := [ ] templates . Output {
{ Name : "step_includes.tpl" , Type : templates . File , Comment : "#" , Path : "ssh/includes" , Content : [ ] byte ( "Include \"<no value>/ssh/config\"\n" ) } ,
}
userOutputWithoutTemplateVersion := [ ] templates . Output {
{ Name : "step_includes.tpl" , Type : templates . File , Comment : "#" , Path : "ssh/includes" , Content : [ ] byte ( "Include \"/home/user/.step/ssh/config\"\n" ) } ,
}
userOutputWithTemplateVersion := [ ] templates . Output {
{ Name : "step_includes.tpl" , Type : templates . PrependLine , Comment : "#" , Path : "${STEPPATH}/ssh/includes" , Content : [ ] byte ( "Include \"/home/user/.step/ssh/config\"\n" ) } ,
}
2019-10-15 19:18:29 +00:00
tmplConfigErr := & templates . Templates {
SSH : & templates . SSHTemplates {
User : [ ] templates . Template {
{ Name : "error.tpl" , Type : templates . File , TemplatePath : "./testdata/templates/error.tpl" , Path : "ssh/error" , Comment : "#" } ,
} ,
Host : [ ] templates . Template {
{ Name : "error.tpl" , Type : templates . File , TemplatePath : "./testdata/templates/error.tpl" , Path : "ssh/error" , Comment : "#" } ,
} ,
} ,
}
2020-06-17 01:21:44 +00:00
tmplConfigFail := & templates . Templates {
SSH : & templates . SSHTemplates {
User : [ ] templates . Template {
{ Name : "fail.tpl" , Type : templates . File , TemplatePath : "./testdata/templates/fail.tpl" , Path : "ssh/fail" , Comment : "#" } ,
} ,
} ,
}
2019-10-15 00:10:47 +00:00
type fields struct {
templates * templates . Templates
userSigner ssh . Signer
hostSigner ssh . Signer
}
type args struct {
typ string
data map [ string ] string
}
tests := [ ] struct {
name string
fields fields
args args
want [ ] templates . Output
wantErr bool
} {
{ "user" , fields { tmplConfig , userSigner , hostSigner } , args { "user" , nil } , userOutput , false } ,
2019-10-15 19:18:29 +00:00
{ "user" , fields { tmplConfig , userSigner , nil } , args { "user" , nil } , userOutput , false } ,
2019-10-15 00:10:47 +00:00
{ "host" , fields { tmplConfig , userSigner , hostSigner } , args { "host" , nil } , hostOutput , false } ,
2019-10-15 19:18:29 +00:00
{ "host" , fields { tmplConfig , nil , hostSigner } , args { "host" , nil } , hostOutput , false } ,
{ "userWithData" , fields { tmplConfigWithUserData , userSigner , hostSigner } , args { "user" , map [ string ] string { "StepPath" : "/home/user/.step" } } , userOutputWithUserData , false } ,
{ "hostWithData" , fields { tmplConfigWithUserData , userSigner , hostSigner } , args { "host" , map [ string ] string { "Certificate" : "ssh_host_ecdsa_key-cert.pub" , "Key" : "ssh_host_ecdsa_key" } } , hostOutputWithUserData , false } ,
2021-11-15 23:32:07 +00:00
{ "userIncludesEmptyData" , fields { tmplConfigUserIncludes , userSigner , hostSigner } , args { "user" , nil } , userOutputEmptyData , false } ,
{ "userIncludesWithoutTemplateVersion" , fields { tmplConfigUserIncludes , userSigner , hostSigner } , args { "user" , map [ string ] string { "StepPath" : "/home/user/.step" } } , userOutputWithoutTemplateVersion , false } ,
{ "userIncludesWithTemplateVersion" , fields { tmplConfigUserIncludes , userSigner , hostSigner } , args { "user" , map [ string ] string { "StepPath" : "/home/user/.step" , "StepSSHTemplateVersion" : "v2" } } , userOutputWithTemplateVersion , false } ,
2019-10-15 19:18:29 +00:00
{ "disabled" , fields { tmplConfig , nil , nil } , args { "host" , nil } , nil , true } ,
{ "badType" , fields { tmplConfig , userSigner , hostSigner } , args { "bad" , nil } , nil , true } ,
{ "userError" , fields { tmplConfigErr , userSigner , hostSigner } , args { "user" , nil } , nil , true } ,
{ "hostError" , fields { tmplConfigErr , userSigner , hostSigner } , args { "host" , map [ string ] string { "Function" : "foo" } } , nil , true } ,
2020-06-16 00:25:47 +00:00
{ "noTemplates" , fields { nil , userSigner , hostSigner } , args { "user" , nil } , nil , true } ,
2020-06-17 01:21:44 +00:00
{ "missingData" , fields { tmplConfigWithUserData , userSigner , hostSigner } , args { "host" , map [ string ] string { "Certificate" : "ssh_host_ecdsa_key-cert.pub" } } , nil , true } ,
{ "failError" , fields { tmplConfigFail , userSigner , hostSigner } , args { "user" , nil } , nil , true } ,
2019-10-15 00:10:47 +00:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
a := testAuthority ( t )
2020-06-17 00:57:35 +00:00
a . templates = tt . fields . templates
2019-10-15 00:10:47 +00:00
a . sshCAUserCertSignKey = tt . fields . userSigner
a . sshCAHostCertSignKey = tt . fields . hostSigner
2020-03-11 02:17:32 +00:00
got , err := a . GetSSHConfig ( context . Background ( ) , tt . args . typ , tt . args . data )
2019-10-15 00:10:47 +00:00
if ( err != nil ) != tt . wantErr {
t . Errorf ( "Authority.GetSSHConfig() error = %v, wantErr %v" , err , tt . wantErr )
return
}
if ! reflect . DeepEqual ( got , tt . want ) {
t . Errorf ( "Authority.GetSSHConfig() = %v, want %v" , got , tt . want )
}
} )
}
}
2019-10-15 19:18:29 +00:00
func TestAuthority_CheckSSHHost ( t * testing . T ) {
type fields struct {
exists bool
err error
}
type args struct {
2019-12-12 02:21:20 +00:00
ctx context . Context
2019-10-15 19:18:29 +00:00
principal string
2019-12-12 02:21:20 +00:00
token string
2019-10-15 19:18:29 +00:00
}
tests := [ ] struct {
name string
fields fields
args args
want bool
wantErr bool
} {
2020-01-24 06:04:34 +00:00
{ "true" , fields { true , nil } , args { context . Background ( ) , "foo.internal.com" , "" } , true , false } ,
{ "false" , fields { false , nil } , args { context . Background ( ) , "foo.internal.com" , "" } , false , false } ,
{ "notImplemented" , fields { false , db . ErrNotImplemented } , args { context . Background ( ) , "foo.internal.com" , "" } , false , true } ,
{ "notImplemented" , fields { true , db . ErrNotImplemented } , args { context . Background ( ) , "foo.internal.com" , "" } , false , true } ,
{ "internal" , fields { false , fmt . Errorf ( "an error" ) } , args { context . Background ( ) , "foo.internal.com" , "" } , false , true } ,
{ "internal" , fields { true , fmt . Errorf ( "an error" ) } , args { context . Background ( ) , "foo.internal.com" , "" } , false , true } ,
2019-10-15 19:18:29 +00:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
a := testAuthority ( t )
2019-12-20 21:30:05 +00:00
a . db = & db . MockAuthDB {
MIsSSHHost : func ( _ string ) ( bool , error ) {
2019-10-15 19:18:29 +00:00
return tt . fields . exists , tt . fields . err
} ,
}
2019-12-12 02:21:20 +00:00
got , err := a . CheckSSHHost ( tt . args . ctx , tt . args . principal , tt . args . token )
2019-10-15 19:18:29 +00:00
if ( err != nil ) != tt . wantErr {
t . Errorf ( "Authority.CheckSSHHost() error = %v, wantErr %v" , err , tt . wantErr )
return
}
if got != tt . want {
t . Errorf ( "Authority.CheckSSHHost() = %v, want %v" , got , tt . want )
}
} )
}
}
func TestSSHConfig_Validate ( t * testing . T ) {
key , err := jose . GenerateJWK ( "EC" , "P-256" , "" , "sig" , "" , 0 )
assert . FatalError ( t , err )
tests := [ ] struct {
name string
sshConfig * SSHConfig
wantErr bool
} {
{ "nil" , nil , false } ,
{ "ok" , & SSHConfig { Keys : [ ] * SSHPublicKey { { Type : "user" , Key : key . Public ( ) } } } , false } ,
{ "ok" , & SSHConfig { Keys : [ ] * SSHPublicKey { { Type : "host" , Key : key . Public ( ) } } } , false } ,
{ "badType" , & SSHConfig { Keys : [ ] * SSHPublicKey { { Type : "bad" , Key : key . Public ( ) } } } , true } ,
{ "badKey" , & SSHConfig { Keys : [ ] * SSHPublicKey { { Type : "user" , Key : * key } } } , true } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
if err := tt . sshConfig . Validate ( ) ; ( err != nil ) != tt . wantErr {
t . Errorf ( "SSHConfig.Validate() error = %v, wantErr %v" , err , tt . wantErr )
}
} )
}
}
2019-11-15 02:24:58 +00:00
func TestAuthority_GetSSHBastion ( t * testing . T ) {
bastion := & Bastion {
Hostname : "bastion.local" ,
Port : "2222" ,
}
type fields struct {
config * Config
2020-03-11 02:17:32 +00:00
sshBastionFunc func ( ctx context . Context , user , hostname string ) ( * Bastion , error )
2019-11-15 02:24:58 +00:00
}
type args struct {
user string
hostname string
}
tests := [ ] struct {
name string
fields fields
args args
want * Bastion
wantErr bool
} {
{ "config" , fields { & Config { SSH : & SSHConfig { Bastion : bastion } } , nil } , args { "user" , "host.local" } , bastion , false } ,
2020-06-19 19:37:08 +00:00
{ "bastion" , fields { & Config { SSH : & SSHConfig { Bastion : bastion } } , nil } , args { "user" , "bastion.local" } , nil , false } ,
2019-11-15 02:24:58 +00:00
{ "nil" , fields { & Config { SSH : & SSHConfig { Bastion : nil } } , nil } , args { "user" , "host.local" } , nil , false } ,
{ "empty" , fields { & Config { SSH : & SSHConfig { Bastion : & Bastion { } } } , nil } , args { "user" , "host.local" } , nil , false } ,
2020-03-11 02:17:32 +00:00
{ "func" , fields { & Config { } , func ( _ context . Context , _ , _ string ) ( * Bastion , error ) { return bastion , nil } } , args { "user" , "host.local" } , bastion , false } ,
{ "func err" , fields { & Config { } , func ( _ context . Context , _ , _ string ) ( * Bastion , error ) { return nil , errors . New ( "foo" ) } } , args { "user" , "host.local" } , nil , true } ,
2019-11-15 02:24:58 +00:00
{ "error" , fields { & Config { SSH : nil } , nil } , args { "user" , "host.local" } , nil , true } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
a := & Authority {
config : tt . fields . config ,
sshBastionFunc : tt . fields . sshBastionFunc ,
}
2020-03-11 02:17:32 +00:00
got , err := a . GetSSHBastion ( context . Background ( ) , tt . args . user , tt . args . hostname )
2019-11-15 02:24:58 +00:00
if ( err != nil ) != tt . wantErr {
t . Errorf ( "Authority.GetSSHBastion() error = %v, wantErr %v" , err , tt . wantErr )
return
2019-12-20 21:30:05 +00:00
} else if err != nil {
2022-03-30 08:22:22 +00:00
_ , ok := err . ( render . StatusCodedError )
assert . Fatal ( t , ok , "error does not implement StatusCodedError interface" )
2019-11-15 02:24:58 +00:00
}
if ! reflect . DeepEqual ( got , tt . want ) {
t . Errorf ( "Authority.GetSSHBastion() = %v, want %v" , got , tt . want )
}
} )
}
}
2019-12-20 21:30:05 +00:00
func TestAuthority_GetSSHHosts ( t * testing . T ) {
a := testAuthority ( t )
type test struct {
2020-08-10 18:26:51 +00:00
getHostsFunc func ( context . Context , * x509 . Certificate ) ( [ ] Host , error )
2019-12-20 21:30:05 +00:00
auth * Authority
cert * x509 . Certificate
2020-08-10 18:26:51 +00:00
cmp func ( got [ ] Host )
2019-12-20 21:30:05 +00:00
err error
code int
}
tests := map [ string ] func ( t * testing . T ) * test {
"fail/getHostsFunc-fail" : func ( t * testing . T ) * test {
return & test {
2020-08-10 18:26:51 +00:00
getHostsFunc : func ( ctx context . Context , cert * x509 . Certificate ) ( [ ] Host , error ) {
2019-12-20 21:30:05 +00:00
return nil , errors . New ( "force" )
} ,
cert : & x509 . Certificate { } ,
err : errors . New ( "getSSHHosts: force" ) ,
code : http . StatusInternalServerError ,
}
} ,
"ok/getHostsFunc-defined" : func ( t * testing . T ) * test {
2020-08-10 18:26:51 +00:00
hosts := [ ] Host {
2019-12-20 21:30:05 +00:00
{ HostID : "1" , Hostname : "foo" } ,
{ HostID : "2" , Hostname : "bar" } ,
}
return & test {
2020-08-10 18:26:51 +00:00
getHostsFunc : func ( ctx context . Context , cert * x509 . Certificate ) ( [ ] Host , error ) {
2019-12-20 21:30:05 +00:00
return hosts , nil
} ,
cert : & x509 . Certificate { } ,
2020-08-10 18:26:51 +00:00
cmp : func ( got [ ] Host ) {
2019-12-20 21:30:05 +00:00
assert . Equals ( t , got , hosts )
} ,
}
} ,
"fail/db-get-fail" : func ( t * testing . T ) * test {
return & test {
auth : testAuthority ( t , WithDatabase ( & db . MockAuthDB {
MGetSSHHostPrincipals : func ( ) ( [ ] string , error ) {
return nil , errors . New ( "force" )
} ,
} ) ) ,
cert : & x509 . Certificate { } ,
err : errors . New ( "getSSHHosts: force" ) ,
code : http . StatusInternalServerError ,
}
} ,
"ok" : func ( t * testing . T ) * test {
return & test {
auth : testAuthority ( t , WithDatabase ( & db . MockAuthDB {
MGetSSHHostPrincipals : func ( ) ( [ ] string , error ) {
return [ ] string { "foo" , "bar" } , nil
} ,
} ) ) ,
cert : & x509 . Certificate { } ,
2020-08-10 18:26:51 +00:00
cmp : func ( got [ ] Host ) {
assert . Equals ( t , got , [ ] Host {
2019-12-20 21:30:05 +00:00
{ Hostname : "foo" } ,
{ Hostname : "bar" } ,
} )
} ,
}
} ,
}
for name , genTestCase := range tests {
t . Run ( name , func ( t * testing . T ) {
tc := genTestCase ( t )
auth := tc . auth
if auth == nil {
auth = a
}
auth . sshGetHostsFunc = tc . getHostsFunc
2020-03-11 02:17:32 +00:00
hosts , err := auth . GetSSHHosts ( context . Background ( ) , tc . cert )
2019-12-20 21:30:05 +00:00
if err != nil {
if assert . NotNil ( t , tc . err ) {
2022-09-22 07:04:31 +00:00
var sc render . StatusCodedError
assert . Fatal ( t , errors . As ( err , & sc ) , "error does not implement StatusCodedError interface" )
2019-12-20 21:30:05 +00:00
assert . Equals ( t , sc . StatusCode ( ) , tc . code )
assert . HasPrefix ( t , err . Error ( ) , tc . err . Error ( ) )
}
} else {
if assert . Nil ( t , tc . err ) {
tc . cmp ( hosts )
}
}
} )
}
}
func TestAuthority_RekeySSH ( t * testing . T ) {
key , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
pub , err := ssh . NewPublicKey ( key . Public ( ) )
assert . FatalError ( t , err )
signKey , err := ecdsa . GenerateKey ( elliptic . P256 ( ) , rand . Reader )
assert . FatalError ( t , err )
signer , err := ssh . NewSignerFromKey ( signKey )
assert . FatalError ( t , err )
userOptions := sshTestModifier {
CertType : ssh . UserCert ,
}
now := time . Now ( ) . UTC ( )
a := testAuthority ( t )
2021-07-21 22:22:57 +00:00
a . db = & db . MockAuthDB {
MIsSSHRevoked : func ( sn string ) ( bool , error ) {
return false , nil
} ,
}
2019-12-20 21:30:05 +00:00
type test struct {
auth * Authority
userSigner ssh . Signer
hostSigner ssh . Signer
cert * ssh . Certificate
key ssh . PublicKey
signOpts [ ] provisioner . SignOption
cmpResult func ( old , n * ssh . Certificate )
err error
code int
}
tests := map [ string ] func ( t * testing . T ) * test {
2021-07-21 22:22:57 +00:00
"fail/is-revoked" : func ( t * testing . T ) * test {
auth := testAuthority ( t )
auth . db = & db . MockAuthDB {
MIsSSHRevoked : func ( sn string ) ( bool , error ) {
return true , nil
} ,
}
return & test {
auth : auth ,
userSigner : signer ,
hostSigner : signer ,
cert : & ssh . Certificate {
Serial : 1234567890 ,
ValidAfter : uint64 ( now . Unix ( ) ) ,
ValidBefore : uint64 ( now . Add ( time . Hour ) . Unix ( ) ) ,
CertType : ssh . UserCert ,
ValidPrincipals : [ ] string { "foo" , "bar" } ,
KeyId : "foo" ,
} ,
key : pub ,
signOpts : [ ] provisioner . SignOption { } ,
err : errors . New ( "authority.authorizeSSHCertificate: certificate has been revoked" ) ,
code : http . StatusUnauthorized ,
}
} ,
"fail/is-revoked-error" : func ( t * testing . T ) * test {
auth := testAuthority ( t )
auth . db = & db . MockAuthDB {
MIsSSHRevoked : func ( sn string ) ( bool , error ) {
return false , errors . New ( "an error" )
} ,
}
return & test {
auth : auth ,
userSigner : signer ,
hostSigner : signer ,
cert : & ssh . Certificate {
Serial : 1234567890 ,
ValidAfter : uint64 ( now . Unix ( ) ) ,
ValidBefore : uint64 ( now . Add ( time . Hour ) . Unix ( ) ) ,
CertType : ssh . UserCert ,
ValidPrincipals : [ ] string { "foo" , "bar" } ,
KeyId : "foo" ,
} ,
key : pub ,
signOpts : [ ] provisioner . SignOption { } ,
err : errors . New ( "authority.authorizeSSHCertificate: an error" ) ,
code : http . StatusInternalServerError ,
}
} ,
2019-12-20 21:30:05 +00:00
"fail/opts-type" : func ( t * testing . T ) * test {
return & test {
userSigner : signer ,
hostSigner : signer ,
key : pub ,
signOpts : [ ] provisioner . SignOption { userOptions } ,
err : errors . New ( "rekeySSH; invalid extra option type" ) ,
code : http . StatusInternalServerError ,
}
} ,
"fail/old-cert-validAfter" : func ( t * testing . T ) * test {
return & test {
userSigner : signer ,
hostSigner : signer ,
cert : & ssh . Certificate { } ,
key : pub ,
signOpts : [ ] provisioner . SignOption { } ,
2021-11-19 02:17:36 +00:00
err : errors . New ( "cannot rekey a certificate without validity period" ) ,
2019-12-20 21:30:05 +00:00
code : http . StatusBadRequest ,
}
} ,
"fail/old-cert-validBefore" : func ( t * testing . T ) * test {
return & test {
userSigner : signer ,
hostSigner : signer ,
cert : & ssh . Certificate { ValidAfter : uint64 ( now . Unix ( ) ) } ,
key : pub ,
signOpts : [ ] provisioner . SignOption { } ,
2021-11-19 02:17:36 +00:00
err : errors . New ( "cannot rekey a certificate without validity period" ) ,
2019-12-20 21:30:05 +00:00
code : http . StatusBadRequest ,
}
} ,
"fail/old-cert-no-user-key" : func ( t * testing . T ) * test {
return & test {
userSigner : nil ,
hostSigner : signer ,
cert : & ssh . Certificate { ValidAfter : uint64 ( now . Unix ( ) ) , ValidBefore : uint64 ( now . Add ( 10 * time . Minute ) . Unix ( ) ) , CertType : ssh . UserCert } ,
key : pub ,
signOpts : [ ] provisioner . SignOption { } ,
err : errors . New ( "rekeySSH; user certificate signing is not enabled" ) ,
code : http . StatusNotImplemented ,
}
} ,
"fail/old-cert-no-host-key" : func ( t * testing . T ) * test {
return & test {
userSigner : signer ,
hostSigner : nil ,
cert : & ssh . Certificate { ValidAfter : uint64 ( now . Unix ( ) ) , ValidBefore : uint64 ( now . Add ( 10 * time . Minute ) . Unix ( ) ) , CertType : ssh . HostCert } ,
key : pub ,
signOpts : [ ] provisioner . SignOption { } ,
err : errors . New ( "rekeySSH; host certificate signing is not enabled" ) ,
code : http . StatusNotImplemented ,
}
} ,
"fail/unexpected-old-cert-type" : func ( t * testing . T ) * test {
return & test {
userSigner : signer ,
hostSigner : signer ,
cert : & ssh . Certificate { ValidAfter : uint64 ( now . Unix ( ) ) , ValidBefore : uint64 ( now . Add ( 10 * time . Minute ) . Unix ( ) ) , CertType : 0 } ,
key : pub ,
signOpts : [ ] provisioner . SignOption { } ,
2021-11-19 02:17:36 +00:00
err : errors . New ( "unexpected certificate type '0'" ) ,
2019-12-20 21:30:05 +00:00
code : http . StatusBadRequest ,
}
} ,
"fail/db-store" : func ( t * testing . T ) * test {
return & test {
auth : testAuthority ( t , WithDatabase ( & db . MockAuthDB {
2021-07-21 22:22:57 +00:00
MIsSSHRevoked : func ( sn string ) ( bool , error ) {
return false , nil
} ,
2019-12-20 21:30:05 +00:00
MStoreSSHCertificate : func ( cert * ssh . Certificate ) error {
return errors . New ( "force" )
} ,
} ) ) ,
userSigner : signer ,
hostSigner : nil ,
cert : & ssh . Certificate { ValidAfter : uint64 ( now . Unix ( ) ) , ValidBefore : uint64 ( now . Add ( 10 * time . Minute ) . Unix ( ) ) , CertType : ssh . UserCert } ,
key : pub ,
signOpts : [ ] provisioner . SignOption { } ,
err : errors . New ( "rekeySSH; error storing certificate in db: force" ) ,
code : http . StatusInternalServerError ,
}
} ,
"ok" : func ( t * testing . T ) * test {
va1 := now . Add ( - 24 * time . Hour )
vb1 := now . Add ( - 23 * time . Hour )
return & test {
userSigner : signer ,
hostSigner : nil ,
cert : & ssh . Certificate {
ValidAfter : uint64 ( va1 . Unix ( ) ) ,
ValidBefore : uint64 ( vb1 . Unix ( ) ) ,
CertType : ssh . UserCert ,
ValidPrincipals : [ ] string { "foo" , "bar" } ,
KeyId : "foo" ,
} ,
key : pub ,
signOpts : [ ] provisioner . SignOption { } ,
cmpResult : func ( old , n * ssh . Certificate ) {
assert . Equals ( t , n . CertType , old . CertType )
assert . Equals ( t , n . ValidPrincipals , old . ValidPrincipals )
assert . Equals ( t , n . KeyId , old . KeyId )
assert . True ( t , n . ValidAfter > uint64 ( now . Add ( - 5 * time . Minute ) . Unix ( ) ) )
assert . True ( t , n . ValidAfter < uint64 ( now . Add ( 5 * time . Minute ) . Unix ( ) ) )
l8r := now . Add ( 1 * time . Hour )
assert . True ( t , n . ValidBefore > uint64 ( l8r . Add ( - 5 * time . Minute ) . Unix ( ) ) )
assert . True ( t , n . ValidBefore < uint64 ( l8r . Add ( 5 * time . Minute ) . Unix ( ) ) )
} ,
}
} ,
}
for name , genTestCase := range tests {
t . Run ( name , func ( t * testing . T ) {
tc := genTestCase ( t )
auth := tc . auth
if auth == nil {
auth = a
}
a . sshCAUserCertSignKey = tc . userSigner
a . sshCAHostCertSignKey = tc . hostSigner
2020-03-11 02:17:32 +00:00
cert , err := auth . RekeySSH ( context . Background ( ) , tc . cert , tc . key , tc . signOpts ... )
2019-12-20 21:30:05 +00:00
if err != nil {
if assert . NotNil ( t , tc . err ) {
2022-09-22 07:04:31 +00:00
var sc render . StatusCodedError
assert . Fatal ( t , errors . As ( err , & sc ) , "error does not implement StatusCodedError interface" )
2019-12-20 21:30:05 +00:00
assert . Equals ( t , sc . StatusCode ( ) , tc . code )
assert . HasPrefix ( t , err . Error ( ) , tc . err . Error ( ) )
}
} else {
if assert . Nil ( t , tc . err ) {
tc . cmpResult ( tc . cert , cert )
}
}
} )
}
}
2020-04-24 02:42:55 +00:00
func TestIsValidForAddUser ( t * testing . T ) {
type args struct {
cert * ssh . Certificate
}
tests := [ ] struct {
name string
args args
wantErr bool
} {
{ "ok" , args { & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { "john" } } } , false } ,
{ "ok oidc" , args { & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { "jane" , "jane@smallstep.com" } } } , false } ,
2020-04-24 17:27:44 +00:00
{ "fail at" , args { & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { "jane" , "@smallstep.com" } } } , true } ,
2020-04-24 02:42:55 +00:00
{ "fail host" , args { & ssh . Certificate { CertType : ssh . HostCert , ValidPrincipals : [ ] string { "john" } } } , true } ,
{ "fail principals" , args { & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { "john" , "jane" } } } , true } ,
{ "fail no principals" , args { & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { } } } , true } ,
{ "fail extra principals" , args { & ssh . Certificate { CertType : ssh . UserCert , ValidPrincipals : [ ] string { "john" , "jane" , "doe" } } } , true } ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
if err := IsValidForAddUser ( tt . args . cert ) ; ( err != nil ) != tt . wantErr {
t . Errorf ( "IsValidForAddUser() error = %v, wantErr %v" , err , tt . wantErr )
}
} )
}
}