package config import ( "bytes" "crypto/rsa" "fmt" "strings" "golang.org/x/crypto/ssh" ) const ( maxTrustSize = 64 * 1024 keyAllowedSigners = "config.git.allowed_signers" keyConfigKnownHosts = "config.git.known_hosts" gitNamespace = "git" ) const ( reasonTrustTooLarge = "file is larger than 64 KiB" reasonSignersLine = "line is not an allowed-signers entry (principals, optional options, a public key)" reasonSignersKeyType = "key must be ed25519, RSA with at least 2048 bits, or ECDSA P-256, P-384 or P-521" reasonSignersCertAuthority = "cert-authority entries are not supported (list each signing key)" reasonSignersNoGitKey = "no key is allowed for the git namespace" reasonKnownHostsLine = "line is not a known_hosts entry" reasonKnownHostsEmpty = "file holds no host key" ) func readTrustFile(path, key string) ([]byte, []Problem) { data, reason := readRegular(path, maxTrustSize, reasonTrustTooLarge, nil) if reason != "" { return nil, []Problem{{File: path, Key: key, Reason: reason}} } return data, nil } func readTrust(cfg *Config) []Problem { g := cfg.ConfigGit if cfg.Source != sourceGit || g == nil { return nil } data, problems := readTrustFile(g.AllowedSigners, keyAllowedSigners) if problems == nil { g.Signers, problems = parseAllowedSigners(g.AllowedSigners, data) } if g.SSH() { hosts, hostProblems := readTrustFile(g.KnownHosts, keyConfigKnownHosts) if hostProblems == nil { hostProblems = checkKnownHosts(g.KnownHosts, hosts) } if hostProblems == nil { g.KnownHostsData = hosts } problems = append(problems, hostProblems...) } return problems } func trustLines(data []byte, each func(n int, line string)) { for i, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) if line != "" && line[0] != '#' { each(i+1, line) } } } func parseAllowedSigners(file string, data []byte) ([]ssh.PublicKey, []Problem) { var keys []ssh.PublicKey var problems []Problem trustLines(data, func(n int, line string) { problem := func(reason string) { problems = append(problems, Problem{File: file, Line: n, Key: keyAllowedSigners, Reason: reason}) } i := strings.IndexAny(line, " \t") if i < 0 { problem(reasonSignersLine) return } key, _, options, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(line[i:]))) switch { case err != nil: problem(reasonSignersLine) case len(SignatureAlgorithms(key)) == 0: problem(reasonSignersKeyType) case hasOption(options, "cert-authority"): problem(reasonSignersCertAuthority) case signsGit(options): keys = append(keys, key) } }) if len(problems) == 0 && len(keys) == 0 { problems = append(problems, Problem{File: file, Key: keyAllowedSigners, Reason: reasonSignersNoGitKey}) } return keys, problems } func SignatureAlgorithms(key ssh.PublicKey) []string { if key == nil { return nil } switch key.Type() { case ssh.KeyAlgoED25519, ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521: return []string{key.Type()} case ssh.KeyAlgoRSA: if crypto, ok := key.(ssh.CryptoPublicKey); ok { if rsaKey, ok := crypto.CryptoPublicKey().(*rsa.PublicKey); ok && rsaKey.N.BitLen() >= 2048 { return []string{ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512} } } } return nil } func hasOption(options []string, name string) bool { for _, o := range options { if strings.EqualFold(o, name) { return true } } return false } func signsGit(options []string) bool { for _, o := range options { name, value, ok := strings.Cut(o, "=") if !ok || !strings.EqualFold(name, "namespaces") { continue } for _, ns := range strings.Split(strings.Trim(value, `"`), ",") { if ns == gitNamespace { return true } } return false } return true } func checkKnownHosts(file string, data []byte) []Problem { var problems []Problem entries := 0 trustLines(data, func(n int, line string) { if _, _, _, _, _, err := ssh.ParseKnownHosts([]byte(line)); err != nil { problems = append(problems, Problem{File: file, Line: n, Key: keyConfigKnownHosts, Reason: reasonKnownHostsLine}) return } entries++ }) if len(problems) == 0 && entries == 0 { problems = append(problems, Problem{File: file, Key: keyConfigKnownHosts, Reason: reasonKnownHostsEmpty}) } return problems } func SignerPrincipal(file string, key ssh.PublicKey) (string, error) { data, problems := readTrustFile(file, keyAllowedSigners) if len(problems) != 0 { return "", fmt.Errorf("cannot read allowed signers") } principal := "" trustLines(data, func(_ int, line string) { i := strings.IndexAny(line, " \t") if i < 0 { return } listed, _, options, _, err := ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(line[i:]))) if err == nil && !hasOption(options, "cert-authority") && signsGit(options) && bytes.Equal(listed.Marshal(), key.Marshal()) && principal == "" { principal = line[:i] } }) if principal == "" { return "", fmt.Errorf("verified signer is no longer listed") } return principal, nil }