Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x/crypto/ssh: client requires first hostkey to match, knownhosts doesn't expose available key types #29286

beiriannydd opened this issue Dec 15, 2018 · 7 comments


Copy link

@beiriannydd beiriannydd commented Dec 15, 2018

What version of Go are you using (go version)?

$ go version
go version go1.11.3 linux/amd64

Does this issue reproduce with the latest release?

This is the latest docker release (yes)

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build745071247=/tmp/go-build -gno-record-gcc-switches"

What did you do?

	config := &ssh.ClientConfig{
		User:            GetRemoteUsername(host),
		HostKeyCallback: knownHostsCallback,
		Timeout:         30 * time.Second,
		Auth:            []ssh.AuthMethod{},

knownHostsCallback is an ssh.HostKeysCallback.

2 machines:
machine A has ecdsa-sha2-nistp256 key for remote host in known hosts file
machine B has ssh-ed25519 key for remote host in known hosts file

If machine A connects to the remote host, the remote host sends the ecdsa-sha2-nistp256 key and is allowed to continue the handshake
If machine B connects to the remote host, the remote host sends the ecdsa-sha2-nistp256 key and is rejected because the callback returns an error.

If I set the host key list to prefer ["ssh-ed25519","ecdsa-sha2-nistp256"...]
Machine A fails
Machine B succeeds

What did you expect to see?

Since there is no mechanism for discovering the available hostkeys for a host published in ssh/knownhosts , I expected it to try each hostkey in the preference list in turn, failing only if all the keys had been tried.

What did you see instead?

SSH Client ceases handshake after receiving the first error response from the HostKeysCallback.

Since only the method on the database is returned by knownhosts.New() it is not easy to add another method accessing the same hostKeyDB instance.

I propose adding an initializing function for the database from knownhosts which returns the database - NewDB to complement New which would return a hostkeys interface.

There would be published methods on the receiver:

type KnownHostDB interface {
	// HostKeyCallback is knownhosts.New without the DB initialization.
	HostKeyCallback() ssh.HostKeyCallback
	// HostKeyAlgorithms takes an address and returns a list of matching key types.
	HostKeyAlgorithms(address string) ([]string, error)
// NewDB is knownhosts.New without the callback code
func NewDB(filename string) (KnownHostDB, error)

That way you can just use:

	knownHosts := knownhosts.NewDB(knownhostfile)
	// just to cut down on example code, the error is ignored.
	algos, _ := knownHosts.HostKeyAlgorithms(host)
	config := &ssh.ClientConfig{
		User:            GetRemoteUsername(host),
		HostKeyCallback: knownHosts.HostKeysCallback(),
		Timeout:         30 * time.Second,
		Auth:            []ssh.AuthMethod{},
		HostKeyAlgorithms: algos,

Alternatively if the protocol allows for it, the host key algorithms could be tried in turn until you run out of algorithms (fail) or you have a match (success)

Copy link

@beiriannydd beiriannydd commented Dec 15, 2018

Copy link

@agnivade agnivade commented Dec 16, 2018

If you are proposing to add new API, then this should be a proposal, rather than a bug report.

/cc @hanwen

Copy link

@hanwen hanwen commented Dec 17, 2018

you can have only one hostkey algorithm per connection attempt, so it doesn't make sense to automate this, I think.

We could look at exposing the DB in knownhosts. I think that should just make the existing types public, rather than introducing an interface.

Copy link

@beiriannydd beiriannydd commented Dec 17, 2018

db is a local inside the New() method currently, are you suggesting making it create a global, or having a different method which returns a db, which you call instead of .New()?
If you return the db then you need a different method to construct the CertChecker for use as the callback function.

Copy link

@hanwen hanwen commented Dec 18, 2018

different method.

Copy link

@beiriannydd beiriannydd commented Dec 19, 2018

So it sounds like the only thing you don't like about my solution is that it provides the interface. I guess the wonderful thing about Go is that you don't have to know that you respond to an interface, so as long as we call the methods something sane, people who need to be able to access file based knownhosts and eg. registry based knownhosts can create their own interface.

Copy link

@hanwen hanwen commented Jan 23, 2019

interfaces should be defined where they are consumed, and functions should return concrete types if possible. See

I know that the SSH package violates this guidelines in some places, but unfortunately, we can't fix it without breaking callers. However, I don't want to introduce more instances of this pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants