certificates/authority/ssh_test.go

423 lines
16 KiB
Go
Raw Normal View History

package authority
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
2019-10-15 00:10:47 +00:00
"encoding/base64"
"fmt"
2019-10-15 00:10:47 +00:00
"reflect"
"testing"
"time"
"github.com/smallstep/assert"
"github.com/smallstep/certificates/authority/provisioner"
2019-10-15 00:10:47 +00:00
"github.com/smallstep/certificates/templates"
"golang.org/x/crypto/ssh"
)
type sshTestModifier ssh.Certificate
func (m sshTestModifier) Modify(cert *ssh.Certificate) error {
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
func (m sshTestCertModifier) Modify(cert *ssh.Certificate) error {
if m == "" {
return nil
}
return fmt.Errorf(string(m))
}
type sshTestCertValidator string
func (v sshTestCertValidator) Valid(crt *ssh.Certificate) error {
if v == "" {
return nil
}
return fmt.Errorf(string(v))
}
type sshTestOptionsValidator string
func (v sshTestOptionsValidator) Valid(opts provisioner.SSHOptions) error {
if v == "" {
return nil
}
return fmt.Errorf(string(v))
}
type sshTestOptionsModifier string
func (m sshTestOptionsModifier) Option(opts provisioner.SSHOptions) provisioner.SSHCertificateModifier {
return sshTestCertModifier(string(m))
}
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)
userOptions := sshTestModifier{
CertType: ssh.UserCert,
}
hostOptions := sshTestModifier{
CertType: ssh.HostCert,
}
now := time.Now()
type fields struct {
2019-09-28 02:05:53 +00:00
sshCAUserCertSignKey ssh.Signer
sshCAHostCertSignKey ssh.Signer
}
type args struct {
key ssh.PublicKey
opts provisioner.SSHOptions
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
}{
2019-09-28 02:05:53 +00:00
{"ok-user", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions}}, want{CertType: ssh.UserCert}, false},
{"ok-host", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{hostOptions}}, want{CertType: ssh.HostCert}, false},
{"ok-opts-type-user", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert}, false},
{"ok-opts-type-host", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert}, false},
{"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "user", Principals: []string{"user"}}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, Principals: []string{"user"}}, false},
{"ok-opts-principals", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "host", Principals: []string{"foo.test.com", "bar.test.com"}}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, Principals: []string{"foo.test.com", "bar.test.com"}}, false},
{"ok-opts-valid-after", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "user", ValidAfter: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.UserCert, ValidAfter: uint64(now.Unix())}, false},
{"ok-opts-valid-before", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "host", ValidBefore: provisioner.NewTimeDuration(now)}, []provisioner.SignOption{}}, want{CertType: ssh.HostCert, ValidBefore: uint64(now.Unix())}, false},
{"ok-cert-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("")}}, want{CertType: ssh.UserCert}, false},
{"ok-cert-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("")}}, want{CertType: ssh.UserCert}, false},
{"ok-opts-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("")}}, want{CertType: ssh.UserCert}, false},
{"ok-opts-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("")}}, want{CertType: ssh.UserCert}, false},
{"fail-opts-type", fields{signer, signer}, args{pub, provisioner.SSHOptions{CertType: "foo"}, []provisioner.SignOption{}}, want{}, true},
{"fail-cert-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertValidator("an error")}}, want{}, true},
{"fail-cert-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestCertModifier("an error")}}, want{}, true},
{"fail-opts-validator", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsValidator("an error")}}, want{}, true},
{"fail-opts-modifier", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, sshTestOptionsModifier("an error")}}, want{}, true},
{"fail-bad-sign-options", fields{signer, signer}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{userOptions, "wrong type"}}, want{}, true},
{"fail-no-user-key", fields{nil, signer}, args{pub, provisioner.SSHOptions{CertType: "user"}, []provisioner.SignOption{}}, want{}, true},
{"fail-no-host-key", fields{signer, nil}, args{pub, provisioner.SSHOptions{CertType: "host"}, []provisioner.SignOption{}}, want{}, true},
{"fail-bad-type", fields{signer, nil}, args{pub, provisioner.SSHOptions{}, []provisioner.SignOption{sshTestModifier{CertType: 0}}}, want{}, true},
}
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
got, err := a.SignSSH(tt.args.key, tt.args.opts, tt.args.signOpts...)
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)
type fields struct {
2019-09-28 02:05:53 +00:00
sshCAUserCertSignKey ssh.Signer
sshCAHostCertSignKey ssh.Signer
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},
}
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,
}
got, err := a.SignSSHAddUser(tt.args.key, tt.args.subject)
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
got, err := a.GetSSHRoots()
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
got, err := a.GetSSHFederation()
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)},
}
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},
{"host", fields{tmplConfig, userSigner, hostSigner}, args{"host", nil}, hostOutput, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := testAuthority(t)
a.config.Templates = tt.fields.templates
a.sshCAUserCertSignKey = tt.fields.userSigner
a.sshCAHostCertSignKey = tt.fields.hostSigner
got, err := a.GetSSHConfig(tt.args.typ, tt.args.data)
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)
}
})
}
}