skeema/knownhosts
GitHub: skeema/knownhosts
对 Go 标准库 SSH known_hosts 包的轻量封装,补充了主机密钥查找、算法自动填充和写入等实用功能并修复了已知缺陷。
Stars: 39 | Forks: 5
# knownhosts:增强型 Golang SSH known_hosts 管理
[](https://github.com/skeema/knownhosts/actions)
[](https://coveralls.io/r/skeema/knownhosts)
[](https://pkg.go.dev/github.com/skeema/knownhosts)
Go 在其外部包 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 中为 OpenSSH known_hosts 文件提供了出色的功能。
然而,这个包的层级相对较低,使得实现类似于 OpenSSH 命令行行为的完整 known_hosts 管理变得困难。此外,[golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 在一些边缘情况下存在几个已知问题,其中一些已经保持了多年未解决的状态。
包 [github.com/skeema/knownhosts](https://github.com/skeema/knownhosts) 提供了一个针对 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 的*轻量级封装*,在不重复其核心逻辑的情况下添加了以下改进和修复:
* 查找任意给定主机的 known_hosts 公钥
* 基于 known_hosts 轻松自动填充 ssh.ClientConfig.HostKeyAlgorithms,为 [golang/go#29286](https://github.com/golang/go/issues/29286) 提供了解决方案。(当在 skeema/knownhosts v1.3.0 中[使用 NewDB 构造函数](#enhancements-requiring-extra-parsing) 时,这也能正确处理使用 CA 密钥的主机的证书算法。)
* 无论端口号为何,都能正确匹配通配符主机名的 known_hosts 条目,为 [golang/go#52056](https://github.com/golang/go/issues/52056) 提供了解决方案。(在 v1.3.0 中添加;需要[使用 NewDB 构造函数](#enhancements-requiring-extra-parsing))
* 将新的 known_hosts 条目写入 io.Writer
* 轻松判断 ssh.HostKeyCallback 的错误是指密钥已发生更改的主机(表明可能存在 MitM 攻击),还是指尚未知晓的主机
## 主机密钥查找的工作原理
虽然 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 并没有直接暴露查询其 known_host 映射的方法,但我们使用了一个巧妙的技巧来实现这一点:使用有效的主机但伪造的密钥来调用 HostKeyCallback。由此产生的 KeyError 使我们能够确定该主机实际存在哪些公钥。
通过使用这种技术,[github.com/skeema/knownhosts](https://github.com/skeema/knownhosts) 无需从 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 复制任何核心的 known_hosts 主机查找逻辑。
## 基于 known_hosts 填充 ssh.ClientConfig.HostKeyAlgorithms
主机通常有多个公钥,每个公钥的类型(算法)各不相同。这在 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 中可能会[产生问题](https://github.com/golang/go/issues/29286):如果主机的第一个公钥*不*在 known_hosts 中,但*存在*另一种类型的密钥,HostKeyCallback 就会返回错误。解决方法是根据该主机的 known_hosts 条目的算法来填充 `ssh.ClientConfig.HostKeyAlgorithms`,但是
[golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts)
并没有提供明确的方法来实现这一点。
本包利用其主机密钥查找技巧,使得填充 ssh.ClientConfig.HostKeyAlgorithms 变得非常简单:
```
import (
"golang.org/x/crypto/ssh"
"github.com/skeema/knownhosts"
)
func sshConfigForHost(hostWithPort string) (*ssh.ClientConfig, error) {
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(),
HostKeyAlgorithms: kh.HostKeyAlgorithms(hostWithPort),
}
return config, nil
}
```
## 需要额外解析的增强功能
最初,这个包根本不会重新读取/重新解析 known_hosts 文件,而是完全依赖 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 来进行所有的 known_hosts 文件读取和处理。这个包只提供了一个名为 `New` 的构造函数,返回一个主机密钥回调,这与 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 的调用模式相同,但回调类型上提供了额外的方法。
然而,[golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 中的一些缺陷如果不重新读取 known_hosts 文件就无法解决。因此,从该包的 v1.3.0 版本开始,我们现在提供了一个替代构造函数 `NewDB`,它会额外读取一次 known_hosts 文件(在 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 读取之后),以便检测:
* @cert-authority 行,以便在适当的时候我们能够正确返回证书密钥算法,而不是普通的主机密钥算法
* 主机模式通配符,以便我们能够像 OpenSSH 那样匹配非标准端口号,而不是像 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 通常处理的那样
除了*检测*这些特殊情况外,本包在其他方面仍然直接使用 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 来进行主机查找和所有其他的 known_hosts 文件处理。我们**不会**fork 或重新实现 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 的这些核心行为。
这种额外读取 known_hosts 文件对性能的影响应该微乎其微,因为在 [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) 最初读取时,该文件通常应该已经存在于文件系统缓存中了。话虽如此,希望避免额外读取的用户可以继续使用 `New` 构造函数,它有意保留了 v1.3.0 之前的原有行为。但是,在这种情况下,针对 @cert-authority 和主机模式通配符的额外修复将不会被启用。
## 写入新的 known_hosts 条目
如果您希望模拟 OpenSSH 的 `StrictHostKeyChecking=no` 或 `StrictHostKeyChecking=ask` 行为,本包提供了几个函数来简化此任务。例如:
```
sshHost := "yourserver.com:22"
khPath := "/home/myuser/.ssh/known_hosts"
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 {
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) {
f, ferr := os.OpenFile(khPath, os.O_APPEND|os.O_WRONLY, 0600)
if ferr == nil {
defer f.Close()
ferr = knownhosts.WriteKnownHost(f, hostname, remote, key)
}
if ferr == nil {
log.Printf("Added host %s to known_hosts\n", hostname)
} else {
log.Printf("Failed to add host %s to known_hosts: %v\n", hostname, ferr)
}
return nil // permit previously-unknown hosts (warning: may be insecure)
}
return err
})
config := &ssh.ClientConfig{
User: "myuser",
Auth: []ssh.AuthMethod{ /* ... */ },
HostKeyCallback: cb,
HostKeyAlgorithms: kh.HostKeyAlgorithms(sshHost),
}
```
## 许可证
**源代码版权归 2025 Skeema LLC 及 Skeema Knownhosts 作者所有**
```
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
标签:EVTX分析, 内存分配, 日志审计