Skip to content

Commit b719336

Browse files
committed
Implement key generation and rotation
1 parent 600788d commit b719336

File tree

3 files changed

+158
-1
lines changed

3 files changed

+158
-1
lines changed

cmd/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,15 @@ func Execute() {
4949
var (
5050
cfgFile string
5151
signingKeyFile string
52+
caDir string
5253
)
5354

5455
func init() {
5556
cobra.OnInitialize(initConfig)
5657
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.sshca.toml)")
5758
rootCmd.PersistentFlags().StringVar(&signingKeyFile, "signing-key", "", "signing key file")
59+
rootCmd.PersistentFlags().StringVar(&caDir, "ca-dir", "", "CA directory")
60+
viper.BindPFlag("signing_ca_directory", rootCmd.PersistentFlags().Lookup("ca-dir"))
5861
viper.BindPFlag("signing_key_filename", rootCmd.PersistentFlags().Lookup("signing-key"))
5962
}
6063

cmd/rotate.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package cmd
2+
3+
import (
4+
"crypto/ed25519"
5+
"crypto/rand"
6+
"encoding/pem"
7+
"errors"
8+
"fmt"
9+
"io/fs"
10+
"os"
11+
"path"
12+
"slices"
13+
"strconv"
14+
15+
"github.com/spf13/cobra"
16+
"golang.org/x/crypto/ssh"
17+
)
18+
19+
var rotateCmd = &cobra.Command{
20+
Use: "rotate",
21+
Short: "Rotate the CA keys",
22+
Long: `Creates a new CA key, makes the next key current, and rotates the current key into the past.`,
23+
SilenceUsage: true,
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
// Generate a base set of keys.
26+
previous, err := getVersionOrGenerate("previous")
27+
if err != nil {
28+
return err
29+
}
30+
current, err := getVersionOrGenerate("current")
31+
if err != nil {
32+
return err
33+
}
34+
next, err := getVersionOrGenerate("next")
35+
if err != nil {
36+
return err
37+
}
38+
fmt.Printf("Current: previous: %d; current: %d; next: %d\n", previous, current, next)
39+
fmt.Println("Rotating keys...")
40+
v, err := getNextVersion(caDir)
41+
if err != nil {
42+
return fmt.Errorf("could not get next version: %w", err)
43+
}
44+
if err := generateKey(path.Join(caDir, strconv.Itoa(v))); err != nil {
45+
return fmt.Errorf("could not generate next key: %w", err)
46+
}
47+
if err := setStageVersion("next", v); err != nil {
48+
return err
49+
}
50+
if err := setStageVersion("current", next); err != nil {
51+
return err
52+
}
53+
if err := setStageVersion("previous", current); err != nil {
54+
return err
55+
}
56+
fmt.Printf("Current: previous: %d; current: %d; next: %d\n", current, next, v)
57+
return nil
58+
},
59+
}
60+
61+
func resolveStageVersion(fn string) (int, error) {
62+
t, err := os.Readlink(fn)
63+
if err != nil {
64+
return -1, fmt.Errorf("could not resolve symlink %q: %w", fn, err)
65+
}
66+
i, err := strconv.Atoi(t)
67+
if err != nil {
68+
return -1, fmt.Errorf("could not parse symlink target %q to int: %w", t, err)
69+
}
70+
return i, nil
71+
}
72+
73+
func getNextVersion(dir string) (int, error) {
74+
files, err := os.ReadDir(dir)
75+
if err != nil {
76+
return -1, fmt.Errorf("could not read directory %q: %w", dir, err)
77+
}
78+
var versions []int
79+
for _, file := range files {
80+
v, err := strconv.Atoi(file.Name())
81+
if err != nil {
82+
continue
83+
}
84+
versions = append(versions, v)
85+
}
86+
if len(versions) == 0 {
87+
return 1, nil
88+
}
89+
return slices.Max(versions) + 1, nil
90+
}
91+
92+
func generateKey(fn string) error {
93+
pub, priv, err := ed25519.GenerateKey(rand.Reader)
94+
if err != nil {
95+
return fmt.Errorf("could not generate ED25519 key: %w", err)
96+
}
97+
b, err := ssh.MarshalPrivateKey(priv, "")
98+
if err != nil {
99+
return fmt.Errorf("could not marshal SSH private key: %w", err)
100+
}
101+
if err := os.WriteFile(fn, pem.EncodeToMemory(b), 0o400); err != nil {
102+
return fmt.Errorf("could not write private key file %q: %w", fn, err)
103+
}
104+
sshPub, err := ssh.NewPublicKey(pub)
105+
if err != nil {
106+
return fmt.Errorf("could not create SSH public key: %w", err)
107+
}
108+
if err := os.WriteFile(fn+".pub", ssh.MarshalAuthorizedKey(sshPub), 0o444); err != nil {
109+
return fmt.Errorf("could not write public key file %q: %w", fn+".pub", err)
110+
}
111+
return nil
112+
}
113+
114+
func symlink(oldname string, newname string) error {
115+
if err := os.Symlink(oldname, newname+".new"); err != nil {
116+
return err
117+
}
118+
return os.Rename(newname+".new", newname)
119+
}
120+
121+
func setStageVersion(stage string, version int) error {
122+
if err := symlink(strconv.Itoa(version), path.Join(caDir, stage)); err != nil {
123+
return err
124+
}
125+
return symlink(strconv.Itoa(version)+".pub", path.Join(caDir, stage+".pub"))
126+
}
127+
128+
func getVersionOrGenerate(stage string) (int, error) {
129+
v, err := resolveStageVersion(path.Join(caDir, stage))
130+
if err == nil {
131+
return v, nil
132+
} else if errors.Is(err, fs.ErrNotExist) {
133+
v, err = getNextVersion(caDir)
134+
if err != nil {
135+
return -1, fmt.Errorf("could not get next version for %q: %w", caDir, err)
136+
}
137+
if err := generateKey(path.Join(caDir, strconv.Itoa(v))); err != nil {
138+
return -1, fmt.Errorf("could not generate key version %d: %w", v, err)
139+
}
140+
if err := setStageVersion(stage, v); err != nil {
141+
return -1, fmt.Errorf("could not set version of stage %q to %d: %w", stage, v, err)
142+
}
143+
return v, nil
144+
}
145+
return -1, err
146+
}
147+
148+
func init() {
149+
rootCmd.AddCommand(rotateCmd)
150+
}

cmd/run.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ var runCmd = &cobra.Command{
150150
}
151151
}
152152

153-
priv, err := loadSigningKey(viper.GetString("signing_key_filename"))
153+
keyFn := viper.GetString("signing_key_filename")
154+
if keyFn == "" {
155+
keyFn = path.Join(viper.GetString("signing_ca_directory"), "current")
156+
}
157+
priv, err := loadSigningKey(keyFn)
154158
if err != nil {
155159
return fmt.Errorf("could not load signing key file %q: %w", signingKeyFile, err)
156160
}

0 commit comments

Comments
 (0)