├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /whoarethey 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a 2 | copy of this software and associated documentation files (the "Software"), 3 | to deal in the Software without restriction, including without limitation 4 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 5 | and/or sell copies of the Software, and to permit persons to whom the 6 | Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included 9 | in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 14 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 15 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 16 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 17 | OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | Except as contained in this notice, the name(s) of the above copyright 20 | holders shall not be used in advertising or otherwise to promote the 21 | sale, use or other dealings in this Software without prior written 22 | authorization. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # whoarethey 2 | 3 | A program to determine what SSH keys a server accepts. See 4 | https://www.agwa.name/blog/post/whoarethey for background. 5 | Inspired by [whoami.filippo.io](https://words.filippo.io/dispatches/whoami-updated/). 6 | 7 | ## Install 8 | 9 | If you have the latest version of Go installed, you can run: 10 | 11 | ``` 12 | go install src.agwa.name/whoarethey@latest 13 | ``` 14 | 15 | ## Usage 16 | 17 | Specify the host/port of the SSH server, the username to try logging 18 | in as, and one or more keys, which can specified as the name of an 19 | `authorized_keys`-formatted file, or a GitHub username prefixed with 20 | `github:`: 21 | 22 | ``` 23 | whoarethey HOST:PORT USERNAME KEYSFILE|github:USERNAME... 24 | ``` 25 | 26 | The program outputs a list of the keys which were accepted by the server. 27 | 28 | ## Example 29 | 30 | To determine if @AGWA or @FiloSottile can log in as root on 192.0.2.4: 31 | 32 | ``` 33 | $ whoarethey 192.0.2.4:22 root github:AGWA github:FiloSottile 34 | github:AGWA 35 | ``` 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module src.agwa.name/whoarethey 2 | 3 | go 1.19 4 | 5 | require golang.org/x/crypto v0.5.0 6 | 7 | require golang.org/x/sys v0.4.0 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 2 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 3 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 4 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 5 | golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= 6 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020-2023 Andrew Ayer 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a 4 | // copy of this software and associated documentation files (the "Software"), 5 | // to deal in the Software without restriction, including without limitation 6 | // the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | // and/or sell copies of the Software, and to permit persons to whom the 8 | // Software is furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included 11 | // in all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | // OTHER DEALINGS IN THE SOFTWARE. 20 | // 21 | // Except as contained in this notice, the name(s) of the above copyright 22 | // holders shall not be used in advertising or otherwise to promote the 23 | // sale, use or other dealings in this Software without prior written 24 | // authorization. 25 | 26 | package main 27 | 28 | import ( 29 | "fmt" 30 | "golang.org/x/crypto/ssh" 31 | "io" 32 | "net/http" 33 | "os" 34 | "strings" 35 | ) 36 | 37 | type DummySigner struct { 38 | PubKey ssh.PublicKey 39 | Comment string 40 | Tried bool 41 | Accepted bool 42 | } 43 | 44 | func (signer *DummySigner) PublicKey() ssh.PublicKey { 45 | signer.Tried = true 46 | return signer.PubKey 47 | } 48 | func (signer *DummySigner) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) { 49 | signer.Accepted = true 50 | //if signer.Comment != "" { 51 | // log.Printf("Server accepted '%s'.", signer.Comment) 52 | //} 53 | return &ssh.Signature{ 54 | Format: signer.PubKey.Type(), 55 | }, nil 56 | } 57 | func ParseAuthorizedKeys(in []byte) ([]*DummySigner, error) { 58 | signers := []*DummySigner{} 59 | for len(in) > 0 { 60 | pubkey, comment, _, rest, err := ssh.ParseAuthorizedKey(in) 61 | if err != nil { 62 | return nil, err 63 | } 64 | signers = append(signers, &DummySigner{ 65 | PubKey: pubkey, 66 | Comment: comment, 67 | }) 68 | in = rest 69 | } 70 | return signers, nil 71 | } 72 | func MakeAuthMethod(dummySigners []*DummySigner) ssh.AuthMethod { 73 | signers := make([]ssh.Signer, len(dummySigners)) 74 | for i := range dummySigners { 75 | signers[i] = dummySigners[i] 76 | } 77 | return ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { return signers, nil }) 78 | } 79 | 80 | func LoadSignersFromFile(filename string) ([]*DummySigner, error) { 81 | fileBytes, err := os.ReadFile(filename) 82 | if err != nil { 83 | return nil, err 84 | } 85 | signers, err := ParseAuthorizedKeys(fileBytes) 86 | if err != nil { 87 | return nil, err 88 | } 89 | return signers, nil 90 | } 91 | func LoadSignersFromGitHub(username string) ([]*DummySigner, error) { 92 | url := "https://github.com/" + username + ".keys" 93 | resp, err := http.Get(url) 94 | if err != nil { 95 | return nil, err 96 | } 97 | defer resp.Body.Close() 98 | if resp.StatusCode != 200 { 99 | return nil, fmt.Errorf("Error retrieving %s: %s", url, resp.Status) 100 | } 101 | body, err := io.ReadAll(resp.Body) 102 | if err != nil { 103 | return nil, err 104 | } 105 | signers, err := ParseAuthorizedKeys(body) 106 | if err != nil { 107 | return nil, err 108 | } 109 | return signers, nil 110 | } 111 | func LoadSigners(source string) ([]*DummySigner, error) { 112 | if strings.HasPrefix(source, "github:") { 113 | return LoadSignersFromGitHub(strings.TrimPrefix(source, "github:")) 114 | } else { 115 | return LoadSignersFromFile(source) 116 | } 117 | } 118 | 119 | func TryKeys(server string, username string, keysSource string) (bool, error) { 120 | signers, err := LoadSigners(keysSource) 121 | if err != nil { 122 | return false, fmt.Errorf("Failed to load public keys: %w", err) 123 | } 124 | 125 | config := &ssh.ClientConfig{ 126 | User: username, 127 | Auth: []ssh.AuthMethod{MakeAuthMethod(signers)}, 128 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 129 | } 130 | 131 | client, dialErr := ssh.Dial("tcp", server, config) 132 | if dialErr == nil { 133 | client.Close() 134 | } 135 | numTried := 0 136 | numAccepted := 0 137 | for _, signer := range signers { 138 | if signer.Tried { 139 | numTried++ 140 | } 141 | if signer.Accepted { 142 | numAccepted++ 143 | } 144 | } 145 | if numAccepted > 0 { 146 | return true, nil 147 | } else if numTried == len(signers) { 148 | return false, nil 149 | } else { 150 | return false, fmt.Errorf("Error dialing server: %w", dialErr) 151 | } 152 | } 153 | 154 | func main() { 155 | if len(os.Args) < 4 { 156 | fmt.Fprintln(os.Stderr, "Usage: whoarethey HOST:PORT USERNAME KEYSFILE|github:USERNAME...") 157 | os.Exit(2) 158 | } 159 | server, username, keySources := os.Args[1], os.Args[2], os.Args[3:] 160 | 161 | type result struct { 162 | keySource string 163 | accepted bool 164 | err error 165 | } 166 | results := make(chan result) 167 | for _, keySource := range keySources { 168 | go func(keySource string) { 169 | accepted, err := TryKeys(server, username, keySource) 170 | results <- result{ 171 | keySource: keySource, 172 | accepted: accepted, 173 | err: err, 174 | } 175 | }(keySource) 176 | } 177 | 178 | anyAccepted := false 179 | anyErrors := false 180 | for range keySources { 181 | result := <-results 182 | if result.accepted { 183 | fmt.Fprintln(os.Stdout, result.keySource) 184 | anyAccepted = true 185 | } else if result.err != nil { 186 | fmt.Fprintf(os.Stderr, "%s: %s\n", result.keySource, result.err) 187 | anyErrors = true 188 | } 189 | } 190 | 191 | if anyAccepted { 192 | os.Exit(0) 193 | } else if anyErrors { 194 | os.Exit(4) 195 | } else { 196 | fmt.Fprintln(os.Stderr, "Server accepted none of the provided keys.") 197 | os.Exit(1) 198 | } 199 | } 200 | --------------------------------------------------------------------------------