hostkeyalgos.go

v0.6.1
Doc Versions Source
1
package transport
2
3
import (
4
	"errors"
5
	"net"
6
7
	xssh "golang.org/x/crypto/ssh"
8
	"golang.org/x/crypto/ssh/knownhosts"
9
)
10
11
// Why this file exists
12
//
13
// x/crypto/ssh negotiates the host key algorithm from ClientConfig.
14
// HostKeyAlgorithms; left empty, the client advertises its own default
15
// preference and the server picks from that. knownhosts then checks whatever
16
// key came back against known_hosts — and if the file holds a key for the host
17
// but not in the negotiated algorithm, the result is a *mismatch*, not a
18
// fallback to an algorithm we do have.
19
//
20
// That is the difference between OpenSSH and a naive Go client. OpenSSH orders
21
// HostKeyAlgorithms by what known_hosts already has for the host, so it asks for
22
// the key it can verify. A Go client that skips this connects fine to a host
23
// whose known_hosts entry happens to match the negotiated algorithm and reports
24
// a bogus "key mismatch" for one that does not — the same file, the same server,
25
// verdict decided by which key type was recorded first.
26
//
27
// Seen on a fresh Arch host offering ssh-rsa + ecdsa + ed25519 whose known_hosts
28
// entry was ed25519-only: Go negotiated ecdsa and the dial failed with
29
// "knownhosts: key mismatch" even though the recorded key was correct and
30
// unchanged. Other hosts in the same file worked purely because all three of
31
// their key types had been recorded.
32
//
33
// x/crypto/ssh/knownhosts exposes no helper for this (github.com/skeema/knownhosts
34
// exists precisely to fill the gap), but KeyError.Want carries the known keys for
35
// the host — so probing the callback with a key the file cannot contain hands
36
// back exactly the list we need, with no new dependency and no second parser to
37
// keep in sync with OpenSSH's hashed-hostname format.
38
39
// probeKey is a non-certificate public key of a type no known_hosts file can
40
// contain, so the knownhosts callback always reports a mismatch and populates
41
// KeyError.Want with the host's recorded keys. It is never sent to a server.
42
type probeKey struct{}
43
44
func (probeKey) Type() string                         { return "legatus-probe" }
45
func (probeKey) Marshal() []byte                      { return []byte("legatus-probe") }
46
func (probeKey) Verify([]byte, *xssh.Signature) error { return errors.New("probe key cannot verify") }
47
48
// hostKeyAlgosFor returns the host key algorithms recorded in known_hosts for
49
// addr ("host:port"), in the order they appear, or nil when the host is unknown
50
// (nil leaves ClientConfig.HostKeyAlgorithms empty, i.e. the library default —
51
// an unknown host must still fail the callback, not fail to negotiate).
52
func hostKeyAlgosFor(cb xssh.HostKeyCallback, addr string) []string {
53
	if cb == nil {
54
		return nil
55
	}
56
	// A dummy remote: the knownhosts callback prefers the address argument and
57
	// only falls back to remote when it is empty, so this is never consulted.
58
	remote := &net.TCPAddr{IP: net.IPv4zero, Port: 0}
59
60
	err := cb(addr, remote, probeKey{})
61
	var ke *knownhosts.KeyError
62
	if !errors.As(err, &ke) || len(ke.Want) == 0 {
63
		// Unknown host, or a non-knownhosts callback (InsecureIgnoreHostKey, a
64
		// test double). Either way we have nothing to prefer.
65
		return nil
66
	}
67
68
	var algos []string
69
	seen := map[string]bool{}
70
	add := func(a string) {
71
		if !seen[a] {
72
			seen[a] = true
73
			algos = append(algos, a)
74
		}
75
	}
76
	for _, k := range ke.Want {
77
		switch t := k.Key.Type(); t {
78
		case xssh.KeyAlgoRSA:
79
			// known_hosts records an RSA key as "ssh-rsa" — that is the key
80
			// format, not the signature algorithm. Modern servers sign with
81
			// rsa-sha2-*, and SHA-1 ssh-rsa is disabled by default on current
82
			// OpenSSH, so advertising only "ssh-rsa" would fail to negotiate
83
			// against exactly the hosts that matter. Offer the SHA-2 variants
84
			// first and keep ssh-rsa last for ancient servers.
85
			add(xssh.KeyAlgoRSASHA512)
86
			add(xssh.KeyAlgoRSASHA256)
87
			add(xssh.KeyAlgoRSA)
88
		default:
89
			add(t)
90
		}
91
	}
92
	return algos
93
}
94

Source Files