Skip to content

Commit d766e96

Browse files
jooolaapricote
andauthored
feat(exp): add ssh key functions (#441)
This adds a few ssh key utilities in the ssh kit package. The functions are used in multiple Hetzner Cloud projects. --------- Co-authored-by: Julian Tölle <[email protected]>
1 parent b07d7ad commit d766e96

File tree

4 files changed

+143
-0
lines changed

4 files changed

+143
-0
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/prometheus/client_golang v1.19.1
1313
github.com/stretchr/testify v1.9.0
1414
github.com/vburenin/ifacemaker v1.2.1
15+
golang.org/x/crypto v0.23.0
1516
golang.org/x/net v0.25.0
1617
)
1718

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
3535
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
3636
github.com/vburenin/ifacemaker v1.2.1 h1:3Vq8B/bfBgjWTkv+jDg4dVL1KHt3k1K4lO7XRxYA2sk=
3737
github.com/vburenin/ifacemaker v1.2.1/go.mod h1:5WqrzX2aD7/hi+okBjcaEQJMg4lDGrpuEX3B8L4Wgrs=
38+
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
39+
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
3840
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
3941
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
4042
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=

hcloud/exp/kit/ssh/ssh_key.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package sshkey
2+
3+
import (
4+
"crypto"
5+
"crypto/ed25519"
6+
"encoding/pem"
7+
"fmt"
8+
9+
"golang.org/x/crypto/ssh"
10+
)
11+
12+
// GenerateKeyPair generates a new ed25519 ssh key pair, and returns the private key and
13+
// the public key respectively.
14+
func GenerateKeyPair() ([]byte, []byte, error) {
15+
pub, priv, err := ed25519.GenerateKey(nil)
16+
if err != nil {
17+
return nil, nil, fmt.Errorf("could not generate key pair: %w", err)
18+
}
19+
20+
privBytes, err := encodePrivateKey(priv)
21+
if err != nil {
22+
return nil, nil, fmt.Errorf("could not encode private key: %w", err)
23+
}
24+
25+
pubBytes, err := encodePublicKey(pub)
26+
if err != nil {
27+
return nil, nil, fmt.Errorf("could not encode public key: %w", err)
28+
}
29+
30+
return privBytes, pubBytes, nil
31+
}
32+
33+
func encodePrivateKey(priv crypto.PrivateKey) ([]byte, error) {
34+
privPem, err := ssh.MarshalPrivateKey(priv, "")
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
return pem.EncodeToMemory(privPem), nil
40+
}
41+
42+
func encodePublicKey(pub crypto.PublicKey) ([]byte, error) {
43+
sshPub, err := ssh.NewPublicKey(pub)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
return ssh.MarshalAuthorizedKey(sshPub), nil
49+
}
50+
51+
type privateKeyWithPublicKey interface {
52+
crypto.PrivateKey
53+
Public() crypto.PublicKey
54+
}
55+
56+
// GeneratePublicKey generate a public key from the provided private key.
57+
func GeneratePublicKey(privBytes []byte) ([]byte, error) {
58+
priv, err := ssh.ParseRawPrivateKey(privBytes)
59+
if err != nil {
60+
return nil, fmt.Errorf("could not decode private key: %w", err)
61+
}
62+
63+
key, ok := priv.(privateKeyWithPublicKey)
64+
if !ok {
65+
return nil, fmt.Errorf("private key doesn't export Public() crypto.PublicKey")
66+
}
67+
68+
pubBytes, err := encodePublicKey(key.Public())
69+
if err != nil {
70+
return nil, fmt.Errorf("could not encode public key: %w", err)
71+
}
72+
73+
return pubBytes, nil
74+
}
75+
76+
// GetPublicKeyFingerprint generate the finger print for the provided public key.
77+
func GetPublicKeyFingerprint(pubBytes []byte) (string, error) {
78+
pub, _, _, _, err := ssh.ParseAuthorizedKey(pubBytes)
79+
if err != nil {
80+
return "", fmt.Errorf("could not decode public key: %w", err)
81+
}
82+
83+
fingerprint := ssh.FingerprintLegacyMD5(pub)
84+
85+
return fingerprint, nil
86+
}

hcloud/exp/kit/ssh/ssh_key_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package sshkey
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestGenerateKeyPair(t *testing.T) {
12+
privBytes, pubBytes, err := GenerateKeyPair()
13+
assert.Nil(t, err)
14+
15+
priv := string(privBytes)
16+
pub := string(pubBytes)
17+
18+
if !(strings.HasPrefix(priv, "-----BEGIN OPENSSH PRIVATE KEY-----\n") &&
19+
strings.HasSuffix(priv, "-----END OPENSSH PRIVATE KEY-----\n")) {
20+
assert.Fail(t, "private key is invalid", priv)
21+
}
22+
23+
if !strings.HasPrefix(pub, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA") {
24+
assert.Fail(t, "public key is invalid", pub)
25+
}
26+
}
27+
28+
func TestGeneratePublicKey(t *testing.T) {
29+
privBytes, pubBytesOrig, err := GenerateKeyPair()
30+
require.NoError(t, err)
31+
32+
pubBytes, err := GeneratePublicKey(privBytes)
33+
require.NoError(t, err)
34+
35+
pub := string(pubBytes)
36+
priv := string(privBytes)
37+
38+
if !(strings.HasPrefix(priv, "-----BEGIN OPENSSH PRIVATE KEY-----\n") &&
39+
strings.HasSuffix(priv, "-----END OPENSSH PRIVATE KEY-----\n")) {
40+
assert.Fail(t, "private key is invalid", priv)
41+
}
42+
43+
if !strings.HasPrefix(pub, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA") {
44+
assert.Fail(t, "public key is invalid", pub)
45+
}
46+
47+
assert.Equal(t, pubBytesOrig, pubBytes)
48+
}
49+
50+
func TestGetPublicKeyFingerprint(t *testing.T) {
51+
fingerprint, err := GetPublicKeyFingerprint([]byte(`ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIccHCW76xx2rrPAUrjnuT6IjpEF1O+/U4IByVgv99Oi`))
52+
require.NoError(t, err)
53+
assert.Equal(t, "77:79:69:b1:4d:c6:b6:45:6a:e9:52:29:04:3e:59:48", fingerprint)
54+
}

0 commit comments

Comments
 (0)