1package config
2
3import (
4 "bytes"
5 "crypto/rsa"
6 "fmt"
7 "strings"
8
9 "golang.org/x/crypto/ssh"
10)
11
12const (
13 maxTrustSize = 64 * 1024
14 keyAllowedSigners = "config.git.allowed_signers"
15 keyConfigKnownHosts = "config.git.known_hosts"
16 gitNamespace = "git"
17)
18
19const (
20 reasonTrustTooLarge = "file is larger than 64 KiB"
21 reasonSignersLine = "line is not an allowed-signers entry (principals, optional options, a public key)"
22 reasonSignersKeyType = "key must be ed25519, RSA with at least 2048 bits, or ECDSA P-256, P-384 or P-521"
23 reasonSignersCertAuthority = "cert-authority entries are not supported (list each signing key)"
24 reasonSignersNoGitKey = "no key is allowed for the git namespace"
25 reasonKnownHostsLine = "line is not a known_hosts entry"
26 reasonKnownHostsEmpty = "file holds no host key"
27)
28
29func readTrustFile(path, key string) ([]byte, []Problem) {
30 data, reason := readRegular(path, maxTrustSize, reasonTrustTooLarge, nil)
31 if reason != "" {
32 return nil, []Problem{{File: path, Key: key, Reason: reason}}
33 }
34 return data, nil
35}
36
37func readTrust(cfg *Config) []Problem {
38 g := cfg.ConfigGit
39 if cfg.Source != sourceGit || g == nil {
40 return nil
41 }
42 data, problems := readTrustFile(g.AllowedSigners, keyAllowedSigners)
43 if problems == nil {
44 g.Signers, problems = parseAllowedSigners(g.AllowedSigners, data)
45 }
46 if g.SSH() {
47 hosts, hostProblems := readTrustFile(g.KnownHosts, keyConfigKnownHosts)
48 if hostProblems == nil {
49 hostProblems = checkKnownHosts(g.KnownHosts, hosts)
50 }
51 if hostProblems == nil {
52 g.KnownHostsData = hosts
53 }
54 problems = append(problems, hostProblems...)
55 }
56 return problems
57}
58
59func trustLines(data []byte, each func(n int, line string)) {
60 for i, line := range strings.Split(string(data), "\n") {
61 line = strings.TrimSpace(line)
62 if line != "" && line[0] != '#' {
63 each(i+1, line)
64 }
65 }
66}
67
68func parseAllowedSigners(file string, data []byte) ([]ssh.PublicKey, []Problem) {
69 var keys []ssh.PublicKey
70 var problems []Problem
71 trustLines(data, func(n int, line string) {
72 problem := func(reason string) {
73 problems = append(problems, Problem{File: file, Line: n, Key: keyAllowedSigners, Reason: reason})
74 }
75 i := strings.IndexAny(line, " \t")
76 if i < 0 {
77 problem(reasonSignersLine)
78 return
79 }
80 key, _, options, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(line[i:])))
81 switch {
82 case err != nil:
83 problem(reasonSignersLine)
84 case len(SignatureAlgorithms(key)) == 0:
85 problem(reasonSignersKeyType)
86 case hasOption(options, "cert-authority"):
87 problem(reasonSignersCertAuthority)
88 case signsGit(options):
89 keys = append(keys, key)
90 }
91 })
92 if len(problems) == 0 && len(keys) == 0 {
93 problems = append(problems, Problem{File: file, Key: keyAllowedSigners, Reason: reasonSignersNoGitKey})
94 }
95 return keys, problems
96}
97
98func SignatureAlgorithms(key ssh.PublicKey) []string {
99 if key == nil {
100 return nil
101 }
102 switch key.Type() {
103 case ssh.KeyAlgoED25519, ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
104 return []string{key.Type()}
105 case ssh.KeyAlgoRSA:
106 if crypto, ok := key.(ssh.CryptoPublicKey); ok {
107 if rsaKey, ok := crypto.CryptoPublicKey().(*rsa.PublicKey); ok && rsaKey.N.BitLen() >= 2048 {
108 return []string{ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512}
109 }
110 }
111 }
112 return nil
113}
114
115func hasOption(options []string, name string) bool {
116 for _, o := range options {
117 if strings.EqualFold(o, name) {
118 return true
119 }
120 }
121 return false
122}
123
124func signsGit(options []string) bool {
125 for _, o := range options {
126 name, value, ok := strings.Cut(o, "=")
127 if !ok || !strings.EqualFold(name, "namespaces") {
128 continue
129 }
130 for _, ns := range strings.Split(strings.Trim(value, `"`), ",") {
131 if ns == gitNamespace {
132 return true
133 }
134 }
135 return false
136 }
137 return true
138}
139
140func checkKnownHosts(file string, data []byte) []Problem {
141 var problems []Problem
142 entries := 0
143 trustLines(data, func(n int, line string) {
144 if _, _, _, _, _, err := ssh.ParseKnownHosts([]byte(line)); err != nil {
145 problems = append(problems, Problem{File: file, Line: n, Key: keyConfigKnownHosts, Reason: reasonKnownHostsLine})
146 return
147 }
148 entries++
149 })
150 if len(problems) == 0 && entries == 0 {
151 problems = append(problems, Problem{File: file, Key: keyConfigKnownHosts, Reason: reasonKnownHostsEmpty})
152 }
153 return problems
154}
155
156func SignerPrincipal(file string, key ssh.PublicKey) (string, error) {
157 data, problems := readTrustFile(file, keyAllowedSigners)
158 if len(problems) != 0 {
159 return "", fmt.Errorf("cannot read allowed signers")
160 }
161 principal := ""
162 trustLines(data, func(_ int, line string) {
163 i := strings.IndexAny(line, " \t")
164 if i < 0 {
165 return
166 }
167 listed, _, options, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(line[i:])))
168 if err == nil && !hasOption(options, "cert-authority") && signsGit(options) && bytes.Equal(listed.Marshal(), key.Marshal()) && principal == "" {
169 principal = line[:i]
170 }
171 })
172 if principal == "" {
173 return "", fmt.Errorf("verified signer is no longer listed")
174 }
175 return principal, nil
176}