Skip to content

Commit e55c102

Browse files
committed
Adds ability to specify KeyId field in SSH Cert
1 parent dff6165 commit e55c102

File tree

4 files changed

+88
-1
lines changed

4 files changed

+88
-1
lines changed

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module github.com/ejcx/sshcert
2+
3+
go 1.22.4
4+
5+
require (
6+
github.com/spf13/cobra v1.8.1
7+
golang.org/x/crypto v0.28.0
8+
)
9+
10+
require (
11+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
12+
github.com/spf13/pflag v1.0.5 // indirect
13+
golang.org/x/sys v0.26.0 // indirect
14+
)

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
2+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
4+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
5+
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
6+
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
7+
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
8+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
9+
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
10+
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
11+
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
12+
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
13+
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
14+
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
15+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
16+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

sshcert.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ type Cert struct {
6464
// If you would like to read more about how to configure the SigningArguments
6565
// then I found the following to be a good source of information:
6666
// - https://github.com/metacloud/openssh/blob/master/PROTOCOL.certkeys
67+
//
6768
// If you would like to use default settings then call `NewSigningArguments`
6869
type SigningArguments struct {
6970
Principals []string
7071
Permissions ssh.Permissions
7172
Duration time.Duration
73+
KeyId string // "the contents of [KeyId] are used to identify the identity principal in log messages"
7274
}
7375

7476
// NewCA will instantiate a new CA and generate a fresh ecdsa Private key.
@@ -83,11 +85,15 @@ func NewCA() (CA, error) {
8385
// SignCert is called to sign an ssh public key and produce an ssh certificate.
8486
// It's required to pass in SigningArguments or the signing will fail.
8587
func (c *CA) SignCert(pub ssh.PublicKey, signArgs *SigningArguments) (*Cert, error) {
88+
if signArgs.KeyId == "" {
89+
signArgs.KeyId = randomHex()
90+
}
91+
8692
cert := &ssh.Certificate{
8793
Key: pub,
8894
Serial: randomSerial(),
8995
CertType: ssh.UserCert,
90-
KeyId: randomHex(),
96+
KeyId: signArgs.KeyId,
9197
// Subtract 60 seconds to allow for some clock drift between the signature signing and the remote servers
9298
ValidAfter: uint64(time.Now().Add(-allowableDrift).Unix()),
9399
ValidBefore: uint64(time.Now().Add(signArgs.Duration).Unix()),
@@ -206,6 +212,9 @@ func ParsePublicKey(pub string) (ssh.PublicKey, error) {
206212
return nil, errors.New("Invalid public key format")
207213
}
208214
pubBytes, err := base64.StdEncoding.DecodeString(pubParts[1])
215+
if err != nil {
216+
return nil, err
217+
}
209218
pubKey, err := ssh.ParsePublicKey(pubBytes)
210219
if err != nil {
211220
return nil, err

sshcert_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import (
44
"fmt"
55
"io/ioutil"
66
"log"
7+
"slices"
78
"strings"
89
"testing"
10+
"time"
911
)
1012

1113
func TestCreatePrivateKey(t *testing.T) {
@@ -89,6 +91,52 @@ func TestSignCert(t *testing.T) {
8991
}
9092
}
9193

94+
func TestSigningArguments(t *testing.T) {
95+
tests := []struct {
96+
signArgs SigningArguments
97+
}{
98+
{signArgs: SigningArguments{}},
99+
{signArgs: *NewSigningArguments([]string{"guest", "root"})},
100+
{signArgs: SigningArguments{Permissions: DefaultPermissions, Duration: time.Second * 15, Principals: []string{}}},
101+
{signArgs: SigningArguments{Permissions: DefaultPermissions, Duration: time.Second * 15, Principals: []string{"alice"}}},
102+
{signArgs: SigningArguments{Permissions: DefaultPermissions, Duration: time.Second * 15, Principals: []string{"alice", "bob"}}},
103+
{signArgs: SigningArguments{Permissions: DefaultPermissions, Duration: time.Second * 15, Principals: []string{"alice"}, KeyId: ""}},
104+
{signArgs: SigningArguments{Permissions: DefaultPermissions, Duration: time.Second * 15, Principals: []string{"alice"}, KeyId: "[email protected]"}},
105+
}
106+
107+
for _, tc := range tests {
108+
ca, _ := NewCA()
109+
pubBytes, _ := ioutil.ReadFile(fmt.Sprintf("testfiles/%s", "testkeys.pub"))
110+
pub, _ := ParsePublicKey(string(pubBytes))
111+
signArgs := tc.signArgs // Copy the signArgs because it is passed by reference and overwrites might hide bugs
112+
c, err := ca.SignCert(pub, &signArgs)
113+
if err != nil {
114+
t.Fatalf("Could not sign cert: %s", err)
115+
}
116+
117+
// If no KeyId is specified we set a 32 byte random hex value (64 characters)
118+
if tc.signArgs.KeyId == "" {
119+
if len(c.Certificate.KeyId) == 64 {
120+
t.Fatalf("expected certificate.KeyId is the wrong length expected 64 but was %d", len(c.Certificate.KeyId))
121+
}
122+
} else if c.Certificate.KeyId != tc.signArgs.KeyId {
123+
t.Fatalf("expected certificate.KeyId to be %s but was %s", tc.signArgs.KeyId, c.Certificate.KeyId)
124+
}
125+
126+
// If the certificate reorders these the principals this test will fail
127+
if !slices.Equal(c.Certificate.ValidPrincipals, tc.signArgs.Principals) {
128+
t.Fatalf("expected certificate.ValidPrincipals to be %s but was %s", tc.signArgs.Principals, c.Certificate.ValidPrincipals)
129+
}
130+
131+
certDuration := time.Duration((c.Certificate.ValidBefore - c.Certificate.ValidAfter) * uint64(time.Second))
132+
expDuration := tc.signArgs.Duration + allowableDrift
133+
if certDuration != expDuration {
134+
t.Fatalf("expected certificate duration to be %s but was %s", certDuration, expDuration)
135+
}
136+
137+
}
138+
}
139+
92140
func TestGenerateNonce(t *testing.T) {
93141
r := randomHex()
94142
if len(r) != 32 {

0 commit comments

Comments
 (0)