github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/utils/sshutils/ssh_test.go (about) 1 /* 2 Copyright 2021 Gravitational, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package sshutils 18 19 import ( 20 "encoding/base64" 21 "fmt" 22 "testing" 23 24 "github.com/stretchr/testify/require" 25 "golang.org/x/crypto/ssh" 26 ) 27 28 // TestHostKeyCallback verifies that host key callback properly validates 29 // host certificates. 30 func TestHostKeyCallback(t *testing.T) { 31 ca, err := MakeTestSSHCA() 32 require.NoError(t, err) 33 34 realCert, err := MakeRealHostCert(ca) 35 require.NoError(t, err) 36 37 spoofedCert, err := MakeSpoofedHostCert(ca) 38 require.NoError(t, err) 39 40 knownHosts := [][]byte{[]byte(makeKnownHostsLine("127.0.0.1", ca.PublicKey()))} 41 trustedKeys, err := ParseKnownHosts(knownHosts) 42 require.NoError(t, err) 43 44 hostKeyCallback, err := HostKeyCallback(trustedKeys, false) 45 require.NoError(t, err) 46 47 err = hostKeyCallback("127.0.0.1:3022", nil, realCert.PublicKey()) 48 require.NoError(t, err, "host key callback rejected valid host certificate") 49 50 err = hostKeyCallback("127.0.0.1:3022", nil, spoofedCert.PublicKey()) 51 require.Error(t, err, "host key callback accepted spoofed host certificate") 52 } 53 54 func makeKnownHostsLine(host string, key ssh.PublicKey) string { 55 return fmt.Sprintf("%v %v %v", host, key.Type(), 56 base64.StdEncoding.EncodeToString(key.Marshal())) 57 } 58 59 // Tests symmetric equality of key equality. 60 func TestSSHKeysEqual(t *testing.T) { 61 rsaKey1, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQClwXUKOp/S4XEtFjgr8mfaCy4OyI7N9ZMibdCGxvk2VHP9+Vn8Al1lUSVwuBxHI7EHiq42RCTBetIpTjzn6yiPNAeGNL5cfl9i6r+P5k7og1hz+2oheWveGodx6Dp+Z4o2dw65NGf5EPaotXF8AcHJc3+OiMS5yp/x2A3tu2I1SPQ6dtPa067p8q1L49BKbFwrFRBCVwkr6kpEQAIjnMESMPGD5Buu/AtyAdEZQSLTt8RZajJZDfXFKMEtQm2UF248NFl3hSMAcbbTxITBbZxX7THbwQz22Yuw7422G5CYBPf6WRXBY84Rs6jCS4I4GMxj+3rF4mGtjvuz0wOE32s3w4eMh9h3bPuEynufjE8henmPCIW49+kuZO4LZut7Zg5BfVDQnZYclwokEIMz+gR02YpyflxQOa98t/0mENu+t4f0LNAdkQEBpYtGKKDth5kLphi2Sdi9JpGO2sTivlxMsGyBqdd0wT9VwQpWf4wro6t09HdZJX1SAuEi/0tNI10= friel@test")) 62 require.NoError(t, err) 63 // Same as above, but different comment 64 rsaKey1Alt, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQClwXUKOp/S4XEtFjgr8mfaCy4OyI7N9ZMibdCGxvk2VHP9+Vn8Al1lUSVwuBxHI7EHiq42RCTBetIpTjzn6yiPNAeGNL5cfl9i6r+P5k7og1hz+2oheWveGodx6Dp+Z4o2dw65NGf5EPaotXF8AcHJc3+OiMS5yp/x2A3tu2I1SPQ6dtPa067p8q1L49BKbFwrFRBCVwkr6kpEQAIjnMESMPGD5Buu/AtyAdEZQSLTt8RZajJZDfXFKMEtQm2UF248NFl3hSMAcbbTxITBbZxX7THbwQz22Yuw7422G5CYBPf6WRXBY84Rs6jCS4I4GMxj+3rF4mGtjvuz0wOE32s3w4eMh9h3bPuEynufjE8henmPCIW49+kuZO4LZut7Zg5BfVDQnZYclwokEIMz+gR02YpyflxQOa98t/0mENu+t4f0LNAdkQEBpYtGKKDth5kLphi2Sdi9JpGO2sTivlxMsGyBqdd0wT9VwQpWf4wro6t09HdZJX1SAuEi/0tNI10= other@foo")) 65 require.NoError(t, err) 66 rsaKey2, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxNlAqagvmYJSKcSZeTfAJj8ch/wKeTuwOjUOvuudXelAl6ZNslu2a9IrRyu0SIKtHSxgJG7BPuz4bPSkh7/unjCDIRjuxzIEyA2Dud+MDG2QgFrgQqSuDQS/1CJwlqm9i/UdlbrQOrkX7dwIoY2f+bzW1JR3tLjhCQkCZzkViGkY3ELew+Pdu+aagS/BeR+fJNLKvGx8NwAFjZe/YoTJ+vwDNhRP2NyDe1ISSX9PWGvqy4cCQ3WBOsuocpX+XR01tTuxVFsw5fQ+U4vb52aIDUv5VYU5ioG9TLpR1HL9Lu8l0mDI1lGtzHl0/uKEyAFUghyD0ow25vLQzkG/3bjOla6ehQKvSrXly5TbGed+QfphK27hVVctnehO8PyP3ANJK4bzy3fKdq7EmzoGwBIc2QvX3RjdzWJw37kNw44cAsEYVJRJdtaFOB9nFvtCRSPYM0CJj081Sjgqwvnd4cGgUfQjGYdQiMmzLA11PryUQ6ahZVeVhu3TE809ZrJI8HIc= friel@test")) 67 require.NoError(t, err) 68 69 ed25519Key1, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGtqQKEkGIY5+Bc4EmEv7NeSn6aA7KMl5eiNEAOqwTBl friel@test")) 70 require.NoError(t, err) 71 ed25519Key1Alt, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGtqQKEkGIY5+Bc4EmEv7NeSn6aA7KMl5eiNEAOqwTBl other@foo")) 72 require.NoError(t, err) 73 ed25519Key2, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAhOF1Yw9LyTcIM1ku2hrqYcJ4e+784zp2XX4oIAWRuZ friel@test")) 74 require.NoError(t, err) 75 76 // The key material says this is RSA, the prefix is irrelevant. 77 disguisedRsaKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAB3NzaC1yc2EAAAADAQABAAABgQClwXUKOp/S4XEtFjgr8mfaCy4OyI7N9ZMibdCGxvk2VHP9+Vn8Al1lUSVwuBxHI7EHiq42RCTBetIpTjzn6yiPNAeGNL5cfl9i6r+P5k7og1hz+2oheWveGodx6Dp+Z4o2dw65NGf5EPaotXF8AcHJc3+OiMS5yp/x2A3tu2I1SPQ6dtPa067p8q1L49BKbFwrFRBCVwkr6kpEQAIjnMESMPGD5Buu/AtyAdEZQSLTt8RZajJZDfXFKMEtQm2UF248NFl3hSMAcbbTxITBbZxX7THbwQz22Yuw7422G5CYBPf6WRXBY84Rs6jCS4I4GMxj+3rF4mGtjvuz0wOE32s3w4eMh9h3bPuEynufjE8henmPCIW49+kuZO4LZut7Zg5BfVDQnZYclwokEIMz+gR02YpyflxQOa98t/0mENu+t4f0LNAdkQEBpYtGKKDth5kLphi2Sdi9JpGO2sTivlxMsGyBqdd0wT9VwQpWf4wro6t09HdZJX1SAuEi/0tNI10= friel@Zing")) 78 require.NoError(t, err) 79 80 // The key material says this is Ed25519, the prefix is irrelevant. 81 disguisedEd25519Key, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-rsa AAAAC3NzaC1lZDI1NTE5AAAAIGtqQKEkGIY5+Bc4EmEv7NeSn6aA7KMl5eiNEAOqwTBl friel@test")) 82 require.NoError(t, err) 83 84 type test struct { 85 name string 86 variant string 87 key ssh.PublicKey 88 } 89 90 keys := []test{ 91 {name: "rsaKey1", key: rsaKey1}, 92 {name: "rsaKey1", variant: "-alt", key: rsaKey1Alt}, 93 {name: "rsaKey2", key: rsaKey2}, 94 {name: "ed25519Key1", variant: "-rsa-prefixed", key: disguisedEd25519Key}, 95 {name: "ed25519Key1", key: ed25519Key1}, 96 {name: "ed25519Key1", variant: "-alt", key: ed25519Key1Alt}, 97 {name: "ed25519Key2", key: ed25519Key2}, 98 {name: "rsaKey1", variant: "-ed25519-prefixed", key: disguisedRsaKey}, 99 } 100 101 for _, ak := range keys { 102 for _, bk := range keys { 103 expected := ak.name == bk.name 104 105 var op string 106 if expected { 107 op = "==" 108 } else { 109 op = "!=" 110 } 111 112 t.Run(fmt.Sprintf("%v%v%v%v%v", ak.name, ak.variant, op, bk.name, bk.variant), func(t *testing.T) { 113 actual := KeysEqual(ak.key, bk.key) 114 require.Equal(t, expected, actual) 115 }) 116 } 117 } 118 } 119 120 func TestSSHMarshalEd25519(t *testing.T) { 121 ak, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGtqQKEkGIY5+Bc4EmEv7NeSn6aA7KMl5eiNEAOqwTBl friel@test")) 122 require.NoError(t, err) 123 124 bk, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGtqQKEkGIY5+Bc4EmEv7NeSn6aA7KMl5eiNEAOqwTBl friel@test")) 125 require.NoError(t, err) 126 127 result := KeysEqual(ak, bk) 128 require.True(t, result) 129 } 130 131 func TestMatchesWildcard(t *testing.T) { 132 t.Parallel() 133 t.Run("Wildcard match", func(t *testing.T) { 134 require.True(t, matchesWildcard("foo.example.com", "*.example.com")) 135 require.True(t, matchesWildcard("bar.example.com", "*.example.com")) 136 require.True(t, matchesWildcard("bar.example.com.", "*.example.com")) 137 require.True(t, matchesWildcard("bar.foo", "*.foo")) 138 }) 139 140 t.Run("Wildcard mismatch", func(t *testing.T) { 141 require.False(t, matchesWildcard("foo.example.com", "example.com"), "Not a wildcard pattern") 142 require.False(t, matchesWildcard("foo.example.org", "*.example.com"), "Wildcard pattern shouldn't match different suffix") 143 require.False(t, matchesWildcard("a.b.example.com", "*.example.com"), "Wildcard pattern shouldn't match multiple prefixes") 144 145 t.Run("Single part hostname", func(t *testing.T) { 146 require.False(t, matchesWildcard("example", "*.example.com")) 147 require.False(t, matchesWildcard("example", "*.example")) 148 require.False(t, matchesWildcard("example", "example")) 149 require.False(t, matchesWildcard("example", "*.")) 150 }) 151 }) 152 }