Skip to content

Commit

Permalink
certs: reimplement previous commit to maintains backwards compat
Browse files Browse the repository at this point in the history
The previous commit d314bf3 added support for @cert-authority lines, but
technically broke backwards compatibility due to changing the return type of
one exported method. This commit adjusts that previous commit's new logic to
restore backwards compatibility, and makes additional changes as follows:

* Introduce new exported type HostKeyDB, which handles @cert-authority lines
  correctly and is returned by NewDB; old exported type HostKeyCallback (which
  is returned by New) omits that handling. Git-specific use-cases can likely
  remain with using New, since Git forges typically don't support CAs. Non-Git
  use-cases, such as general-purpose SSH clients, should consider switching to
  NewDB to get the CA logic.

* When NewDB re-reads the known_hosts files to implement the CA support, it
  only re-reads each file a single time (vs potentially multiple times at
  callback execution time in d314bf3), and it reads using buffered IO similar
  to x/crypto/ssh/knownhosts.

* This package's PublicKey struct now exports its Cert boolean field, vs
  keeping it private in d314bf3.

* Refactor the RSA-to-algo expansion logic to simplify its handling in the CA
  situation.

* Add test coverage for all new behaviors and @cert-authority logic.
  • Loading branch information
evanelias committed Jul 7, 2024
1 parent d314bf3 commit d1babad
Show file tree
Hide file tree
Showing 4 changed files with 434 additions and 85 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This repo ([github.com/skeema/knownhosts](https://github.com/skeema/knownhosts))

Although [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) doesn't directly expose a way to query its known_host map, we use a subtle trick to do so: invoke the HostKeyCallback with a valid host but a bogus key. The resulting KeyError allows us to determine which public keys are actually present for that host.

By using this technique, [github.com/skeema/knownhosts](https://github.com/skeema/knownhosts) doesn't need to duplicate or re-implement any of the actual known_hosts management from [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts).
By using this technique, [github.com/skeema/knownhosts](https://github.com/skeema/knownhosts) doesn't need to duplicate any of the core known_hosts host-lookup logic from [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts).

## Populating ssh.ClientConfig.HostKeyAlgorithms based on known_hosts

Expand All @@ -43,14 +43,14 @@ import (
)

func sshConfigForHost(hostWithPort string) (*ssh.ClientConfig, error) {
kh, err := knownhosts.New("/home/myuser/.ssh/known_hosts")
kh, err := knownhosts.NewDB("/home/myuser/.ssh/known_hosts")
if err != nil {
return nil, err
}
config := &ssh.ClientConfig{
User: "myuser",
Auth: []ssh.AuthMethod{ /* ... */ },
HostKeyCallback: kh.HostKeyCallback(), // or, equivalently, use ssh.HostKeyCallback(kh)
HostKeyCallback: kh.HostKeyCallback(),
HostKeyAlgorithms: kh.HostKeyAlgorithms(hostWithPort),
}
return config, nil
Expand All @@ -64,15 +64,16 @@ If you wish to mimic the behavior of OpenSSH's `StrictHostKeyChecking=no` or `St
```golang
sshHost := "yourserver.com:22"
khPath := "/home/myuser/.ssh/known_hosts"
kh, err := knownhosts.New(khPath)
kh, err := knownhosts.NewDB(khPath)
if err != nil {
log.Fatal("Failed to read known_hosts: ", err)
}

// Create a custom permissive hostkey callback which still errors on hosts
// with changed keys, but allows unknown hosts and adds them to known_hosts
cb := ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error {
err := kh(hostname, remote, key)
innerCallback := kh.HostKeyCallback()
err := innerCallback(hostname, remote, key)
if knownhosts.IsHostKeyChanged(err) {
return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for host %s! This may indicate a MitM attack.", hostname)
} else if knownhosts.IsHostUnknown(err) {
Expand Down
26 changes: 23 additions & 3 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,26 @@ func ExampleNew() {
config := &ssh.ClientConfig{
User: "myuser",
Auth: []ssh.AuthMethod{ /* ... */ },
HostKeyCallback: kh.HostKeyCallback(), // or, equivalently, use ssh.HostKeyCallback(kh)
HostKeyCallback: kh.HostKeyCallback(),
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}
client, err := ssh.Dial("tcp", sshHost, config)
if err != nil {
log.Fatal("Failed to dial: ", err)
}
defer client.Close()
}

func ExampleNewDB() {
sshHost := "yourserver.com:22"
kh, err := knownhosts.NewDB("/home/myuser/.ssh/known_hosts")
if err != nil {
log.Fatal("Failed to read known_hosts: ", err)
}
config := &ssh.ClientConfig{
User: "myuser",
Auth: []ssh.AuthMethod{ /* ... */ },
HostKeyCallback: kh.HostKeyCallback(),
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}
client, err := ssh.Dial("tcp", sshHost, config)
Expand All @@ -32,15 +51,16 @@ func ExampleNew() {
func ExampleWriteKnownHost() {
sshHost := "yourserver.com:22"
khPath := "/home/myuser/.ssh/known_hosts"
kh, err := knownhosts.New(khPath)
kh, err := knownhosts.NewDB(khPath)
if err != nil {
log.Fatal("Failed to read known_hosts: ", err)
}

// Create a custom permissive hostkey callback which still errors on hosts
// with changed keys, but allows unknown hosts and adds them to known_hosts
cb := ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error {
err := kh(hostname, remote, key)
innerCallback := kh.HostKeyCallback()
err := innerCallback(hostname, remote, key)
if knownhosts.IsHostKeyChanged(err) {
return fmt.Errorf("REMOTE HOST IDENTIFICATION HAS CHANGED for host %s! This may indicate a MitM attack.", hostname)
} else if knownhosts.IsHostUnknown(err) {
Expand Down
Loading

0 comments on commit d1babad

Please sign in to comment.