blob: b28b0b54a8db37d4d00f486ea3ffefa539c59c1d [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Tim Windelschmidtb6308cd2023-10-10 21:19:03 +02004package manager
5
6import (
7 "crypto/ed25519"
8 "crypto/rand"
9 "flag"
10 "fmt"
11 "os"
12 "sync"
13
14 "golang.org/x/crypto/ssh"
15 "k8s.io/klog/v2"
16)
17
18type SSHKey struct {
19 // myKey guards Key.
20 muKey sync.Mutex
21
22 // SSH key to use when creating machines and then connecting to them. If not
23 // provided, it will be automatically loaded from KeyPersistPath, and if that
24 // doesn't exist either, it will be first generated and persisted there.
25 Key ed25519.PrivateKey
26
27 // Path at which the SSH key will be loaded from and persisted to, if Key is not
28 // explicitly set. Either KeyPersistPath or Key must be set.
29 KeyPersistPath string
30}
31
32func (c *SSHKey) RegisterFlags() {
33 flag.StringVar(&c.KeyPersistPath, "ssh_key_path", "", "Local filesystem path to read SSH key from, and save generated key to")
34}
35
36// sshKey returns the SSH key as defined by the Key and KeyPersistPath options,
37// loading/generating/persisting it as necessary.
38func (c *SSHKey) sshKey() (ed25519.PrivateKey, error) {
39 c.muKey.Lock()
40 defer c.muKey.Unlock()
41
42 if c.Key != nil {
43 return c.Key, nil
44 }
45 if c.KeyPersistPath == "" {
46 return nil, fmt.Errorf("-ssh_key_path must be set")
47 }
48
49 data, err := os.ReadFile(c.KeyPersistPath)
50 switch {
51 case err == nil:
52 if len(data) != ed25519.PrivateKeySize {
53 return nil, fmt.Errorf("%s is not a valid ed25519 private key", c.KeyPersistPath)
54 }
55 c.Key = data
56 klog.Infof("Loaded SSH key from %s", c.KeyPersistPath)
57 return c.Key, nil
58 case os.IsNotExist(err):
59 if err := c.sshGenerateUnlocked(); err != nil {
60 return nil, err
61 }
62 if err := os.WriteFile(c.KeyPersistPath, c.Key, 0400); err != nil {
63 return nil, fmt.Errorf("could not persist key: %w", err)
64 }
65 return c.Key, nil
66 default:
67 return nil, fmt.Errorf("could not load peristed key: %w", err)
68 }
69}
70
71// PublicKey returns the SSH public key marshaled for use, based on sshKey.
72func (c *SSHKey) PublicKey() (string, error) {
73 private, err := c.sshKey()
74 if err != nil {
75 return "", err
76 }
77 // Marshal the public key part in OpenSSH authorized_keys.
78 sshpub, err := ssh.NewPublicKey(private.Public())
79 if err != nil {
80 return "", fmt.Errorf("while building SSH public key: %w", err)
81 }
82 return string(ssh.MarshalAuthorizedKey(sshpub)), nil
83}
84
85// Signer builds an ssh.Signer (for use in SSH connections) based on sshKey.
86func (c *SSHKey) Signer() (ssh.Signer, error) {
87 private, err := c.sshKey()
88 if err != nil {
89 return nil, err
90 }
91 // Set up the internal ssh.Signer to be later used to initiate SSH
92 // connections with newly provided hosts.
93 signer, err := ssh.NewSignerFromKey(private)
94 if err != nil {
95 return nil, fmt.Errorf("while building SSH signer: %w", err)
96 }
97 return signer, nil
98}
99
100// sshGenerateUnlocked saves a new private key into SharedConfig.Key.
101func (c *SSHKey) sshGenerateUnlocked() error {
102 if c.Key != nil {
103 return nil
104 }
105 _, priv, err := ed25519.GenerateKey(rand.Reader)
106 if err != nil {
107 return fmt.Errorf("while generating SSH key: %w", err)
108 }
109 c.Key = priv
110 return nil
111}