Skip to content

HostKeyAlgorithms from known_hosts @cert-authority entries should include non-cert fallback algorithms #16

@elazarl

Description

@elazarl

HostKeyAlgorithms from known_hosts @cert-authority entries should include non-cert fallback algorithms

Summary

used Claude code to make the minimal recreation and root cause explanation

When a known_hosts file contains a @cert-authority entry for a host, the skeema/knownhosts library (commonly used with x/crypto/ssh) returns only certificate-based algorithms (e.g., ssh-ed25519-cert-v01@openssh.com). This causes connection failures to servers that support the underlying key type (ssh-ed25519) but not certificate-based host key authentication.

Expected behavior: When a host has a @cert-authority entry, the client should offer both the certificate algorithm and the plain algorithm as a fallback, allowing connections to servers that don't support certificate-based host keys.

Reproduction

Clone podman

git clone https://github.com/containers/podman.git
cd podman
git switch -c skeema/knownhosts/issues/16 559dce7bf836cf78458cff3aa1410517b4003825

Now, create a poc.go in the podman director (to use its dependencies) and run it with go run pod.go

package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"time"

	"github.com/skeema/knownhosts"
	"golang.org/x/crypto/ssh"
)

func main() {
	tmpDir, _ := os.MkdirTemp("", "ssh-poc-*")
	defer os.RemoveAll(tmpDir)

	// Generate server host key (ed25519)
	_, hostPrivKey, _ := ed25519.GenerateKey(rand.Reader)
	hostSigner, _ := ssh.NewSignerFromKey(hostPrivKey)

	// Start SSH server that only supports plain ssh-ed25519 (not certs)
	serverConfig := &ssh.ServerConfig{NoClientAuth: true}
	serverConfig.AddHostKey(hostSigner)

	listener, _ := net.Listen("tcp", "127.0.0.1:0")
	defer listener.Close()
	addr := listener.Addr().String()
	_, port, _ := net.SplitHostPort(addr)

	go func() {
		for {
			conn, err := listener.Accept()
			if err != nil {
				return
			}
			go func(c net.Conn) {
				defer c.Close()
				ssh.NewServerConn(c, serverConfig)
			}(conn)
		}
	}()
	time.Sleep(50 * time.Millisecond)

	// Create @cert-authority known_hosts entry
	certLine := fmt.Sprintf("@cert-authority [127.0.0.1]:%s %s %s\n",
		port,
		hostSigner.PublicKey().Type(),
		base64.StdEncoding.EncodeToString(hostSigner.PublicKey().Marshal()))

	knownHostsFile := filepath.Join(tmpDir, "known_hosts")
	os.WriteFile(knownHostsFile, []byte(certLine), 0600)

	// Load known_hosts - this is where the problem occurs
	kh, _ := knownhosts.NewDB(knownHostsFile)
	algos := kh.HostKeyAlgorithms(addr)

	fmt.Printf("Server supports:     [%s]\n", hostSigner.PublicKey().Type())
	fmt.Printf("Client will offer:   %v\n", algos)
	fmt.Printf("known_hosts content: %s", certLine)

	// Try to connect
	clientConfig := &ssh.ClientConfig{
		User:              "test",
		Auth:              []ssh.AuthMethod{ssh.Password("test")},
		HostKeyCallback:   ssh.InsecureIgnoreHostKey(),
		HostKeyAlgorithms: algos,
	}

	conn, err := ssh.Dial("tcp", addr, clientConfig)
	if err != nil {
		fmt.Printf("\nConnection FAILED: %v\n", err)
		return
	}
	conn.Close()
	fmt.Println("\nConnection succeeded")
}

Output

Server supports:     [ssh-ed25519]
Client will offer:   [ssh-ed25519-cert-v01@openssh.com]
known_hosts content: @cert-authority [127.0.0.1]:56361 ssh-ed25519 AAAAC3...

Connection FAILED: ssh: handshake failed: ssh: no common algorithm for host key;
    we offered: [ssh-ed25519-cert-v01@openssh.com],
    peer offered: [ssh-ed25519]

Real-World Impact

This issue affects Podman on macOS when users have SSH certificate authority configuration in their ~/.ssh/known_hosts. The error manifests as:

$ podman info
Error: unable to connect to Podman socket: failed to connect: ssh: handshake failed:
ssh: no common algorithm for host key; we offered: [ssh-ed25519-cert-v01@openssh.com],
peer offered: [rsa-sha2-512 rsa-sha2-256 ecdsa-sha2-nistp256 ssh-ed25519]

The Podman VM's SSH server supports plain ssh-ed25519, but because the user has a @cert-authority entry (possibly a wildcard * entry for their organization's CA), Podman's Go SSH client only offers the certificate algorithm.

Root Cause

In skeema/knownhosts, the HostKeyAlgorithms() function converts key types to certificate algorithms when the entry has @cert-authority:

// knownhosts.go:202-205
addAlgo := func(typ string, cert bool) {
    if cert {
        typ = keyTypeToCertAlgo(typ)  // Converts ssh-ed25519 → ssh-ed25519-cert-v01@openssh.com
    }
    // ...
}

This conversion is correct for the certificate case, but it excludes the plain algorithm entirely.

Proposed Solution

When a @cert-authority entry exists, HostKeyAlgorithms() should return both the certificate algorithm (preferred) and the plain algorithm (fallback):

addAlgo := func(typ string, cert bool) {
    if cert {
        // Add certificate algorithm first (preferred)
        certTyp := keyTypeToCertAlgo(typ)
        if _, already := seen[certTyp]; !already {
            algos = append(algos, certTyp)
            seen[certTyp] = struct{}{}
        }
        // Also add plain algorithm as fallback
    }
    if _, already := seen[typ]; !already {
        algos = append(algos, typ)
        seen[typ] = struct{}{}
    }
}

This would produce:

Client will offer: [ssh-ed25519-cert-v01@openssh.com, ssh-ed25519]

The SSH handshake would then:

  1. Try the certificate algorithm first (for servers that support it)
  2. Fall back to the plain algorithm (for servers that don't)

Why This Should Be the Default Behavior

  1. Backward compatibility: Users with @cert-authority entries expect to connect to both cert-enabled and plain servers
  2. OpenSSH behavior: OpenSSH clients can connect to servers regardless of whether they support the CA
  3. Least surprise: A known_hosts configuration shouldn't prevent connections that would otherwise work
  4. Defense in depth: The CA entry provides trust for cert-based auth, but shouldn't exclude plain host key verification

Environment

  • Go version: go1.22+
  • OS: darwin/arm64 (also affects Linux)
  • golang.org/x/crypto: latest
  • github.com/skeema/knownhosts: v1.3.0

Note: While the immediate fix would be in skeema/knownhosts, this report is filed here because:

  1. The x/crypto/ssh package defines the algorithm constants and negotiation
  2. A more robust solution might involve x/crypto/ssh/knownhosts providing better support for mixed cert/non-cert scenarios
  3. The Go SSH ecosystem should have consistent behavior for this common use case

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions