Browse Source

Implement ssh-keygen wrapper functions

Adam Harrison 2 years ago
parent
commit
a2adeb7bbd
1 changed files with 181 additions and 0 deletions
  1. 181
    0
      ssh/keygen.go

+ 181
- 0
ssh/keygen.go View File

@@ -0,0 +1,181 @@
1
+package ssh
2
+
3
+import (
4
+	"bytes"
5
+	"fmt"
6
+	"io/ioutil"
7
+	"os/exec"
8
+	"path"
9
+	"regexp"
10
+	"strconv"
11
+
12
+	"github.com/spf13/pflag"
13
+)
14
+
15
+// OptionalValue is an extension of pflag.Value that remembers if it has been
16
+// set.
17
+type OptionalValue interface {
18
+	pflag.Value
19
+	Specified() bool
20
+}
21
+
22
+// KeyBitsValue is an OptionalValue allowing specification of the -b argument
23
+// to ssh-keygen.
24
+type KeyBitsValue struct {
25
+	specified bool
26
+	keyBits   uint64
27
+}
28
+
29
+func (kbv *KeyBitsValue) String() string {
30
+	return strconv.FormatUint(kbv.keyBits, 10)
31
+}
32
+
33
+func (kbv *KeyBitsValue) Set(s string) error {
34
+	v, err := strconv.ParseUint(s, 0, 64)
35
+	if err != nil {
36
+		return err
37
+	}
38
+
39
+	kbv.keyBits = v
40
+	kbv.specified = true
41
+
42
+	return nil
43
+}
44
+
45
+func (kbv *KeyBitsValue) Type() string {
46
+	return "uint64"
47
+}
48
+
49
+func (kbv *KeyBitsValue) Specified() bool {
50
+	return kbv.specified
51
+}
52
+
53
+// KeyTypeValue is an OptionalValue allowing specification of the -t argument
54
+// to ssh-keygen.
55
+type KeyTypeValue struct {
56
+	specified bool
57
+	keyType   string
58
+}
59
+
60
+func (ktv *KeyTypeValue) String() string {
61
+	return ktv.keyType
62
+}
63
+
64
+func (ktv *KeyTypeValue) Set(s string) error {
65
+	if len(s) > 0 {
66
+		ktv.keyType = s
67
+		ktv.specified = true
68
+	}
69
+	return nil
70
+}
71
+
72
+func (ktv *KeyTypeValue) Type() string {
73
+	return "string"
74
+}
75
+
76
+func (ktv *KeyTypeValue) Specified() bool {
77
+	return ktv.specified
78
+}
79
+
80
+// KeyGen generates a new keypair with ssh-keygen, optionally overriding the
81
+// default type and size. Each generated keypair is written to a new unique
82
+// subdirectory of tmpfsPath, which should point to a tmpfs mount as the
83
+// private key is not encrypted.
84
+func KeyGen(keyBits, keyType OptionalValue, tmpfsPath string) (privateKeyPath string, privateKey []byte, publicKey PublicKey, err error) {
85
+	tempDir, err := ioutil.TempDir(tmpfsPath, "keygen")
86
+	if err != nil {
87
+		return "", nil, PublicKey{}, err
88
+	}
89
+
90
+	privateKeyPath = path.Join(tempDir, "identity")
91
+	args := []string{"-q", "-N", "", "-f", privateKeyPath}
92
+	if keyBits.Specified() {
93
+		args = append(args, "-b", keyBits.String())
94
+	}
95
+	if keyType.Specified() {
96
+		args = append(args, "-t", keyType.String())
97
+	}
98
+
99
+	cmd := exec.Command("ssh-keygen", args...)
100
+	if err := cmd.Run(); err != nil {
101
+		return "", nil, PublicKey{}, err
102
+	}
103
+
104
+	privateKey, err = ioutil.ReadFile(privateKeyPath)
105
+	if err != nil {
106
+		return "", nil, PublicKey{}, err
107
+	}
108
+
109
+	publicKey, err = ExtractPublicKey(privateKeyPath)
110
+	if err != nil {
111
+		return "", nil, PublicKey{}, err
112
+	}
113
+
114
+	return privateKeyPath, privateKey, publicKey, nil
115
+}
116
+
117
+type Fingerprint struct {
118
+	Hash      string `json:"hash"`
119
+	Randomart string `json:"randomart"`
120
+}
121
+
122
+var (
123
+	fieldRegexp  = regexp.MustCompile(`^([[:digit:]]+) ([^:]+):([^ ]+) (.*?) \(([^)]+)\)$`)
124
+	captureCount = 6
125
+)
126
+
127
+// Fingerprint extracts and returns the hash and randomart of the public key
128
+// associated with the specified private key.
129
+func ExtractFingerprint(privateKeyPath, hashAlgo string) (Fingerprint, error) {
130
+	output, err := exec.Command("ssh-keygen", "-l", "-v", "-E", hashAlgo, "-f", privateKeyPath).Output()
131
+	if err != nil {
132
+		return Fingerprint{}, err
133
+	}
134
+
135
+	i := bytes.IndexByte(output, '\n')
136
+	if i == -1 {
137
+		return Fingerprint{}, fmt.Errorf("could not parse fingerprint")
138
+	}
139
+
140
+	fields := fieldRegexp.FindSubmatch(output[:i])
141
+	if len(fields) != captureCount {
142
+		return Fingerprint{}, fmt.Errorf("could not parse fingerprint")
143
+	}
144
+
145
+	return Fingerprint{
146
+		Hash:      string(fields[3]),
147
+		Randomart: string(output[i+1:]),
148
+	}, nil
149
+}
150
+
151
+type PublicKey struct {
152
+	Key          string                 `json:"key"`
153
+	Fingerprints map[string]Fingerprint `json:"fingerprints"`
154
+}
155
+
156
+// ExtractPublicKey extracts and returns the public key from the specified
157
+// private key, along with its fingerprint hashes.
158
+func ExtractPublicKey(privateKeyPath string) (PublicKey, error) {
159
+	keyBytes, err := exec.Command("ssh-keygen", "-y", "-f", privateKeyPath).Output()
160
+	if err != nil {
161
+		return PublicKey{}, err
162
+	}
163
+
164
+	md5Print, err := ExtractFingerprint(privateKeyPath, "md5")
165
+	if err != nil {
166
+		return PublicKey{}, err
167
+	}
168
+
169
+	sha256Print, err := ExtractFingerprint(privateKeyPath, "sha256")
170
+	if err != nil {
171
+		return PublicKey{}, err
172
+	}
173
+
174
+	return PublicKey{
175
+		Key: string(keyBytes),
176
+		Fingerprints: map[string]Fingerprint{
177
+			"md5":    md5Print,
178
+			"sha256": sha256Print,
179
+		},
180
+	}, nil
181
+}

Loading…
Cancel
Save