diff --git a/README.md b/README.md index ea536580..7c5f1ae4 100644 --- a/README.md +++ b/README.md @@ -244,9 +244,8 @@ $ chisel client --help Options: - --fingerprint, A *strongly recommended* fingerprint string - to perform host-key validation against the server's public key. - You may provide just a prefix of the key or the entire string. + --fingerprint, A *strongly recommended* fingerprint (base64 encoded SHA256) + string to perform host-key validation against the server's public key. Fingerprint mismatches will close the connection. --auth, An optional username and password (client authentication) @@ -319,7 +318,7 @@ $ chisel client --help ### Security -Encryption is always enabled. When you start up a chisel server, it will generate an in-memory ECDSA public/private key pair. The public key fingerprint will be displayed as the server starts. Instead of generating a random key, the server may optionally specify a key seed, using the `--key` option, which will be used to seed the key generation. When clients connect, they will also display the server's public key fingerprint. The client can force a particular fingerprint using the `--fingerprint` option. See the `--help` above for more information. +Encryption is always enabled. When you start up a chisel server, it will generate an in-memory ECDSA public/private key pair. The public key fingerprint (base64 encoded SHA256) will be displayed as the server starts. Instead of generating a random key, the server may optionally specify a key seed, using the `--key` option, which will be used to seed the key generation. When clients connect, they will also display the server's public key fingerprint. The client can force a particular fingerprint using the `--fingerprint` option. See the `--help` above for more information. ### Authentication @@ -341,7 +340,7 @@ docker run \ 2. Connect your chisel client (using server's fingerprint) ```sh -chisel client --fingerprint ab:12:34 :9312 socks +chisel client --fingerprint 'rHb55mcxf6vSckL2AezFV09rLs7pfPpavVu++MF7AhQ=' :9312 socks ``` 3. Point your SOCKS5 clients (e.g. OS/Browser) to: diff --git a/client/client.go b/client/client.go index 058b3e2b..c88be69a 100644 --- a/client/client.go +++ b/client/client.go @@ -2,8 +2,10 @@ package chclient import ( "context" + "crypto/md5" "crypto/tls" "crypto/x509" + "encoding/base64" "errors" "fmt" "io/ioutil" @@ -194,8 +196,18 @@ func (c *Client) Run() error { func (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKey) error { expect := c.config.Fingerprint + if expect == "" { + return nil + } got := ccrypto.FingerprintKey(key) - if expect != "" && !strings.HasPrefix(got, expect) { + _, err := base64.StdEncoding.DecodeString(expect) + if _, ok := err.(base64.CorruptInputError); ok { + c.Logger.Infof("Specified deprecated MD5 fingerprint (%s), please update to the new SHA256 fingerprint: %s", expect, got) + return c.verifyLegacyFingerprint(key) + } else if err != nil { + return fmt.Errorf("Error decoding fingerprint: %w", err) + } + if got != expect { return fmt.Errorf("Invalid fingerprint (%s)", got) } //overwrite with complete fingerprint @@ -203,6 +215,21 @@ func (c *Client) verifyServer(hostname string, remote net.Addr, key ssh.PublicKe return nil } +//verifyLegacyFingerprint calculates and compares legacy MD5 fingerprints +func (c *Client) verifyLegacyFingerprint(key ssh.PublicKey) error { + bytes := md5.Sum(key.Marshal()) + strbytes := make([]string, len(bytes)) + for i, b := range bytes { + strbytes[i] = fmt.Sprintf("%02x", b) + } + got := strings.Join(strbytes, ":") + expect := c.config.Fingerprint + if !strings.HasPrefix(got, expect) { + return fmt.Errorf("Invalid fingerprint (%s)", got) + } + return nil +} + //Start client and does not block func (c *Client) Start(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) diff --git a/client/client_test.go b/client/client_test.go index 390f7725..a2a45e48 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,12 +1,17 @@ package chclient import ( + "crypto/ecdsa" + "crypto/elliptic" "log" "net/http" "net/http/httptest" "sync" "testing" "time" + + "github.com/jpillora/chisel/share/ccrypto" + "golang.org/x/crypto/ssh" ) func TestCustomHeaders(t *testing.T) { @@ -39,3 +44,72 @@ func TestCustomHeaders(t *testing.T) { wg.Wait() c.Close() } + +func TestFallbackLegacyFingerprint(t *testing.T) { + config := Config{ + Fingerprint: "a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44", + } + c, err := NewClient(&config) + if err != nil { + t.Fatal(err) + } + r := ccrypto.NewDetermRand([]byte("test123")) + priv, err := ecdsa.GenerateKey(elliptic.P256(), r) + if err != nil { + t.Fatal(err) + } + pub, err := ssh.NewPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + err = c.verifyServer("", nil, pub) + if err != nil { + t.Fatal(err) + } +} + +func TestVerifyLegacyFingerprint(t *testing.T) { + config := Config{ + Fingerprint: "a5:32:92:c6:56:7a:9e:61:26:74:1b:81:a6:f5:1b:44", + } + c, err := NewClient(&config) + if err != nil { + t.Fatal(err) + } + r := ccrypto.NewDetermRand([]byte("test123")) + priv, err := ecdsa.GenerateKey(elliptic.P256(), r) + if err != nil { + t.Fatal(err) + } + pub, err := ssh.NewPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + err = c.verifyLegacyFingerprint(pub) + if err != nil { + t.Fatal(err) + } +} + +func TestVerifyFingerprint(t *testing.T) { + config := Config{ + Fingerprint: "qmrRoo8MIqePv3jC8+wv49gU6uaFgD3FASQx9V8KdmY=", + } + c, err := NewClient(&config) + if err != nil { + t.Fatal(err) + } + r := ccrypto.NewDetermRand([]byte("test123")) + priv, err := ecdsa.GenerateKey(elliptic.P256(), r) + if err != nil { + t.Fatal(err) + } + pub, err := ssh.NewPublicKey(&priv.PublicKey) + if err != nil { + t.Fatal(err) + } + err = c.verifyServer("", nil, pub) + if err != nil { + t.Fatal(err) + } +} diff --git a/main.go b/main.go index 5fc27383..a2685b6a 100644 --- a/main.go +++ b/main.go @@ -328,9 +328,8 @@ var clientHelp = ` Options: - --fingerprint, A *strongly recommended* fingerprint string + --fingerprint, A *strongly recommended* fingerprint (base64 encoded SHA256) string to perform host-key validation against the server's public key. - You may provide just a prefix of the key or the entire string. Fingerprint mismatches will close the connection. --auth, An optional username and password (client authentication) diff --git a/test/bench/main.go b/test/bench/main.go index 02ddc32d..091f2d5d 100644 --- a/test/bench/main.go +++ b/test/bench/main.go @@ -190,7 +190,7 @@ func main() { hf := exec.Command("chisel", "client", // "-v", - "--fingerprint", "ed:f2:cf:3c:56", + "--fingerprint", "mOz4rg9zlQ409XAhhj6+fDDVwQMY42CL3Zg2W2oTYxA=", "127.0.0.1:2002", "2001:3000") hf.Stdout = os.Stdout