-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathopenssh_test.go
322 lines (277 loc) · 11.9 KB
/
openssh_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
package sshsig_test
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/crypto/ssh"
"github.com/hiddeco/sshsig"
)
const (
// ed25519PrivateKey is an ED25519 private key, generated with:
// `ssh-keygen -t ed25519 -C "[email protected]"`
ed25519PrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDTcDBzPJS3L3vhzHSpo2mp0Z5HThNEpt2VMZI7+S04IAAAAJjcTWiZ3E1o
mQAAAAtzc2gtZWQyNTUxOQAAACDTcDBzPJS3L3vhzHSpo2mp0Z5HThNEpt2VMZI7+S04IA
AAAEAAQVJdHf/P7QGmNhr/QhAA82Gees/wN41nUfr515ujCNNwMHM8lLcve+HMdKmjaanR
nkdOE0Sm3ZUxkjv5LTggAAAAEnNzaHNpZ0BleGFtcGxlLmNvbQECAw==
-----END OPENSSH PRIVATE KEY-----`
// ed25519PublicKey is the public key corresponding to ed25519PrivateKey.
ed25519PublicKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNwMHM8lLcve+HMdKmjaanRnkdOE0Sm3ZUxkjv5LTgg [email protected]`
// ecdsaPrivateKey is a ECDSA-P256 private key, generated with:
// `ssh-keygen -t ecdsa -b 256 -C "[email protected]"`
ecdsaPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQQ4hi5YXS//DxdWs4tRrfScyEvCJd2x
/hqjDzyR+md8D9mf5eGv2dGH3t601XX8qq/VUT86f9gf7T3giGVq3IQtAAAAsPbhCNX24Q
jVAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDiGLlhdL/8PF1az
i1Gt9JzIS8Il3bH+GqMPPJH6Z3wP2Z/l4a/Z0Yfe3rTVdfyqr9VRPzp/2B/tPeCIZWrchC
0AAAAgat7A5GYa+yEHE/QWotjwVO3cPxGuyn6ErMUKhIzzetwAAAASc3Noc2lnQGV4YW1w
bGUuY29tAQIDBAUG
-----END OPENSSH PRIVATE KEY-----`
// ecdsaPublicKey is the public key corresponding to ecdsaPrivateKey.
ecdsaPublicKey = `ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDiGLlhdL/8PF1azi1Gt9JzIS8Il3bH+GqMPPJH6Z3wP2Z/l4a/Z0Yfe3rTVdfyqr9VRPzp/2B/tPeCIZWrchC0= [email protected]`
// rsaPrivateKey is a 1024-bit RSA key, generated with
// `ssh-keygen -t rsa -b 1024 -C "[email protected]"`
rsaPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAIEAtqER/SEhWnXVYnijqazzf8LkA4bjSrCNUUSg8nn0H9R/f7jb0au7
6ba/ap4RmmzxKzzpkI1eUrEcPG6/g8N/VYFEU6pszHP2lhjFcbF3Y2zNFm9ygaaTtx61EY
7Rtr7W9SkqtE4yeo0Wnnlc1sV9JVcKTndIRSuQogMKyXeF9tEAAAIItMvAebTLwHkAAAAH
c3NoLXJzYQAAAIEAtqER/SEhWnXVYnijqazzf8LkA4bjSrCNUUSg8nn0H9R/f7jb0au76b
a/ap4RmmzxKzzpkI1eUrEcPG6/g8N/VYFEU6pszHP2lhjFcbF3Y2zNFm9ygaaTtx61EY7R
tr7W9SkqtE4yeo0Wnnlc1sV9JVcKTndIRSuQogMKyXeF9tEAAAADAQABAAAAgQCu9ozHVz
Ae+/icSDtzWNBHPC05+8ZRTed1TixrYM6yl+A2OqHNs5tpgrzLpffzXB+IbujMpcMRsb/9
XZR45Zhcb8Zg6yUOeb9zAoTGYLmIBcKEVRe23AkBY0UDordM758oHmX37Etxr8ij/mg7Uq
TPthJkdd8XxO47gT91OrYfyQAAAEAdPeOlb222qWeY1mC8hKTESPAho+DZxBKCy93fNhUD
4M55ef2CQsxYreDnfFDNJOxgfFXUU403wYLPMJJ0lMDfAAAAQQDwVpAPLN3fVYNidS8H0x
AUfNkjLYfE5k4O2TmeYXSbcrCVzUjvb/4ZcCJWSechfJGNX5qyGTrE0ho54Q4HVu03AAAA
QQDCh8deIWcBdCmDRjO3mE1xoav3fCi3BVH7qodIRuYy1hV3xOSUjwnO5mC1YmeTfyL0uR
4XBqbl1cLmti+/bwA3AAAAEnNzaHNpZ0BleGFtcGxlLmNvbQ==
-----END OPENSSH PRIVATE KEY-----`
// rsaPublicKey is the public key corresponding to rsaPrivateKey.
rsaPublicKey = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC2oRH9ISFaddVieKOprPN/wuQDhuNKsI1RRKDyefQf1H9/uNvRq7vptr9qnhGabPErPOmQjV5SsRw8br+Dw39VgURTqmzMc/aWGMVxsXdjbM0Wb3KBppO3HrURjtG2vtb1KSq0TjJ6jRaeeVzWxX0lVwpOd0hFK5CiAwrJd4X20Q== [email protected]`
// otherPrivateKey is a ED25519 key to test failure cases with, generated with:
// ssh-keygen -t ed25519 -C "[email protected]"
otherPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDTcDBzPJS3L3vhzHSpo2mp0Z5HThNEpt2VMZI7+S04IAAAAJjcTWiZ3E1o
mQAAAAtzc2gtZWQyNTUxOQAAACDTcDBzPJS3L3vhzHSpo2mp0Z5HThNEpt2VMZI7+S04IA
AAAEAAQVJdHf/P7QGmNhr/QhAA82Gees/wN41nUfr515ujCNNwMHM8lLcve+HMdKmjaanR
nkdOE0Sm3ZUxkjv5LTggAAAAEnNzaHNpZ0BleGFtcGxlLmNvbQECAw==
-----END OPENSSH PRIVATE KEY-----`
// otherPublicKey is the public key corresponding to otherPrivateKey.
otherPublicKey = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOrvP89uyupCbqyFcCz1nNtKuLT8YIUkj0Vhf/xYamSs [email protected]`
)
func TestSignToOpenSSH(t *testing.T) {
if _, err := exec.LookPath("ssh-keygen"); err != nil {
t.Skip("skipping: missing ssh-keygen in PATH")
}
var (
testNamespace = "file"
testMessage = []byte("I like your game but we have to change the rules.")
)
tests := []struct {
name string
publicKey string
privateKey string
}{
{"ed25519", ed25519PublicKey, ed25519PrivateKey},
{"ecdsa", ecdsaPublicKey, ecdsaPrivateKey},
{"rsa", rsaPublicKey, rsaPrivateKey},
}
for _, tt := range tests {
tt := tt
for _, a := range sshsig.SupportedHashAlgorithms() {
algo := a
t.Run(fmt.Sprintf("%s-%s", tt.name, algo), func(t *testing.T) {
// Make test go brrrr...
t.Parallel()
// Temporary directory used as working directory for ssh-keygen.
tmp := t.TempDir()
// Load the private key.
signer, err := ssh.ParsePrivateKey([]byte(tt.privateKey))
assert.NoError(t, err)
// Sign a message.
sig, err := sshsig.Sign(bytes.NewReader(testMessage), signer, algo, testNamespace)
assert.NoError(t, err)
// Write the PEM to a file.
sigFile := filepath.Join(tmp, "sig")
assert.NoError(t, os.WriteFile(sigFile, sshsig.Armor(sig), 0o600))
// Construct allowed_signers file.
id, row := allowedSigner(t, tt.publicKey)
idOther, rowOther := allowedSigner(t, otherPublicKey)
allowedSigners := fmt.Sprintf("%s\n%s", row, rowOther)
allowedSignersFile := filepath.Join(tmp, "allowed_signers")
assert.NoError(t, os.WriteFile(allowedSignersFile, []byte(allowedSigners), 0o600))
// Check the signature.
_, err = execOpenSSH(t, tmp, bytes.NewReader(testMessage), "-Y", "check-novalidate", "-f", allowedSignersFile,
"-n", testNamespace, "-s", sigFile)
assert.NoError(t, err)
// Verify the signature.
_, err = execOpenSSH(t, tmp, bytes.NewReader(testMessage), "-Y", "verify", "-f", allowedSignersFile,
"-I", id, "-n", testNamespace, "-s", "sig")
assert.NoError(t, err)
// Different key.
out, err := execOpenSSH(t, tmp, bytes.NewReader(testMessage), "-Y", "verify", "-f", allowedSignersFile,
"-I", idOther, "-n", testNamespace, "-s", sigFile)
assert.Error(t, err, out)
// Different namespace.
out, err = execOpenSSH(t, tmp, bytes.NewReader(testMessage), "-Y", "verify", "-f", allowedSignersFile,
"-I", id, "-n", "other", "-s", sigFile)
assert.Error(t, err, out)
// Different data.
out, err = execOpenSSH(t, tmp, bytes.NewReader([]byte("other")), "-Y", "verify", "-f", allowedSignersFile,
"-I", id, "-n", testNamespace, "-s", sigFile)
assert.Error(t, err, out)
})
}
}
}
func TestVerifyFromOpenSSH(t *testing.T) {
if _, err := exec.LookPath("ssh-keygen"); err != nil {
t.Skip("skipping: missing ssh-keygen in PATH")
}
var (
testNamespace = "file"
testMessage = []byte("I never failed to convince an audience that the best thing they could do was to go away.")
sshVersion = getSSHVersion(t)
// Only ssh-keygen 8.9 and later allow selection of hash at sshsig
// signing time. This is unfortunately not available in the version of
// OpenSSH that ships with macOS in GitHub Actions.
// xref: https://www.openssh.com/txt/release-8.9
supportsHashSelection = sshVersion >= 8.9
)
tests := []struct {
name string
publicKey string
privateKey string
}{
{"ed25519", ed25519PublicKey, ed25519PrivateKey},
{"ecdsa", ecdsaPublicKey, ecdsaPrivateKey},
{"rsa", rsaPublicKey, rsaPrivateKey},
}
for _, tt := range tests {
tt := tt
for _, a := range sshsig.SupportedHashAlgorithms() {
algo := a
t.Run(fmt.Sprintf("%s-%s", tt.name, algo), func(t *testing.T) {
if !supportsHashSelection && algo == sshsig.HashSHA256 {
t.Skipf("skipping: ssh-keygen %v does not allow selection of hash at sshsig signing time", sshVersion)
}
// Make test go brrrr...
t.Parallel()
// Temporary directory used as working directory for ssh-keygen.
tmp := t.TempDir()
// Write the private key to a file, has to end with newline or
// ssh-keygen will complain with "couldn't load".
keyFile := filepath.Join(tmp, "id")
assert.NoError(t, os.WriteFile(keyFile, []byte(tt.privateKey+"\n"), 0o600))
// Write the public key to a file as well. This is required
// because OpenSSH <8.3 does not support reading the public key
// from the private key file.
pubFile := filepath.Join(tmp, "id.pub")
assert.NoError(t, os.WriteFile(pubFile, []byte(tt.publicKey+"\n"), 0o600))
// Write the message to a file.
msgFile := filepath.Join(tmp, "message")
assert.NoError(t, os.WriteFile(msgFile, testMessage, 0o600))
// Sign the message.
args := []string{"-Y", "sign", "-n", testNamespace, "-f", keyFile}
if supportsHashSelection {
args = append(args, "-O", "hashalg="+algo.String())
}
_, err := execOpenSSH(t, tmp, nil, append(args, msgFile)...)
assert.NoError(t, err)
// Read and unmarshal signature.
sigB, err := os.ReadFile(msgFile + ".sig")
assert.NoError(t, err)
sig, err := sshsig.Unarmor(sigB)
assert.NoError(t, err)
// Load the public key.
pub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(tt.publicKey))
assert.NoError(t, err)
// Verify the signature.
err = sshsig.Verify(bytes.NewReader(testMessage), sig, pub, sig.HashAlgorithm, testNamespace)
assert.NoError(t, err)
// Different key.
otherPub, _, _, _, err := ssh.ParseAuthorizedKey([]byte(otherPublicKey))
assert.NoError(t, err)
err = sshsig.Verify(bytes.NewReader(testMessage), sig, otherPub, sig.HashAlgorithm, testNamespace)
assert.ErrorIs(t, err, sshsig.ErrPublicKeyMismatch)
// Different algorithm.
err = sshsig.Verify(bytes.NewReader(testMessage), sig, pub, oppositeAlgorithm(algo), testNamespace)
assert.Error(t, err)
// Different namespace.
err = sshsig.Verify(bytes.NewReader(testMessage), sig, pub, sig.HashAlgorithm, "other")
assert.ErrorIs(t, err, sshsig.ErrNamespaceMismatch)
// Different data.
err = sshsig.Verify(bytes.NewReader([]byte("other")), sig, pub, sig.HashAlgorithm, testNamespace)
assert.Error(t, err)
})
}
}
}
// allowedSigner returns the identifier (comment) of the key, and the row for
// the allowed_signers file.
func allowedSigner(t *testing.T, publicKey string) (id, row string) {
t.Helper()
fields := strings.Fields(publicKey)
if len(fields) != 3 {
t.Fatalf("public key is missing element: %s", publicKey)
}
id = fields[2]
row = fmt.Sprintf("%s %s %s", id, fields[0], fields[1])
return
}
// execOpenSSH executes ssh-keygen with the given arguments in the given dir,
// and returns the combined output. When stdin is not nil, it is passed to
// ssh-keygen. If ssh-keygen returns an error, the error is wrapped with the
// combined output.
func execOpenSSH(t *testing.T, dir string, stdin io.Reader, args ...string) ([]byte, error) {
t.Helper()
t.Logf("ssh-keygen %s", args)
cmd := exec.Command("ssh-keygen", args...)
cmd.Dir = dir
cmd.Stdin = stdin
b, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("%w: %s", err, string(b))
}
return b, nil
}
func getSSHVersion(t *testing.T) float64 {
t.Helper()
out, err := exec.Command("ssh", "-V").CombinedOutput()
if err != nil {
t.Fatalf("failed to get SSH version: %s", out)
}
re := regexp.MustCompile(`OpenSSH.*?_(\d+\.\d+)(p\d+)?`)
matches := re.FindStringSubmatch(string(out))
if len(matches) < 2 {
t.Fatalf("failed to parse SSH version: %s", out)
}
v, err := strconv.ParseFloat(matches[1], 64)
if err != nil {
t.Fatalf("failed to extract SSH version: %s", out)
}
return v
}
// oppositeAlgorithm returns the opposite hash algorithm.
func oppositeAlgorithm(algo sshsig.HashAlgorithm) sshsig.HashAlgorithm {
switch algo {
case sshsig.HashSHA256:
return sshsig.HashSHA512
case sshsig.HashSHA512:
return sshsig.HashSHA256
default:
panic("unknown hash algorithm")
}
}