Go · 3527 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package sshkey
4
5 import (
6 "errors"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11 "testing"
12 )
13
14 func mustRead(t *testing.T, name string) string {
15 t.Helper()
16 b, err := os.ReadFile(filepath.Join("testdata", name))
17 if err != nil {
18 t.Fatalf("read %s: %v", name, err)
19 }
20 return string(b)
21 }
22
23 func TestParse_AcceptsEd25519(t *testing.T) {
24 t.Parallel()
25 pub := mustRead(t, "ed25519.pub")
26 got, err := Parse("my laptop", pub)
27 if err != nil {
28 t.Fatalf("Parse: %v", err)
29 }
30 if got.Type != "ssh-ed25519" {
31 t.Fatalf("type = %q", got.Type)
32 }
33 if !strings.HasPrefix(got.Fingerprint, "SHA256:") {
34 t.Fatalf("fingerprint shape: %q", got.Fingerprint)
35 }
36 // Cross-check against ssh-keygen if available — defensive.
37 if _, err := exec.LookPath("ssh-keygen"); err == nil {
38 // G204: argv is a static list with a fixed test fixture path.
39 out, err := exec.Command("ssh-keygen", "-E", "sha256", "-lf", //nolint:gosec
40 filepath.Join("testdata", "ed25519.pub")).Output()
41 if err != nil {
42 t.Fatalf("ssh-keygen: %v", err)
43 }
44 if !strings.Contains(string(out), got.Fingerprint) {
45 t.Fatalf("fingerprint mismatch with ssh-keygen: %q vs %s",
46 got.Fingerprint, out)
47 }
48 }
49 }
50
51 func TestParse_AcceptsRSA2048(t *testing.T) {
52 t.Parallel()
53 pub := mustRead(t, "rsa2048.pub")
54 got, err := Parse("ci runner", pub)
55 if err != nil {
56 t.Fatalf("Parse: %v", err)
57 }
58 if got.Type != "ssh-rsa" {
59 t.Fatalf("type = %q", got.Type)
60 }
61 if got.Bits < 2048 {
62 t.Fatalf("bits = %d", got.Bits)
63 }
64 }
65
66 func TestParse_AcceptsECDSA(t *testing.T) {
67 t.Parallel()
68 pub := mustRead(t, "ecdsa256.pub")
69 got, err := Parse("hsm", pub)
70 if err != nil {
71 t.Fatalf("Parse: %v", err)
72 }
73 if got.Type != "ecdsa-sha2-nistp256" {
74 t.Fatalf("type = %q", got.Type)
75 }
76 }
77
78 func TestParse_RejectsRSA1024(t *testing.T) {
79 t.Parallel()
80 pub := mustRead(t, "rsa1024.pub")
81 _, err := Parse("weak", pub)
82 if !errors.Is(err, ErrRSATooShort) {
83 t.Fatalf("expected ErrRSATooShort, got %v", err)
84 }
85 }
86
87 func TestParse_RejectsUnparseable(t *testing.T) {
88 t.Parallel()
89 cases := []string{
90 "",
91 "not-a-key",
92 "ssh-rsa AAAA broken",
93 "ssh-dss AAAAB3NzaC1kc3MAAACB",
94 }
95 for _, in := range cases {
96 _, err := Parse("title", in)
97 if err == nil {
98 t.Errorf("expected error for %q", in)
99 }
100 }
101 }
102
103 func TestParse_RejectsEmptyTitle(t *testing.T) {
104 t.Parallel()
105 pub := mustRead(t, "ed25519.pub")
106 _, err := Parse(" ", pub)
107 if !errors.Is(err, ErrTitleEmpty) {
108 t.Fatalf("expected ErrTitleEmpty, got %v", err)
109 }
110 }
111
112 func TestParse_RejectsLongTitle(t *testing.T) {
113 t.Parallel()
114 pub := mustRead(t, "ed25519.pub")
115 _, err := Parse(strings.Repeat("a", 81), pub)
116 if !errors.Is(err, ErrTitleTooLong) {
117 t.Fatalf("expected ErrTitleTooLong, got %v", err)
118 }
119 }
120
121 func TestParse_RejectsControlCharsInTitle(t *testing.T) {
122 t.Parallel()
123 pub := mustRead(t, "ed25519.pub")
124 _, err := Parse("oops\nnewline", pub)
125 if !errors.Is(err, ErrTitleControl) {
126 t.Fatalf("expected ErrTitleControl, got %v", err)
127 }
128 }
129
130 func TestParse_CanonicalizesPublicKey(t *testing.T) {
131 t.Parallel()
132 pub := mustRead(t, "ed25519.pub")
133 got, _ := Parse("my laptop", pub)
134 // No trailing newline, no comment.
135 if strings.HasSuffix(got.PublicKey, "\n") {
136 t.Fatal("canonical key has trailing newline")
137 }
138 if strings.Contains(got.PublicKey, "fixture-ed25519") {
139 t.Fatal("canonical key includes original comment")
140 }
141 parts := strings.Fields(got.PublicKey)
142 if len(parts) != 2 {
143 t.Fatalf("canonical key should be 'algo b64', got %q", got.PublicKey)
144 }
145 }
146