pax_global_header00006660000000000000000000000064146455620710014524gustar00rootroot0000000000000052 comment=9485bdec8ed521b32ab0e9310619effa071a99d6 knownhosts-1.3.0/000077500000000000000000000000001464556207100137425ustar00rootroot00000000000000knownhosts-1.3.0/.github/000077500000000000000000000000001464556207100153025ustar00rootroot00000000000000knownhosts-1.3.0/.github/pull_request_template.md000066400000000000000000000003341464556207100222430ustar00rootroot00000000000000 knownhosts-1.3.0/.github/workflows/000077500000000000000000000000001464556207100173375ustar00rootroot00000000000000knownhosts-1.3.0/.github/workflows/tests.yml000066400000000000000000000017071464556207100212310ustar00rootroot00000000000000name: Tests on: [push, pull_request] env: GOVERSION: "1.21" jobs: test: name: Check code quality and run tests if: "!contains(github.event.head_commit.message, '[ci skip]')" runs-on: ubuntu-latest steps: - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{env.GOVERSION}} - name: Check out code uses: actions/checkout@v4 - name: Run gofmt run: test -z "$(gofmt -s -d *.go 2>&1)" - name: Run golint run: go install golang.org/x/lint/golint@latest && golint -set_exit_status - name: Run go vet run: go vet - name: Run tests run: go test -v -coverprofile=coverage.out -covermode=count - name: Report coverage env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: go install github.com/mattn/goveralls@latest && goveralls -coverprofile=coverage.out -service=github continue-on-error: true knownhosts-1.3.0/CONTRIBUTING.md000066400000000000000000000057231464556207100162020ustar00rootroot00000000000000# Contributing to skeema/knownhosts Thank you for your interest in contributing! This document provides guidelines for submitting pull requests. ### Link to an issue Before starting the pull request process, initial discussion should take place on a GitHub issue first. For bug reports, the issue should track the open bug and confirm it is reproducible. For feature requests, the issue should cover why the feature is necessary. In the issue comments, discuss your suggested approach for a fix/implementation, and please wait to get feedback before opening a pull request. ### Test coverage In general, please provide reasonably thorough test coverage. Whenever possible, your PR should aim to match or improve the overall test coverage percentage of the package. You can run tests and check coverage locally using `go test -cover`. We also have CI automation in GitHub Actions which will comment on each pull request with a coverage percentage. That said, it is fine to submit an initial draft / work-in-progress PR without coverage, if you are waiting on implementation feedback before writing the tests. We intentionally avoid hard-coding SSH keys or known_hosts files into the test logic. Instead, the tests generate new keys and then use them to generate a known_hosts file, which is then cached/reused for that overall test run, in order to keep performance reasonable. ### Documentation Exported types require doc comments. The linter CI step will catch this if missing. ### Backwards compatibility Because this package is imported by [nearly 7000 repos on GitHub](https://github.com/skeema/knownhosts/network/dependents), we must be very strict about backwards compatibility of exported symbols and function signatures. Backwards compatibility can be very tricky in some situations. In this case, a maintainer may need to add additional commits to your branch to adjust the approach. Please do not take offense if this occurs; it is sometimes simply faster to implement a refactor on our end directly. When the PR/branch is merged, a merge commit will be used, to ensure your commits appear as-is in the repo history and are still properly credited to you. ### Avoid rewriting core x/crypto/ssh/knownhosts logic skeema/knownhosts is intended to be a relatively thin *wrapper* around x/crypto/ssh/knownhosts, without duplicating or re-implementing the core known_hosts file parsing and host key handling logic. Importers of this package should be confident that it can be used as a nearly-drop-in replacement for x/crypto/ssh/knownhosts without introducing substantial risk, security flaws, parser differentials, or unexpected behavior changes. To solve shortcomings in x/crypto/ssh/knownhosts, we try to come up with workarounds that still utilize x/crypto/ssh/knownhosts functionality whenever possible. Some bugs in x/crypto/ssh/knownhosts do require re-reading the known_hosts file here to solve, but we make that *optional* by offering separate constructors/types with and without that behavior. knownhosts-1.3.0/LICENSE000066400000000000000000000261351464556207100147560ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} 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. knownhosts-1.3.0/NOTICE000066400000000000000000000011111464556207100146400ustar00rootroot00000000000000Copyright 2024 Skeema LLC and the Skeema Knownhosts authors 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. knownhosts-1.3.0/README.md000066400000000000000000000215301464556207100152220ustar00rootroot00000000000000# knownhosts: enhanced Golang SSH known_hosts management [![build status](https://img.shields.io/github/actions/workflow/status/skeema/knownhosts/tests.yml?branch=main)](https://github.com/skeema/knownhosts/actions) [![code coverage](https://img.shields.io/coveralls/skeema/knownhosts.svg)](https://coveralls.io/r/skeema/knownhosts) [![godoc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/skeema/knownhosts) > This repo is brought to you by [Skeema](https://github.com/skeema/skeema), a > declarative pure-SQL schema management system for MySQL and MariaDB. Our > premium products include extensive [SSH tunnel](https://www.skeema.io/docs/features/ssh/) > functionality, which internally makes use of this package. Go provides excellent functionality for OpenSSH known_hosts files in its external package [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts). However, that package is somewhat low-level, making it difficult to implement full known_hosts management similar to OpenSSH's command-line behavior. Additionally, [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) has several known issues in edge cases, some of which have remained open for multiple years. Package [github.com/skeema/knownhosts](https://github.com/skeema/knownhosts) provides a *thin wrapper* around [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts), adding the following improvements and fixes without duplicating its core logic: * Look up known_hosts public keys for any given host * Auto-populate ssh.ClientConfig.HostKeyAlgorithms easily based on known_hosts, providing a solution for [golang/go#29286](https://github.com/golang/go/issues/29286). (This also properly handles cert algorithms for hosts using CA keys when [using the NewDB constructor](#enhancements-requiring-extra-parsing) added in skeema/knownhosts v1.3.0.) * Properly match wildcard hostname known_hosts entries regardless of port number, providing a solution for [golang/go#52056](https://github.com/golang/go/issues/52056). (Added in v1.3.0; requires [using the NewDB constructor](#enhancements-requiring-extra-parsing)) * Write new known_hosts entries to an io.Writer * Properly format/normalize new known_hosts entries containing ipv6 addresses, providing a solution for [golang/go#53463](https://github.com/golang/go/issues/53463) * Easily determine if an ssh.HostKeyCallback's error corresponds to a host whose key has changed (indicating potential MitM attack) vs a host that just isn't known yet ## How host key lookup works 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 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 Hosts often have multiple public keys, each of a different type (algorithm). This can be [problematic](https://github.com/golang/go/issues/29286) in [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts): if a host's first public key is *not* in known_hosts, but a key of a different type *is*, the HostKeyCallback returns an error. The solution is to populate `ssh.ClientConfig.HostKeyAlgorithms` based on the algorithms of the known_hosts entries for that host, but [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) does not provide an obvious way to do so. This package uses its host key lookup trick in order to make ssh.ClientConfig.HostKeyAlgorithms easy to populate: ```golang 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 } ``` ## Enhancements requiring extra parsing Originally, this package did not re-read/re-parse the known_hosts files at all, relying entirely on [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) for all known_hosts file reading and processing. This package only offered a constructor called `New`, returning a host key callback, identical to the call pattern of [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) but with extra methods available on the callback type. However, a couple shortcomings in [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) cannot possibly be solved without re-reading the known_hosts file. Therefore, as of v1.3.0 of this package, we now offer an alternative constructor `NewDB`, which does an additional read of the known_hosts file (after the one from [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts)), in order to detect: * @cert-authority lines, so that we can correctly return cert key algorithms instead of normal host key algorithms when appropriate * host pattern wildcards, so that we can match OpenSSH's behavior for non-standard port numbers, unlike how [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) normally treats them Aside from *detecting* these special cases, this package otherwise still directly uses [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts) for host lookups and all other known_hosts file processing. We do **not** fork or re-implement those core behaviors of [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts). The performance impact of this extra known_hosts read should be minimal, as the file should typically be in the filesystem cache already from the original read by [golang.org/x/crypto/ssh/knownhosts](https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts). That said, users who wish to avoid the extra read can stay with the `New` constructor, which intentionally retains its pre-v1.3.0 behavior as-is. However, the extra fixes for @cert-authority and host pattern wildcards will not be enabled in that case. ## Writing new known_hosts entries If you wish to mimic the behavior of OpenSSH's `StrictHostKeyChecking=no` or `StrictHostKeyChecking=ask`, this package provides a few functions to simplify this task. For example: ```golang 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), } ``` ## License **Source code copyright 2024 Skeema LLC and the Skeema Knownhosts authors** ```text 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. ``` knownhosts-1.3.0/example_test.go000066400000000000000000000075271464556207100167760ustar00rootroot00000000000000package knownhosts_test import ( "fmt" "log" "net" "os" "github.com/skeema/knownhosts" "golang.org/x/crypto/ssh" ) func ExampleNew() { sshHost := "yourserver.com:22" kh, err := knownhosts.New("/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) 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) if err != nil { log.Fatal("Failed to dial: ", err) } defer client.Close() } func ExampleHostKeyCallback_ToDB() { khFile := "/home/myuser/.ssh/known_hosts" var kh *knownhosts.HostKeyDB var err error // Example of using conditional logic to determine whether or not to perform // extra parsing pass on the known_hosts file in order to enable enhanced // behaviors if os.Getenv("SKIP_KNOWNHOSTS_ENHANCEMENTS") != "" { // Create a HostKeyDB using New + ToDB: this will skip the extra known_hosts // processing var cb knownhosts.HostKeyCallback if cb, err = knownhosts.New(khFile); err == nil { kh = cb.ToDB() } } else { // Create a HostKeyDB using NewDB: this will perform extra known_hosts // processing, allowing proper support for CAs, as well as OpenSSH-like // wildcard matching on non-standard ports kh, err = knownhosts.NewDB(khFile) } if err != nil { log.Fatal("Failed to read known_hosts: ", err) } sshHost := "yourserver.com:22" config := &ssh.ClientConfig{ User: "myuser", Auth: []ssh.AuthMethod{ /* ... */ }, 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 ExampleWriteKnownHost() { 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), } client, err := ssh.Dial("tcp", sshHost, config) if err != nil { log.Fatal("Failed to dial: ", err) } defer client.Close() } knownhosts-1.3.0/go.mod000066400000000000000000000002001464556207100150400ustar00rootroot00000000000000module github.com/skeema/knownhosts go 1.17 require golang.org/x/crypto v0.24.0 require golang.org/x/sys v0.21.0 // indirect knownhosts-1.3.0/go.sum000066400000000000000000000133621464556207100151020ustar00rootroot00000000000000github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= knownhosts-1.3.0/knownhosts.go000066400000000000000000000440371464556207100165160ustar00rootroot00000000000000// Package knownhosts is a thin wrapper around golang.org/x/crypto/ssh/knownhosts, // adding the ability to obtain the list of host key algorithms for a known host. package knownhosts import ( "bufio" "bytes" "encoding/base64" "errors" "fmt" "io" "net" "os" "sort" "strings" "golang.org/x/crypto/ssh" xknownhosts "golang.org/x/crypto/ssh/knownhosts" ) // HostKeyDB wraps logic in golang.org/x/crypto/ssh/knownhosts with additional // behaviors, such as the ability to perform host key/algorithm lookups from // known_hosts entries. type HostKeyDB struct { callback ssh.HostKeyCallback isCert map[string]bool // keyed by "filename:line" isWildcard map[string]bool // keyed by "filename:line" } // NewDB creates a HostKeyDB from the given OpenSSH known_hosts file(s). It // reads and parses the provided files one additional time (beyond logic in // golang.org/x/crypto/ssh/knownhosts) in order to: // // - Handle CA lines properly and return ssh.CertAlgo* values when calling the // HostKeyAlgorithms method, for use in ssh.ClientConfig.HostKeyAlgorithms // - Allow * wildcards in hostnames to match on non-standard ports, providing // a workaround for https://github.com/golang/go/issues/52056 in order to // align with OpenSSH's wildcard behavior // // When supplying multiple files, their order does not matter. func NewDB(files ...string) (*HostKeyDB, error) { cb, err := xknownhosts.New(files...) if err != nil { return nil, err } hkdb := &HostKeyDB{ callback: cb, isCert: make(map[string]bool), isWildcard: make(map[string]bool), } // Re-read each file a single time, looking for @cert-authority lines. The // logic for reading the file is designed to mimic hostKeyDB.Read from // golang.org/x/crypto/ssh/knownhosts for _, filename := range files { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() scanner := bufio.NewScanner(f) lineNum := 0 for scanner.Scan() { lineNum++ line := scanner.Bytes() line = bytes.TrimSpace(line) // Does the line start with "@cert-authority" followed by whitespace? if len(line) > 15 && bytes.HasPrefix(line, []byte("@cert-authority")) && (line[15] == ' ' || line[15] == '\t') { mapKey := fmt.Sprintf("%s:%d", filename, lineNum) hkdb.isCert[mapKey] = true line = bytes.TrimSpace(line[16:]) } // truncate line to just the host pattern field if i := bytes.IndexAny(line, "\t "); i >= 0 { line = line[:i] } // Does the host pattern contain a * wildcard and no specific port? if i := bytes.IndexRune(line, '*'); i >= 0 && !bytes.Contains(line[i:], []byte("]:")) { mapKey := fmt.Sprintf("%s:%d", filename, lineNum) hkdb.isWildcard[mapKey] = true } } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("knownhosts: %s:%d: %w", filename, lineNum, err) } } return hkdb, nil } // HostKeyCallback returns an ssh.HostKeyCallback. This can be used directly in // ssh.ClientConfig.HostKeyCallback, as shown in the example for NewDB. // Alternatively, you can wrap it with an outer callback to potentially handle // appending a new entry to the known_hosts file; see example in WriteKnownHost. func (hkdb *HostKeyDB) HostKeyCallback() ssh.HostKeyCallback { // Either NewDB found no wildcard host patterns, or hkdb was created from // HostKeyCallback.ToDB in which case we didn't scan known_hosts for them: // return the callback (which came from x/crypto/ssh/knownhosts) as-is if len(hkdb.isWildcard) == 0 { return hkdb.callback } // If we scanned for wildcards and found at least one, return a wrapped // callback with extra behavior: if the host lookup found no matches, and the // host arg had a non-standard port, re-do the lookup on standard port 22. If // that second call returns a *xknownhosts.KeyError, filter down any resulting // Want keys to known wildcard entries. f := func(hostname string, remote net.Addr, key ssh.PublicKey) error { callbackErr := hkdb.callback(hostname, remote, key) if callbackErr == nil || IsHostKeyChanged(callbackErr) { // hostname has known_host entries as-is return callbackErr } justHost, port, splitErr := net.SplitHostPort(hostname) if splitErr != nil || port == "" || port == "22" { // hostname already using standard port return callbackErr } // If we reach here, the port was non-standard and no known_host entries // were found for the non-standard port. Try again with standard port. if tcpAddr, ok := remote.(*net.TCPAddr); ok && tcpAddr.Port != 22 { remote = &net.TCPAddr{ IP: tcpAddr.IP, Port: 22, Zone: tcpAddr.Zone, } } callbackErr = hkdb.callback(justHost+":22", remote, key) var keyErr *xknownhosts.KeyError if errors.As(callbackErr, &keyErr) && len(keyErr.Want) > 0 { wildcardKeys := make([]xknownhosts.KnownKey, 0, len(keyErr.Want)) for _, wantKey := range keyErr.Want { if hkdb.isWildcard[fmt.Sprintf("%s:%d", wantKey.Filename, wantKey.Line)] { wildcardKeys = append(wildcardKeys, wantKey) } } callbackErr = &xknownhosts.KeyError{ Want: wildcardKeys, } } return callbackErr } return ssh.HostKeyCallback(f) } // PublicKey wraps ssh.PublicKey with an additional field, to identify // whether the key corresponds to a certificate authority. type PublicKey struct { ssh.PublicKey Cert bool } // HostKeys returns a slice of known host public keys for the supplied host:port // found in the known_hosts file(s), or an empty slice if the host is not // already known. For hosts that have multiple known_hosts entries (for // different key types), the result will be sorted by known_hosts filename and // line number. // If hkdb was originally created by calling NewDB, the Cert boolean field of // each result entry reports whether the key corresponded to a @cert-authority // line. If hkdb was NOT obtained from NewDB, then Cert will always be false. func (hkdb *HostKeyDB) HostKeys(hostWithPort string) (keys []PublicKey) { var keyErr *xknownhosts.KeyError placeholderAddr := &net.TCPAddr{IP: []byte{0, 0, 0, 0}} placeholderPubKey := &fakePublicKey{} var kkeys []xknownhosts.KnownKey callback := hkdb.HostKeyCallback() if hkcbErr := callback(hostWithPort, placeholderAddr, placeholderPubKey); errors.As(hkcbErr, &keyErr) { kkeys = append(kkeys, keyErr.Want...) knownKeyLess := func(i, j int) bool { if kkeys[i].Filename < kkeys[j].Filename { return true } return (kkeys[i].Filename == kkeys[j].Filename && kkeys[i].Line < kkeys[j].Line) } sort.Slice(kkeys, knownKeyLess) keys = make([]PublicKey, len(kkeys)) for n := range kkeys { keys[n] = PublicKey{ PublicKey: kkeys[n].Key, } if len(hkdb.isCert) > 0 { keys[n].Cert = hkdb.isCert[fmt.Sprintf("%s:%d", kkeys[n].Filename, kkeys[n].Line)] } } } return keys } // HostKeyAlgorithms returns a slice of host key algorithms for the supplied // host:port found in the known_hosts file(s), or an empty slice if the host // is not already known. The result may be used in ssh.ClientConfig's // HostKeyAlgorithms field, either as-is or after filtering (if you wish to // ignore or prefer particular algorithms). For hosts that have multiple // known_hosts entries (of different key types), the result will be sorted by // known_hosts filename and line number. // If hkdb was originally created by calling NewDB, any @cert-authority lines // in the known_hosts file will properly be converted to the corresponding // ssh.CertAlgo* values. func (hkdb *HostKeyDB) HostKeyAlgorithms(hostWithPort string) (algos []string) { // We ensure that algos never contains duplicates. This is done for robustness // even though currently golang.org/x/crypto/ssh/knownhosts never exposes // multiple keys of the same type. This way our behavior here is unaffected // even if https://github.com/golang/go/issues/28870 is implemented, for // example by https://github.com/golang/crypto/pull/254. hostKeys := hkdb.HostKeys(hostWithPort) seen := make(map[string]struct{}, len(hostKeys)) addAlgo := func(typ string, cert bool) { if cert { typ = keyTypeToCertAlgo(typ) } if _, already := seen[typ]; !already { algos = append(algos, typ) seen[typ] = struct{}{} } } for _, key := range hostKeys { typ := key.Type() if typ == ssh.KeyAlgoRSA { // KeyAlgoRSASHA256 and KeyAlgoRSASHA512 are only public key algorithms, // not public key formats, so they can't appear as a PublicKey.Type. // The corresponding PublicKey.Type is KeyAlgoRSA. See RFC 8332, Section 2. addAlgo(ssh.KeyAlgoRSASHA512, key.Cert) addAlgo(ssh.KeyAlgoRSASHA256, key.Cert) } addAlgo(typ, key.Cert) } return algos } func keyTypeToCertAlgo(keyType string) string { switch keyType { case ssh.KeyAlgoRSA: return ssh.CertAlgoRSAv01 case ssh.KeyAlgoRSASHA256: return ssh.CertAlgoRSASHA256v01 case ssh.KeyAlgoRSASHA512: return ssh.CertAlgoRSASHA512v01 case ssh.KeyAlgoDSA: return ssh.CertAlgoDSAv01 case ssh.KeyAlgoECDSA256: return ssh.CertAlgoECDSA256v01 case ssh.KeyAlgoSKECDSA256: return ssh.CertAlgoSKECDSA256v01 case ssh.KeyAlgoECDSA384: return ssh.CertAlgoECDSA384v01 case ssh.KeyAlgoECDSA521: return ssh.CertAlgoECDSA521v01 case ssh.KeyAlgoED25519: return ssh.CertAlgoED25519v01 case ssh.KeyAlgoSKED25519: return ssh.CertAlgoSKED25519v01 } return "" } // HostKeyCallback wraps ssh.HostKeyCallback with additional methods to // perform host key and algorithm lookups from the known_hosts entries. It is // otherwise identical to ssh.HostKeyCallback, and does not introduce any file- // parsing behavior beyond what is in golang.org/x/crypto/ssh/knownhosts. // // In most situations, use HostKeyDB and its constructor NewDB instead of using // the HostKeyCallback type. The HostKeyCallback type is only provided for // backwards compatibility with older versions of this package, as well as for // very strict situations where any extra known_hosts file-parsing is // undesirable. // // Methods of HostKeyCallback do not provide any special treatment for // @cert-authority lines, which will (incorrectly) look like normal non-CA host // keys. Additionally, HostKeyCallback lacks the fix for applying * wildcard // known_host entries to all ports, like OpenSSH's behavior. type HostKeyCallback ssh.HostKeyCallback // New creates a HostKeyCallback from the given OpenSSH known_hosts file(s). The // returned value may be used in ssh.ClientConfig.HostKeyCallback by casting it // to ssh.HostKeyCallback, or using its HostKeyCallback method. Otherwise, it // operates the same as the New function in golang.org/x/crypto/ssh/knownhosts. // When supplying multiple files, their order does not matter. // // In most situations, you should avoid this function, as the returned value // lacks several enhanced behaviors. See doc comment for HostKeyCallback for // more information. Instead, most callers should use NewDB to create a // HostKeyDB, which includes these enhancements. func New(files ...string) (HostKeyCallback, error) { cb, err := xknownhosts.New(files...) return HostKeyCallback(cb), err } // HostKeyCallback simply casts the receiver back to ssh.HostKeyCallback, for // use in ssh.ClientConfig.HostKeyCallback. func (hkcb HostKeyCallback) HostKeyCallback() ssh.HostKeyCallback { return ssh.HostKeyCallback(hkcb) } // ToDB converts the receiver into a HostKeyDB. However, the returned HostKeyDB // lacks the enhanced behaviors described in the doc comment for NewDB: proper // CA support, and wildcard matching on nonstandard ports. // // It is generally preferable to create a HostKeyDB by using NewDB. The ToDB // method is only provided for situations in which the calling code needs to // make the extra NewDB behaviors optional / user-configurable, perhaps for // reasons of performance or code trust (since NewDB reads the known_host file // an extra time, which may be undesirable in some strict situations). This way, // callers can conditionally create a non-enhanced HostKeyDB by using New and // ToDB. See code example. func (hkcb HostKeyCallback) ToDB() *HostKeyDB { // This intentionally leaves the isCert and isWildcard map fields as nil, as // there is no way to retroactively populate them from just a HostKeyCallback. // Methods of HostKeyDB will skip any related enhanced behaviors accordingly. return &HostKeyDB{callback: ssh.HostKeyCallback(hkcb)} } // HostKeys returns a slice of known host public keys for the supplied host:port // found in the known_hosts file(s), or an empty slice if the host is not // already known. For hosts that have multiple known_hosts entries (for // different key types), the result will be sorted by known_hosts filename and // line number. // In the returned values, there is no way to distinguish between CA keys // (known_hosts lines beginning with @cert-authority) and regular keys. To do // so, see NewDB and HostKeyDB.HostKeys instead. func (hkcb HostKeyCallback) HostKeys(hostWithPort string) []ssh.PublicKey { annotatedKeys := hkcb.ToDB().HostKeys(hostWithPort) rawKeys := make([]ssh.PublicKey, len(annotatedKeys)) for n, ak := range annotatedKeys { rawKeys[n] = ak.PublicKey } return rawKeys } // HostKeyAlgorithms returns a slice of host key algorithms for the supplied // host:port found in the known_hosts file(s), or an empty slice if the host // is not already known. The result may be used in ssh.ClientConfig's // HostKeyAlgorithms field, either as-is or after filtering (if you wish to // ignore or prefer particular algorithms). For hosts that have multiple // known_hosts entries (for different key types), the result will be sorted by // known_hosts filename and line number. // The returned values will not include ssh.CertAlgo* values. If any // known_hosts lines had @cert-authority prefixes, their original key algo will // be returned instead. For proper CA support, see NewDB and // HostKeyDB.HostKeyAlgorithms instead. func (hkcb HostKeyCallback) HostKeyAlgorithms(hostWithPort string) (algos []string) { return hkcb.ToDB().HostKeyAlgorithms(hostWithPort) } // HostKeyAlgorithms is a convenience function for performing host key algorithm // lookups on an ssh.HostKeyCallback directly. It is intended for use in code // paths that stay with the New method of golang.org/x/crypto/ssh/knownhosts // rather than this package's New or NewDB methods. // The returned values will not include ssh.CertAlgo* values. If any // known_hosts lines had @cert-authority prefixes, their original key algo will // be returned instead. For proper CA support, see NewDB and // HostKeyDB.HostKeyAlgorithms instead. func HostKeyAlgorithms(cb ssh.HostKeyCallback, hostWithPort string) []string { return HostKeyCallback(cb).HostKeyAlgorithms(hostWithPort) } // IsHostKeyChanged returns a boolean indicating whether the error indicates // the host key has changed. It is intended to be called on the error returned // from invoking a host key callback, to check whether an SSH host is known. func IsHostKeyChanged(err error) bool { var keyErr *xknownhosts.KeyError return errors.As(err, &keyErr) && len(keyErr.Want) > 0 } // IsHostUnknown returns a boolean indicating whether the error represents an // unknown host. It is intended to be called on the error returned from invoking // a host key callback to check whether an SSH host is known. func IsHostUnknown(err error) bool { var keyErr *xknownhosts.KeyError return errors.As(err, &keyErr) && len(keyErr.Want) == 0 } // Normalize normalizes an address into the form used in known_hosts. This // implementation includes a fix for https://github.com/golang/go/issues/53463 // and will omit brackets around ipv6 addresses on standard port 22. func Normalize(address string) string { host, port, err := net.SplitHostPort(address) if err != nil { host = address port = "22" } entry := host if port != "22" { entry = "[" + entry + "]:" + port } else if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { entry = entry[1 : len(entry)-1] } return entry } // Line returns a line to append to the known_hosts files. This implementation // uses the local patched implementation of Normalize in order to solve // https://github.com/golang/go/issues/53463. func Line(addresses []string, key ssh.PublicKey) string { var trimmed []string for _, a := range addresses { trimmed = append(trimmed, Normalize(a)) } return strings.Join([]string{ strings.Join(trimmed, ","), key.Type(), base64.StdEncoding.EncodeToString(key.Marshal()), }, " ") } // WriteKnownHost writes a known_hosts line to w for the supplied hostname, // remote, and key. This is useful when writing a custom hostkey callback which // wraps a callback obtained from this package to provide additional known_hosts // management functionality. The hostname, remote, and key typically correspond // to the callback's args. This function does not support writing // @cert-authority lines. func WriteKnownHost(w io.Writer, hostname string, remote net.Addr, key ssh.PublicKey) error { // Always include hostname; only also include remote if it isn't a zero value // and doesn't normalize to the same string as hostname. hostnameNormalized := Normalize(hostname) if strings.ContainsAny(hostnameNormalized, "\t ") { return fmt.Errorf("knownhosts: hostname '%s' contains spaces", hostnameNormalized) } addresses := []string{hostnameNormalized} remoteStrNormalized := Normalize(remote.String()) if remoteStrNormalized != "[0.0.0.0]:0" && remoteStrNormalized != hostnameNormalized && !strings.ContainsAny(remoteStrNormalized, "\t ") { addresses = append(addresses, remoteStrNormalized) } line := Line(addresses, key) + "\n" _, err := w.Write([]byte(line)) return err } // WriteKnownHostCA writes a @cert-authority line to w for the supplied host // name/pattern and key. func WriteKnownHostCA(w io.Writer, hostPattern string, key ssh.PublicKey) error { encodedKey := base64.StdEncoding.EncodeToString(key.Marshal()) _, err := fmt.Fprintf(w, "@cert-authority %s %s %s\n", hostPattern, key.Type(), encodedKey) return err } // fakePublicKey is used as part of the work-around for // https://github.com/golang/go/issues/29286 type fakePublicKey struct{} func (fakePublicKey) Type() string { return "fake-public-key" } func (fakePublicKey) Marshal() []byte { return []byte("fake public key") } func (fakePublicKey) Verify(_ []byte, _ *ssh.Signature) error { return errors.New("Verify called on placeholder key") } knownhosts-1.3.0/knownhosts_test.go000066400000000000000000000475521464556207100175620ustar00rootroot00000000000000package knownhosts import ( "bytes" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" "net" "os" "path/filepath" "testing" "golang.org/x/crypto/ssh" ) func TestNewDB(t *testing.T) { khPath := getTestKnownHosts(t) // Valid path should return a non-nil HostKeyDB and no error if kh, err := NewDB(khPath); kh == nil || err != nil { t.Errorf("Unexpected return from NewDB on valid known_hosts path: %v, %v", kh, err) } else { // Confirm return value of HostKeyCallback is an ssh.HostKeyCallback _ = ssh.ClientConfig{ HostKeyCallback: kh.HostKeyCallback(), } } // Append a @cert-authority line to the valid known_hosts file // Valid path should still return a non-nil HostKeyDB and no error appendCertTestKnownHosts(t, khPath, "*", ssh.KeyAlgoECDSA256) if kh, err := NewDB(khPath); kh == nil || err != nil { t.Errorf("Unexpected return from NewDB on valid known_hosts path containing a cert: %v, %v", kh, err) } // Write a second valid known_hosts file // Supplying both valid paths should still return a non-nil HostKeyDB and no // error appendCertTestKnownHosts(t, khPath+"2", "*.certy.test", ssh.KeyAlgoED25519) if kh, err := NewDB(khPath+"2", khPath); kh == nil || err != nil { t.Errorf("Unexpected return from NewDB on two valid known_hosts paths: %v, %v", kh, err) } // Invalid path should return an error, with or without other valid paths if _, err := NewDB(khPath + "_does_not_exist"); err == nil { t.Error("Expected error from NewDB with invalid path, but error was nil") } if _, err := NewDB(khPath, khPath+"_does_not_exist"); err == nil { t.Error("Expected error from NewDB with mix of valid and invalid paths, but error was nil") } } func TestNew(t *testing.T) { khPath := getTestKnownHosts(t) // Valid path should return a callback and no error; callback should be usable // in ssh.ClientConfig.HostKeyCallback if kh, err := New(khPath); err != nil { t.Errorf("Unexpected error from New on valid known_hosts path: %v", err) } else { // Confirm kh can be converted to an ssh.HostKeyCallback _ = ssh.ClientConfig{ HostKeyCallback: ssh.HostKeyCallback(kh), } // Confirm return value of HostKeyCallback is an ssh.HostKeyCallback _ = ssh.ClientConfig{ HostKeyCallback: kh.HostKeyCallback(), } } // Invalid path should return an error, with or without other valid paths if _, err := New(khPath + "_does_not_exist"); err == nil { t.Error("Expected error from New with invalid path, but error was nil") } if _, err := New(khPath, khPath+"_does_not_exist"); err == nil { t.Error("Expected error from New with mix of valid and invalid paths, but error was nil") } } func TestHostKeys(t *testing.T) { khPath := getTestKnownHosts(t) kh, err := New(khPath) if err != nil { t.Fatalf("Unexpected error from New: %v", err) } expectedKeyTypes := map[string][]string{ "only-rsa.example.test:22": {"ssh-rsa"}, "only-ecdsa.example.test:22": {"ecdsa-sha2-nistp256"}, "only-ed25519.example.test:22": {"ssh-ed25519"}, "multi.example.test:2233": {"ssh-rsa", "ecdsa-sha2-nistp256", "ssh-ed25519"}, "192.168.1.102:2222": {"ecdsa-sha2-nistp256", "ssh-ed25519"}, "unknown-host.example.test": {}, // host not in file "multi.example.test:22": {}, // different port than entry in file "192.168.1.102": {}, // different port than entry in file } for host, expected := range expectedKeyTypes { actual := kh.HostKeys(host) if len(actual) != len(expected) { t.Errorf("Unexpected number of keys returned by HostKeys(%q): expected %d, found %d", host, len(expected), len(actual)) continue } for n := range expected { if actualType := actual[n].Type(); expected[n] != actualType { t.Errorf("Unexpected key returned by HostKeys(%q): expected key[%d] to be type %v, found %v", host, n, expected, actualType) break } } } } func TestHostKeyAlgorithms(t *testing.T) { khPath := getTestKnownHosts(t) kh, err := New(khPath) if err != nil { t.Fatalf("Unexpected error from New: %v", err) } expectedAlgorithms := map[string][]string{ "only-rsa.example.test:22": {"rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"}, "only-ecdsa.example.test:22": {"ecdsa-sha2-nistp256"}, "only-ed25519.example.test:22": {"ssh-ed25519"}, "multi.example.test:2233": {"rsa-sha2-512", "rsa-sha2-256", "ssh-rsa", "ecdsa-sha2-nistp256", "ssh-ed25519"}, "192.168.1.102:2222": {"ecdsa-sha2-nistp256", "ssh-ed25519"}, "unknown-host.example.test": {}, // host not in file "multi.example.test:22": {}, // different port than entry in file "192.168.1.102": {}, // different port than entry in file } for host, expected := range expectedAlgorithms { actual := kh.HostKeyAlgorithms(host) actual2 := HostKeyAlgorithms(kh.HostKeyCallback(), host) if len(actual) != len(expected) || len(actual2) != len(expected) { t.Errorf("Unexpected number of algorithms returned by HostKeyAlgorithms(%q): expected %d, found %d", host, len(expected), len(actual)) continue } for n := range expected { if expected[n] != actual[n] || expected[n] != actual2[n] { t.Errorf("Unexpected algorithms returned by HostKeyAlgorithms(%q): expected %v, found %v", host, expected, actual) break } } } } func TestWithCertLines(t *testing.T) { khPath := getTestKnownHosts(t) khPath2 := khPath + "2" appendCertTestKnownHosts(t, khPath, "*.certy.test", ssh.KeyAlgoRSA) appendCertTestKnownHosts(t, khPath2, "*", ssh.KeyAlgoECDSA256) appendCertTestKnownHosts(t, khPath2, "*.certy.test", ssh.KeyAlgoED25519) // Test behavior of HostKeyCallback type, which doesn't properly handle // @cert-authority lines but shouldn't error on them. It should just return // them as regular keys / algorithms. cbOnly, err := New(khPath2, khPath) if err != nil { t.Fatalf("Unexpected error from New: %v", err) } algos := cbOnly.HostKeyAlgorithms("only-ed25519.example.test:22") // algos should return ssh.KeyAlgoED25519 (as per previous test) but now also // ssh.KeyAlgoECDSA256 due to the cert entry on *. They should always be in // that order due to matching the file and line order from NewDB. if len(algos) != 2 || algos[0] != ssh.KeyAlgoED25519 || algos[1] != ssh.KeyAlgoECDSA256 { t.Errorf("Unexpected return from HostKeyCallback.HostKeyAlgorithms: %v", algos) } // Now test behavior of HostKeyDB type, which should properly support // @cert-authority lines as being different from other lines kh, err := NewDB(khPath2, khPath) if err != nil { t.Fatalf("Unexpected error from NewDB: %v", err) } testCases := []struct { host string expectedKeyTypes []string expectedIsCert []bool expectedAlgos []string }{ { host: "only-ed25519.example.test:22", expectedKeyTypes: []string{ssh.KeyAlgoED25519, ssh.KeyAlgoECDSA256}, expectedIsCert: []bool{false, true}, expectedAlgos: []string{ssh.KeyAlgoED25519, ssh.CertAlgoECDSA256v01}, }, { host: "only-rsa.example.test:22", expectedKeyTypes: []string{ssh.KeyAlgoRSA, ssh.KeyAlgoECDSA256}, expectedIsCert: []bool{false, true}, expectedAlgos: []string{ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSA, ssh.CertAlgoECDSA256v01}, }, { host: "whatever.test:22", // only matches the * entry expectedKeyTypes: []string{ssh.KeyAlgoECDSA256}, expectedIsCert: []bool{true}, expectedAlgos: []string{ssh.CertAlgoECDSA256v01}, }, { host: "whatever.test:22022", // only matches the * entry expectedKeyTypes: []string{ssh.KeyAlgoECDSA256}, expectedIsCert: []bool{true}, expectedAlgos: []string{ssh.CertAlgoECDSA256v01}, }, { host: "asdf.certy.test:22", expectedKeyTypes: []string{ssh.KeyAlgoRSA, ssh.KeyAlgoECDSA256, ssh.KeyAlgoED25519}, expectedIsCert: []bool{true, true, true}, expectedAlgos: []string{ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSAv01, ssh.CertAlgoECDSA256v01, ssh.CertAlgoED25519v01}, }, { host: "oddport.certy.test:2345", expectedKeyTypes: []string{ssh.KeyAlgoRSA, ssh.KeyAlgoECDSA256, ssh.KeyAlgoED25519}, expectedIsCert: []bool{true, true, true}, expectedAlgos: []string{ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSAv01, ssh.CertAlgoECDSA256v01, ssh.CertAlgoED25519v01}, }, } for _, tc := range testCases { annotatedKeys := kh.HostKeys(tc.host) if len(annotatedKeys) != len(tc.expectedKeyTypes) { t.Errorf("Unexpected return from HostKeys(%q): %v", tc.host, annotatedKeys) } else { for n := range annotatedKeys { if annotatedKeys[n].Type() != tc.expectedKeyTypes[n] || annotatedKeys[n].Cert != tc.expectedIsCert[n] { t.Errorf("Unexpected return from HostKeys(%q) at index %d: %v", tc.host, n, annotatedKeys) break } } } algos := kh.HostKeyAlgorithms(tc.host) if len(algos) != len(tc.expectedAlgos) { t.Errorf("Unexpected return from HostKeyAlgorithms(%q): %v", tc.host, algos) } else { for n := range algos { if algos[n] != tc.expectedAlgos[n] { t.Errorf("Unexpected return from HostKeyAlgorithms(%q) at index %d: %v", tc.host, n, algos) break } } } } } func TestIsHostKeyChanged(t *testing.T) { khPath := getTestKnownHosts(t) kh, err := New(khPath) if err != nil { t.Fatalf("Unexpected error from New: %v", err) } noAddr, _ := net.ResolveTCPAddr("tcp", "0.0.0.0:0") pubKey := generatePubKeyEd25519(t) // Unknown host: should return false if err := kh("unknown.example.test:22", noAddr, pubKey); IsHostKeyChanged(err) { t.Error("IsHostKeyChanged unexpectedly returned true for unknown host") } // Known host, wrong key: should return true if err := kh("multi.example.test:2233", noAddr, pubKey); !IsHostKeyChanged(err) { t.Error("IsHostKeyChanged unexpectedly returned false for known host with different host key") } // Append the key for a known host that doesn't already have that key type, // re-init the known_hosts, and check again: should return false f, err := os.OpenFile(khPath, os.O_APPEND|os.O_WRONLY, 0600) if err != nil { t.Fatalf("Unable to open %s for writing: %v", khPath, err) } if err := WriteKnownHost(f, "only-ecdsa.example.test:22", noAddr, pubKey); err != nil { t.Fatalf("Unable to write known host line: %v", err) } f.Close() if kh, err = New(khPath); err != nil { t.Fatalf("Unexpected error from New: %v", err) } if err := kh("only-ecdsa.example.test:22", noAddr, pubKey); IsHostKeyChanged(err) { t.Error("IsHostKeyChanged unexpectedly returned true for valid known host") } } func TestIsHostUnknown(t *testing.T) { khPath := getTestKnownHosts(t) kh, err := New(khPath) if err != nil { t.Fatalf("Unexpected error from New: %v", err) } noAddr, _ := net.ResolveTCPAddr("tcp", "0.0.0.0:0") pubKey := generatePubKeyEd25519(t) // Unknown host: should return true if err := kh("unknown.example.test:22", noAddr, pubKey); !IsHostUnknown(err) { t.Error("IsHostUnknown unexpectedly returned false for unknown host") } // Known host, wrong key: should return false if err := kh("multi.example.test:2233", noAddr, pubKey); IsHostUnknown(err) { t.Error("IsHostUnknown unexpectedly returned true for known host with different host key") } // Append the key for an unknown host, re-init the known_hosts, and check // again: should return false f, err := os.OpenFile(khPath, os.O_APPEND|os.O_WRONLY, 0600) if err != nil { t.Fatalf("Unable to open %s for writing: %v", khPath, err) } if err := WriteKnownHost(f, "newhost.example.test:22", noAddr, pubKey); err != nil { t.Fatalf("Unable to write known host line: %v", err) } f.Close() if kh, err = New(khPath); err != nil { t.Fatalf("Unexpected error from New: %v", err) } if err := kh("newhost.example.test:22", noAddr, pubKey); IsHostUnknown(err) { t.Error("IsHostUnknown unexpectedly returned true for valid known host") } } func TestNormalize(t *testing.T) { for in, want := range map[string]string{ "127.0.0.1": "127.0.0.1", "127.0.0.1:22": "127.0.0.1", "[127.0.0.1]:22": "127.0.0.1", "[127.0.0.1]:23": "[127.0.0.1]:23", "127.0.0.1:23": "[127.0.0.1]:23", "[a.b.c]:22": "a.b.c", "abcd::abcd:abcd:abcd": "abcd::abcd:abcd:abcd", "[abcd::abcd:abcd:abcd]": "abcd::abcd:abcd:abcd", "[abcd::abcd:abcd:abcd]:22": "abcd::abcd:abcd:abcd", "[abcd::abcd:abcd:abcd]:23": "[abcd::abcd:abcd:abcd]:23", } { got := Normalize(in) if got != want { t.Errorf("Normalize(%q) = %q, want %q", in, got, want) } } } func TestLine(t *testing.T) { edKeyStr := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF9Wn63tLEhSWl9Ye+4x2GnruH8cq0LIh2vum/fUHrFQ" edKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(edKeyStr)) if err != nil { t.Fatalf("Unable to parse authorized key: %v", err) } for in, want := range map[string]string{ "server.org": "server.org " + edKeyStr, "server.org:22": "server.org " + edKeyStr, "server.org:23": "[server.org]:23 " + edKeyStr, "[c629:1ec4:102:304:102:304:102:304]:22": "c629:1ec4:102:304:102:304:102:304 " + edKeyStr, "[c629:1ec4:102:304:102:304:102:304]:23": "[c629:1ec4:102:304:102:304:102:304]:23 " + edKeyStr, } { if got := Line([]string{in}, edKey); got != want { t.Errorf("Line(%q) = %q, want %q", in, got, want) } } } func TestWriteKnownHost(t *testing.T) { edKeyStr := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIF9Wn63tLEhSWl9Ye+4x2GnruH8cq0LIh2vum/fUHrFQ" edKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(edKeyStr)) if err != nil { t.Fatalf("Unable to parse authorized key: %v", err) } for _, m := range []struct { hostname string remoteAddr string want string err string }{ {hostname: "::1", remoteAddr: "[::1]:22", want: "::1 " + edKeyStr + "\n"}, {hostname: "127.0.0.1", remoteAddr: "127.0.0.1:22", want: "127.0.0.1 " + edKeyStr + "\n"}, {hostname: "ipv4.test", remoteAddr: "192.168.0.1:23", want: "ipv4.test,[192.168.0.1]:23 " + edKeyStr + "\n"}, {hostname: "ipv6.test", remoteAddr: "[ff01::1234]:23", want: "ipv6.test,[ff01::1234]:23 " + edKeyStr + "\n"}, {hostname: "normal.zone", remoteAddr: "[fe80::1%en0]:22", want: "normal.zone,fe80::1%en0 " + edKeyStr + "\n"}, {hostname: "spaces.zone", remoteAddr: "[fe80::1%Ethernet 1]:22", want: "spaces.zone " + edKeyStr + "\n"}, {hostname: "spaces.zone", remoteAddr: "[fe80::1%Ethernet\t2]:23", want: "spaces.zone " + edKeyStr + "\n"}, {hostname: "[fe80::1%Ethernet 1]:22", err: "knownhosts: hostname 'fe80::1%Ethernet 1' contains spaces"}, {hostname: "[fe80::1%Ethernet\t2]:23", err: "knownhosts: hostname '[fe80::1%Ethernet\t2]:23' contains spaces"}, } { remote, err := net.ResolveTCPAddr("tcp", m.remoteAddr) if err != nil { t.Fatalf("Unable to resolve tcp addr: %v", err) } var got bytes.Buffer err = WriteKnownHost(&got, m.hostname, remote, edKey) if m.err != "" { if err == nil || err.Error() != m.err { t.Errorf("WriteKnownHost(%q) expected error %v, found %v", m.hostname, m.err, err) } continue } if err != nil { t.Fatalf("Unable to write known host: %v", err) } if got.String() != m.want { t.Errorf("WriteKnownHost(%q) = %q, want %q", m.hostname, got.String(), m.want) } } } func TestFakePublicKey(t *testing.T) { fpk := fakePublicKey{} if err := fpk.Verify(nil, nil); err == nil { t.Error("Expected fakePublicKey.Verify() to always return an error, but it did not") } if certAlgo := keyTypeToCertAlgo(fpk.Type()); certAlgo != "" { t.Errorf("Expected keyTypeToCertAlgo on a fakePublicKey to return an empty string, but instead found %q", certAlgo) } } var testKnownHostsContents []byte // getTestKnownHosts returns a path to a test known_hosts file. The file path // will differ between test functions, but the contents are always the same, // containing keys generated upon the first invocation. The file is removed // upon test completion. func getTestKnownHosts(t *testing.T) string { // Re-use previously memoized result if len(testKnownHostsContents) > 0 { dir := t.TempDir() khPath := filepath.Join(dir, "known_hosts") if err := os.WriteFile(khPath, testKnownHostsContents, 0600); err != nil { t.Fatalf("Unable to write to %s: %v", khPath, err) } return khPath } khPath := writeTestKnownHosts(t) if contents, err := os.ReadFile(khPath); err == nil { testKnownHostsContents = contents } return khPath } // writeTestKnownHosts generates the test known_hosts file and returns the // file path to it. The generated file contains several hosts with a mix of // key types; each known host has between 1 and 4 different known host keys. // If generating or writing the file fails, the test fails. func writeTestKnownHosts(t *testing.T) string { t.Helper() hosts := map[string][]ssh.PublicKey{ "only-rsa.example.test:22": {generatePubKeyRSA(t)}, "only-ecdsa.example.test:22": {generatePubKeyECDSA(t)}, "only-ed25519.example.test:22": {generatePubKeyEd25519(t)}, "multi.example.test:2233": {generatePubKeyRSA(t), generatePubKeyECDSA(t), generatePubKeyEd25519(t), generatePubKeyEd25519(t)}, "192.168.1.102:2222": {generatePubKeyECDSA(t), generatePubKeyEd25519(t)}, "[fe80::abc:abc:abcd:abcd]:22": {generatePubKeyEd25519(t), generatePubKeyRSA(t)}, } dir := t.TempDir() khPath := filepath.Join(dir, "known_hosts") f, err := os.OpenFile(khPath, os.O_WRONLY|os.O_CREATE, 0600) if err != nil { t.Fatalf("Unable to open %s for writing: %v", khPath, err) } defer f.Close() noAddr, _ := net.ResolveTCPAddr("tcp", "0.0.0.0:0") for host, keys := range hosts { for _, k := range keys { if err := WriteKnownHost(f, host, noAddr, k); err != nil { t.Fatalf("Unable to write known host line: %v", err) } } } return khPath } var testCertKeys = make(map[string]ssh.PublicKey) // key string format is "hostpattern keytype" // appendCertTestKnownHosts adds a @cert-authority line to the file at the // supplied path, creating it if it does not exist yet. The keyType must be one // of ssh.KeyAlgoRSA, ssh.KeyAlgoECDSA256, or ssh.KeyAlgoED25519; while all // valid algos are supported by this package, the test logic hasn't been // written for other algos here yet. Generated keys are memoized to avoid // slow test performance. func appendCertTestKnownHosts(t *testing.T, filePath, hostPattern, keyType string) { t.Helper() var pubKey ssh.PublicKey var ok bool cacheKey := hostPattern + " " + keyType if pubKey, ok = testCertKeys[cacheKey]; !ok { switch keyType { case ssh.KeyAlgoRSA: pubKey = generatePubKeyRSA(t) case ssh.KeyAlgoECDSA256: pubKey = generatePubKeyECDSA(t) case ssh.KeyAlgoED25519: pubKey = generatePubKeyEd25519(t) default: t.Fatalf("test logic does not support generating key of type %s yet", keyType) } testCertKeys[cacheKey] = pubKey } f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) if err != nil { t.Fatalf("Unable to open %s for writing: %v", filePath, err) } defer f.Close() if err := WriteKnownHostCA(f, hostPattern, pubKey); err != nil { t.Fatalf("Unable to append @cert-authority line to %s: %v", filePath, err) } } func generatePubKeyRSA(t *testing.T) ssh.PublicKey { t.Helper() privKey, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { t.Fatalf("Unable to generate RSA key: %v", err) } pub, err := ssh.NewPublicKey(&privKey.PublicKey) if err != nil { t.Fatalf("Unable to convert public key: %v", err) } return pub } func generatePubKeyECDSA(t *testing.T) ssh.PublicKey { t.Helper() privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("Unable to generate ECDSA key: %v", err) } pub, err := ssh.NewPublicKey(privKey.Public()) if err != nil { t.Fatalf("Unable to convert public key: %v", err) } return pub } func generatePubKeyEd25519(t *testing.T) ssh.PublicKey { t.Helper() rawPub, _, err := ed25519.GenerateKey(nil) if err != nil { t.Fatalf("Unable to generate ed25519 key: %v", err) } pub, err := ssh.NewPublicKey(rawPub) if err != nil { t.Fatalf("Unable to convert public key: %v", err) } return pub }