Implement validator for ssh keys.

Fixes #100
This commit is contained in:
Mariano Cano 2019-09-10 17:04:13 -07:00
parent 4c390dcfe1
commit 396b4222aa
11 changed files with 184 additions and 51 deletions

View file

@ -470,6 +470,8 @@ func (p *AWS) authorizeSSHSign(claims *awsPayload) ([]SignOption, error) {
&sshDefaultExtensionModifier{}, &sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set // checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{p.claimer}, &sshCertificateValidityModifier{p.claimer},
// validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate // require all the fields in the SSH certificate
&sshCertificateDefaultValidator{}, &sshCertificateDefaultValidator{},
), nil ), nil

View file

@ -377,6 +377,12 @@ func TestAWS_AuthorizeSign_SSH(t *testing.T) {
signer, err := generateJSONWebKey() signer, err := generateJSONWebKey()
assert.FatalError(t, err) assert.FatalError(t, err)
pub := key.Public().Key
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
assert.FatalError(t, err)
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
assert.FatalError(t, err)
hostDuration := p1.claimer.DefaultHostSSHCertDuration() hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedHostOptions := &SSHOptions{ expectedHostOptions := &SSHOptions{
CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}, CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"},
@ -394,6 +400,7 @@ func TestAWS_AuthorizeSign_SSH(t *testing.T) {
type args struct { type args struct {
token string token string
sshOpts SSHOptions sshOpts SSHOptions
key interface{}
} }
tests := []struct { tests := []struct {
name string name string
@ -403,15 +410,17 @@ func TestAWS_AuthorizeSign_SSH(t *testing.T) {
wantErr bool wantErr bool
wantSignErr bool wantSignErr bool
}{ }{
{"ok", p1, args{t1, SSHOptions{}}, expectedHostOptions, false, false}, {"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}}, expectedHostOptions, false, false}, {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}}, expectedHostOptions, false, false}, {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false},
{"ok-principal-ip", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1"}}}, expectedHostOptionsIP, false, false}, {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, false, false},
{"ok-principal-hostname", p1, args{t1, SSHOptions{Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}}}, expectedHostOptionsHostname, false, false}, {"ok-principal-ip", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1"}}, pub}, expectedHostOptionsIP, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}}, expectedHostOptions, false, false}, {"ok-principal-hostname", p1, args{t1, SSHOptions{Principals: []string{"ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptionsHostname, false, false},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}}, nil, false, true}, {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal"}}, pub}, expectedHostOptions, false, false},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}}, nil, false, true}, {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal", "smallstep.com"}}}, nil, false, true}, {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"127.0.0.1", "ip-127-0-0-1.us-west-1.compute.internal", "smallstep.com"}}, pub}, nil, false, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -424,7 +433,7 @@ func TestAWS_AuthorizeSign_SSH(t *testing.T) {
if err != nil { if err != nil {
assert.Nil(t, got) assert.Nil(t, got)
} else if assert.NotNil(t, got) { } else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(key.Public().Key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
if (err != nil) != tt.wantSignErr { if (err != nil) != tt.wantSignErr {
t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr) t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr)
} else { } else {

View file

@ -327,6 +327,8 @@ func (p *Azure) authorizeSSHSign(claims azurePayload, name string) ([]SignOption
&sshDefaultExtensionModifier{}, &sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set // checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{p.claimer}, &sshCertificateValidityModifier{p.claimer},
// validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate // require all the fields in the SSH certificate
&sshCertificateDefaultValidator{}, &sshCertificateDefaultValidator{},
), nil ), nil

View file

@ -3,6 +3,8 @@ package provisioner
import ( import (
"context" "context"
"crypto" "crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
@ -325,6 +327,12 @@ func TestAzure_AuthorizeSign_SSH(t *testing.T) {
signer, err := generateJSONWebKey() signer, err := generateJSONWebKey()
assert.FatalError(t, err) assert.FatalError(t, err)
pub := key.Public().Key
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
assert.FatalError(t, err)
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
assert.FatalError(t, err)
hostDuration := p1.claimer.DefaultHostSSHCertDuration() hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedHostOptions := &SSHOptions{ expectedHostOptions := &SSHOptions{
CertType: "host", Principals: []string{"virtualMachine"}, CertType: "host", Principals: []string{"virtualMachine"},
@ -334,6 +342,7 @@ func TestAzure_AuthorizeSign_SSH(t *testing.T) {
type args struct { type args struct {
token string token string
sshOpts SSHOptions sshOpts SSHOptions
key interface{}
} }
tests := []struct { tests := []struct {
name string name string
@ -343,13 +352,15 @@ func TestAzure_AuthorizeSign_SSH(t *testing.T) {
wantErr bool wantErr bool
wantSignErr bool wantSignErr bool
}{ }{
{"ok", p1, args{t1, SSHOptions{}}, expectedHostOptions, false, false}, {"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}}, expectedHostOptions, false, false}, {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine"}}}, expectedHostOptions, false, false}, {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"virtualMachine"}}}, expectedHostOptions, false, false}, {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, false, false},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}}, nil, false, true}, {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"virtualMachine"}}, pub}, expectedHostOptions, false, false},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}}, nil, false, true}, {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine", "smallstep.com"}}}, nil, false, true}, {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"virtualMachine", "smallstep.com"}}, pub}, nil, false, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -362,7 +373,7 @@ func TestAzure_AuthorizeSign_SSH(t *testing.T) {
if err != nil { if err != nil {
assert.Nil(t, got) assert.Nil(t, got)
} else if assert.NotNil(t, got) { } else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(key.Public().Key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
if (err != nil) != tt.wantSignErr { if (err != nil) != tt.wantSignErr {
t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr) t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr)
} else { } else {

View file

@ -382,6 +382,8 @@ func (p *GCP) authorizeSSHSign(claims *gcpPayload) ([]SignOption, error) {
&sshDefaultExtensionModifier{}, &sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set // checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{p.claimer}, &sshCertificateValidityModifier{p.claimer},
// validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate // require all the fields in the SSH certificate
&sshCertificateDefaultValidator{}, &sshCertificateDefaultValidator{},
), nil ), nil

View file

@ -3,6 +3,8 @@ package provisioner
import ( import (
"context" "context"
"crypto" "crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
@ -362,6 +364,12 @@ func TestGCP_AuthorizeSign_SSH(t *testing.T) {
signer, err := generateJSONWebKey() signer, err := generateJSONWebKey()
assert.FatalError(t, err) assert.FatalError(t, err)
pub := key.Public().Key
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
assert.FatalError(t, err)
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
assert.FatalError(t, err)
hostDuration := p1.claimer.DefaultHostSSHCertDuration() hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedHostOptions := &SSHOptions{ expectedHostOptions := &SSHOptions{
CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}, CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"},
@ -379,6 +387,7 @@ func TestGCP_AuthorizeSign_SSH(t *testing.T) {
type args struct { type args struct {
token string token string
sshOpts SSHOptions sshOpts SSHOptions
key interface{}
} }
tests := []struct { tests := []struct {
name string name string
@ -388,15 +397,17 @@ func TestGCP_AuthorizeSign_SSH(t *testing.T) {
wantErr bool wantErr bool
wantSignErr bool wantSignErr bool
}{ }{
{"ok", p1, args{t1, SSHOptions{}}, expectedHostOptions, false, false}, {"ok", p1, args{t1, SSHOptions{}, pub}, expectedHostOptions, false, false},
{"ok-type", p1, args{t1, SSHOptions{CertType: "host"}}, expectedHostOptions, false, false}, {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedHostOptions, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}}, expectedHostOptions, false, false}, {"ok-type", p1, args{t1, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false},
{"ok-principal1", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}}, expectedHostOptionsPrincipal1, false, false}, {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, false, false},
{"ok-principal2", p1, args{t1, SSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}}, expectedHostOptionsPrincipal2, false, false}, {"ok-principal1", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal1, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}}, expectedHostOptions, false, false}, {"ok-principal2", p1, args{t1, SSHOptions{Principals: []string{"instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptionsPrincipal2, false, false},
{"fail-type", p1, args{t1, SSHOptions{CertType: "user"}}, nil, false, true}, {"ok-options", p1, args{t1, SSHOptions{CertType: "host", Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal"}}, pub}, expectedHostOptions, false, false},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}}, nil, false, true}, {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedHostOptions, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}}, nil, false, true}, {"fail-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, nil, false, true},
{"fail-principal", p1, args{t1, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, nil, false, true},
{"fail-extra-principal", p1, args{t1, SSHOptions{Principals: []string{"instance-name.c.project-id.internal", "instance-name.zone.c.project-id.internal", "smallstep.com"}}, pub}, nil, false, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -409,7 +420,7 @@ func TestGCP_AuthorizeSign_SSH(t *testing.T) {
if err != nil { if err != nil {
assert.Nil(t, got) assert.Nil(t, got)
} else if assert.NotNil(t, got) { } else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(key.Public().Key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
if (err != nil) != tt.wantSignErr { if (err != nil) != tt.wantSignErr {
t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr) t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr)
} else { } else {

View file

@ -210,6 +210,8 @@ func (p *JWK) authorizeSSHSign(claims *jwtPayload) ([]SignOption, error) {
&sshDefaultExtensionModifier{}, &sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set // checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{p.claimer}, &sshCertificateValidityModifier{p.claimer},
// validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate // require all the fields in the SSH certificate
&sshCertificateDefaultValidator{}, &sshCertificateDefaultValidator{},
), nil ), nil

View file

@ -3,6 +3,8 @@ package provisioner
import ( import (
"context" "context"
"crypto" "crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"errors" "errors"
"strings" "strings"
@ -356,6 +358,12 @@ func TestJWK_AuthorizeSign_SSH(t *testing.T) {
signer, err := generateJSONWebKey() signer, err := generateJSONWebKey()
assert.FatalError(t, err) assert.FatalError(t, err)
pub := key.Public().Key
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
assert.FatalError(t, err)
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
assert.FatalError(t, err)
userDuration := p1.claimer.DefaultUserSSHCertDuration() userDuration := p1.claimer.DefaultUserSSHCertDuration()
hostDuration := p1.claimer.DefaultHostSSHCertDuration() hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedUserOptions := &SSHOptions{ expectedUserOptions := &SSHOptions{
@ -370,6 +378,7 @@ func TestJWK_AuthorizeSign_SSH(t *testing.T) {
type args struct { type args struct {
token string token string
sshOpts SSHOptions sshOpts SSHOptions
key interface{}
} }
tests := []struct { tests := []struct {
name string name string
@ -379,15 +388,17 @@ func TestJWK_AuthorizeSign_SSH(t *testing.T) {
wantErr bool wantErr bool
wantSignErr bool wantSignErr bool
}{ }{
{"user", p1, args{t1, SSHOptions{}}, expectedUserOptions, false, false}, {"user", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, false, false},
{"user-type", p1, args{t1, SSHOptions{CertType: "user"}}, expectedUserOptions, false, false}, {"user-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, false, false},
{"user-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}}, expectedUserOptions, false, false}, {"user-type", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, false, false},
{"user-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}}, expectedUserOptions, false, false}, {"user-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, expectedUserOptions, false, false},
{"host", p1, args{t2, SSHOptions{}}, expectedHostOptions, false, false}, {"user-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, false, false},
{"host-type", p1, args{t2, SSHOptions{CertType: "host"}}, expectedHostOptions, false, false}, {"host", p1, args{t2, SSHOptions{}, pub}, expectedHostOptions, false, false},
{"host-principals", p1, args{t2, SSHOptions{Principals: []string{"smallstep.com"}}}, expectedHostOptions, false, false}, {"host-type", p1, args{t2, SSHOptions{CertType: "host"}, pub}, expectedHostOptions, false, false},
{"host-options", p1, args{t2, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}}, expectedHostOptions, false, false}, {"host-principals", p1, args{t2, SSHOptions{Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false},
{"fail-signature", p1, args{failSig, SSHOptions{}}, nil, true, false}, {"host-options", p1, args{t2, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false},
{"fail-signature", p1, args{failSig, SSHOptions{}, pub}, nil, true, false},
{"rail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, false, true},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -400,7 +411,7 @@ func TestJWK_AuthorizeSign_SSH(t *testing.T) {
if err != nil { if err != nil {
assert.Nil(t, got) assert.Nil(t, got)
} else if assert.NotNil(t, got) { } else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(key.Public().Key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
if (err != nil) != tt.wantSignErr { if (err != nil) != tt.wantSignErr {
t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr) t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr)
} else { } else {

View file

@ -336,6 +336,8 @@ func (o *OIDC) authorizeSSHSign(claims *openIDPayload) ([]SignOption, error) {
&sshDefaultExtensionModifier{}, &sshDefaultExtensionModifier{},
// checks the validity bounds, and set the validity if has not been set // checks the validity bounds, and set the validity if has not been set
&sshCertificateValidityModifier{o.claimer}, &sshCertificateValidityModifier{o.claimer},
// validate public key
&sshDefaultPublicKeyValidator{},
// require all the fields in the SSH certificate // require all the fields in the SSH certificate
&sshCertificateDefaultValidator{}, &sshCertificateDefaultValidator{},
), nil ), nil

View file

@ -3,6 +3,8 @@ package provisioner
import ( import (
"context" "context"
"crypto" "crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"strings" "strings"
@ -343,6 +345,12 @@ func TestOIDC_AuthorizeSign_SSH(t *testing.T) {
signer, err := generateJSONWebKey() signer, err := generateJSONWebKey()
assert.FatalError(t, err) assert.FatalError(t, err)
pub := key.Public().Key
rsa2048, err := rsa.GenerateKey(rand.Reader, 2048)
assert.FatalError(t, err)
rsa1024, err := rsa.GenerateKey(rand.Reader, 1024)
assert.FatalError(t, err)
userDuration := p1.claimer.DefaultUserSSHCertDuration() userDuration := p1.claimer.DefaultUserSSHCertDuration()
hostDuration := p1.claimer.DefaultHostSSHCertDuration() hostDuration := p1.claimer.DefaultHostSSHCertDuration()
expectedUserOptions := &SSHOptions{ expectedUserOptions := &SSHOptions{
@ -361,6 +369,7 @@ func TestOIDC_AuthorizeSign_SSH(t *testing.T) {
type args struct { type args struct {
token string token string
sshOpts SSHOptions sshOpts SSHOptions
key interface{}
} }
tests := []struct { tests := []struct {
name string name string
@ -370,18 +379,20 @@ func TestOIDC_AuthorizeSign_SSH(t *testing.T) {
wantErr bool wantErr bool
wantSignErr bool wantSignErr bool
}{ }{
{"ok", p1, args{t1, SSHOptions{}}, expectedUserOptions, false, false}, {"ok", p1, args{t1, SSHOptions{}, pub}, expectedUserOptions, false, false},
{"ok-user", p1, args{t1, SSHOptions{CertType: "user"}}, expectedUserOptions, false, false}, {"ok-rsa2048", p1, args{t1, SSHOptions{}, rsa2048.Public()}, expectedUserOptions, false, false},
{"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}}, expectedUserOptions, false, false}, {"ok-user", p1, args{t1, SSHOptions{CertType: "user"}, pub}, expectedUserOptions, false, false},
{"ok-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}}, expectedUserOptions, false, false}, {"ok-principals", p1, args{t1, SSHOptions{Principals: []string{"name"}}, pub}, expectedUserOptions, false, false},
{"admin", p3, args{okAdmin, SSHOptions{}}, expectedAdminOptions, false, false}, {"ok-options", p1, args{t1, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, false, false},
{"admin-user", p3, args{okAdmin, SSHOptions{CertType: "user"}}, expectedAdminOptions, false, false}, {"admin", p3, args{okAdmin, SSHOptions{}, pub}, expectedAdminOptions, false, false},
{"admin-principals", p3, args{okAdmin, SSHOptions{Principals: []string{"root"}}}, expectedAdminOptions, false, false}, {"admin-user", p3, args{okAdmin, SSHOptions{CertType: "user"}, pub}, expectedAdminOptions, false, false},
{"admin-options", p3, args{okAdmin, SSHOptions{CertType: "user", Principals: []string{"name"}}}, expectedUserOptions, false, false}, {"admin-principals", p3, args{okAdmin, SSHOptions{Principals: []string{"root"}}, pub}, expectedAdminOptions, false, false},
{"admin-host", p3, args{okAdmin, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}}, expectedHostOptions, false, false}, {"admin-options", p3, args{okAdmin, SSHOptions{CertType: "user", Principals: []string{"name"}}, pub}, expectedUserOptions, false, false},
{"fail-user-host", p1, args{t1, SSHOptions{CertType: "host"}}, nil, false, true}, {"admin-host", p3, args{okAdmin, SSHOptions{CertType: "host", Principals: []string{"smallstep.com"}}, pub}, expectedHostOptions, false, false},
{"fail-user-principals", p1, args{t1, SSHOptions{Principals: []string{"root"}}}, nil, false, true}, {"fail-rsa1024", p1, args{t1, SSHOptions{}, rsa1024.Public()}, expectedUserOptions, false, true},
{"fail-email", p3, args{failEmail, SSHOptions{}}, nil, true, false}, {"fail-user-host", p1, args{t1, SSHOptions{CertType: "host"}, pub}, nil, false, true},
{"fail-user-principals", p1, args{t1, SSHOptions{Principals: []string{"root"}}, pub}, nil, false, true},
{"fail-email", p3, args{failEmail, SSHOptions{}, pub}, nil, true, false},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@ -394,7 +405,7 @@ func TestOIDC_AuthorizeSign_SSH(t *testing.T) {
if err != nil { if err != nil {
assert.Nil(t, got) assert.Nil(t, got)
} else if assert.NotNil(t, got) { } else if assert.NotNil(t, got) {
cert, err := signSSHCertificate(key.Public().Key, tt.args.sshOpts, got, signer.Key.(crypto.Signer)) cert, err := signSSHCertificate(tt.args.key, tt.args.sshOpts, got, signer.Key.(crypto.Signer))
if (err != nil) != tt.wantSignErr { if (err != nil) != tt.wantSignErr {
t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr) t.Errorf("SignSSH error = %v, wantSignErr %v", err, tt.wantSignErr)
} else { } else {

View file

@ -1,6 +1,9 @@
package provisioner package provisioner
import ( import (
"crypto/rsa"
"encoding/binary"
"math/big"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -275,6 +278,35 @@ func (v *sshCertificateDefaultValidator) Valid(cert *ssh.Certificate) error {
} }
} }
// sshDefaultPublicKeyValidator implements a validator for the certificate key.
type sshDefaultPublicKeyValidator struct{}
// Valid checks that certificate request common name matches the one configured.
func (v sshDefaultPublicKeyValidator) Valid(cert *ssh.Certificate) error {
if cert.Key == nil {
return errors.New("ssh certificate key cannot be nil")
}
switch cert.Key.Type() {
case ssh.KeyAlgoRSA:
_, in, ok := sshParseString(cert.Key.Marshal())
if !ok {
return errors.New("ssh certificate key is invalid")
}
key, err := sshParseRSAPublicKey(in)
if err != nil {
return err
}
if key.Size() < 256 {
return errors.New("ssh certificate key must be at least 2048 bits (256 bytes)")
}
return nil
case ssh.KeyAlgoDSA:
return errors.New("ssh certificate key algorithm (DSA) is not supported")
default:
return nil
}
}
// sshCertTypeUInt32 // sshCertTypeUInt32
func sshCertTypeUInt32(ct string) uint32 { func sshCertTypeUInt32(ct string) uint32 {
switch ct { switch ct {
@ -304,3 +336,41 @@ func containsAllMembers(group, subgroup []string) bool {
} }
return true return true
} }
func sshParseString(in []byte) (out, rest []byte, ok bool) {
if len(in) < 4 {
return
}
length := binary.BigEndian.Uint32(in)
in = in[4:]
if uint32(len(in)) < length {
return
}
out = in[:length]
rest = in[length:]
ok = true
return
}
func sshParseRSAPublicKey(in []byte) (*rsa.PublicKey, error) {
var w struct {
E *big.Int
N *big.Int
Rest []byte `ssh:"rest"`
}
if err := ssh.Unmarshal(in, &w); err != nil {
return nil, errors.Wrap(err, "error unmarshalling public key")
}
if w.E.BitLen() > 24 {
return nil, errors.New("invalid public key: exponent too large")
}
e := w.E.Int64()
if e < 3 || e&1 == 0 {
return nil, errors.New("invalid public key: incorrect exponent")
}
var key rsa.PublicKey
key.E = int(e)
key.N = w.N
return &key, nil
}