forked from skeema/knownhosts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathknownhosts.go
244 lines (224 loc) · 8.61 KB
/
knownhosts.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// 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 (
"encoding/base64"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"sort"
"strings"
"golang.org/x/crypto/ssh"
xknownhosts "golang.org/x/crypto/ssh/knownhosts"
)
// HostKeyCallback wraps ssh.HostKeyCallback with an additional method to
// perform host key algorithm lookups from the known_hosts entries.
type HostKeyCallback ssh.HostKeyCallback
// New creates a host key callback from the given OpenSSH host key files. 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.
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)
}
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.
func (hkcb HostKeyCallback) HostKeys(hostWithPort string) (keys []PublicKey) {
var keyErr *xknownhosts.KeyError
placeholderAddr := &net.TCPAddr{IP: []byte{0, 0, 0, 0}}
placeholderPubKey := &fakePublicKey{}
var kkeys []xknownhosts.KnownKey
if hkcbErr := hkcb(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, k := range kkeys {
content, err := ioutil.ReadFile(k.Filename)
if err != nil {
continue
}
lines := strings.Split(string(content), "\n")
line := lines[k.Line-1]
isCert := strings.HasPrefix(line, "@cert-authority")
keys[n] = PublicKey{
PublicKey: k.Key,
cert: isCert,
}
}
}
return keys
}
func keyTypeToCertType(keyType string) string {
switch keyType {
case ssh.KeyAlgoRSA:
return ssh.CertAlgoRSAv01
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 ""
}
// 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.
func (hkcb HostKeyCallback) 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 := hkcb.HostKeys(hostWithPort)
seen := make(map[string]struct{}, len(hostKeys))
addAlgo := func(typ string) {
if _, already := seen[typ]; !already {
algos = append(algos, typ)
seen[typ] = struct{}{}
}
}
for _, key := range hostKeys {
typ := key.Type()
if key.cert {
certType := keyTypeToCertType(typ)
if certType == ssh.CertAlgoRSAv01 {
// CertAlgoRSASHA256v01 and CertAlgoRSASHA512v01 can't appear as a
// Certificate.Type (or PublicKey.Type), but only in
// ClientConfig.HostKeyAlgorithms.
addAlgo(ssh.CertAlgoRSASHA256v01)
addAlgo(ssh.CertAlgoRSASHA512v01)
}
addAlgo(certType)
} else {
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)
addAlgo(ssh.KeyAlgoRSASHA256)
}
addAlgo(typ)
}
}
return algos
}
// 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 method.
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 HostKeyCallback 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 HostKeyCallback 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 writer for the supplied hostname,
// remote, and key. This is useful when writing a custom hostkey callback which
// wraps a callback obtained from knownhosts.New to provide additional
// known_hosts management functionality. The hostname, remote, and key typically
// correspond to the callback's args.
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
}
// 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")
}