trzsz / trzsz-ssh

trzsz-ssh ( tssh ) is an ssh client designed as a drop-in replacement for the openssh client. It aims to provide complete compatibility with openssh, mirroring all its features, while also offering additional useful features. Such as login prompt, batch login, remember password, automated interaction, trzsz, zmodem(rz/sz), udp mode like mosh, etc.
https://trzsz.github.io/ssh
MIT License
1.74k stars 102 forks source link

If the key not found in known_host then OpenSSH adds all the host keys to it. #129

Closed abakum closed 3 months ago

abakum commented 3 months ago

This behavior can be done in your login.go by adding after the line https://github.com/trzsz/trzsz-ssh/blob/d154d5bba805fa21d36fd0b02a4df6cd4dae374d/tssh/login.go#L234

    for _, key := range scanHostKeys(host, key.Type()) {
        warning("Permanently added '%s' (%s) to the list of known hosts.", host, key.Type())
        line += knownhosts.Line([]string{hostNormalized}, key) + "\n"
    }

scanHostKeys:

func scanHostKeys(hostPort, firstHostKeyAlgorithm string) (HostPublicKeys []ssh.PublicKey) {
    const (
        BadAlgoritm = "no such algorithm"
        TO          = time.Second * 2
    )
    KeyScanCallback := func(hostname string, remote net.Addr, key ssh.PublicKey) error {
        HostPublicKeys = append(HostPublicKeys, key)
        return fmt.Errorf(BadAlgoritm)
    }
    config := &ssh.ClientConfig{
        HostKeyCallback:   KeyScanCallback,
        HostKeyAlgorithms: []string{BadAlgoritm},
        Timeout:           TO,
    }
    // Get HostKeyAlgorithms.
    client, err := ssh.Dial("tcp", hostPort, config)
    if err != nil {
        ss := strings.Split(err.Error(), "server offered: [")
        if len(ss) < 2 {
            return
        }
        ss = strings.Split(ss[1], "]")
        if len(ss) < 2 {
            return
        }
        HostKeyAlgorithms := strings.Fields(ss[0])

        // Do not search first algorithm.
        CertAlgoRSA := false
        KeyAlgoRSA := false
        switch firstHostKeyAlgorithm {
        case ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSAv01:
            CertAlgoRSA = true
        case ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSA:
            KeyAlgoRSA = true
        }
        for _, HostKeyAlgorithm := range HostKeyAlgorithms {
            switch HostKeyAlgorithm {
            case ssh.CertAlgoRSASHA256v01, ssh.CertAlgoRSASHA512v01, ssh.CertAlgoRSAv01:
                if CertAlgoRSA {
                    continue
                }
                CertAlgoRSA = true
            case ssh.KeyAlgoRSASHA256, ssh.KeyAlgoRSASHA512, ssh.KeyAlgoRSA:
                if KeyAlgoRSA {
                    continue
                }
                KeyAlgoRSA = true
            default:
                if HostKeyAlgorithm == firstHostKeyAlgorithm {
                    continue
                }
            }
            // This is not first algoritm.
            config.HostKeyAlgorithms = []string{HostKeyAlgorithm}
            client, err := ssh.Dial("tcp", hostPort, config)
            if err != nil {
                continue
            }
            client.Close()
        }
        return
    }
    client.Close()
    return
}

Will you accept PR?

lonnywong commented 3 months ago

Do you have to dial a new connection? How to prevent man-in-the-middle attacks?

abakum commented 3 months ago

Maybe look for the first key in the list of the new dial?

lonnywong commented 3 months ago

Unless it works without any new connections.

abakum commented 3 months ago

Does OpenSSH do this without new connections?

lonnywong commented 3 months ago

Does OpenSSH do this without new connections?

Of course. Check https://github.com/golang/go/issues/37245

abakum commented 3 months ago

But look ssh-keyscan -vv ssh-j.com - it use 5 connection

abakum commented 3 months ago

Maybe give the user a choice? 1) yes

debug: login to [12], addr: 10.161.115.143:12
The authenticity of host '10.161.115.143:12' can't be established.
ecdsa-sha2-nistp256 key fingerprint is SHA256:MgIL01Bxop23VnARGMM0Ouz9ufLRnJb78vLTiUule3M
ssh-ed25519 key fingerprint is SHA256:pr+YBhrI0y822dBXTNoluQ6wULycpQEiGiyZeYU/s2o
ssh-rsa key fingerprint is SHA256:6n4F7GNw8SgLaSavF9sE/1bFCxRgvrDu7p2YYXHN4A8
Are you sure you want to continue connecting (yes/no/all/[fingerprint])? yes
Warning: Permanently added '10.161.115.143:12' (ecdsa-sha2-nistp256) to the list of known hosts.

2) all

debug: login to [12], addr: 10.161.115.143:12
The authenticity of host '10.161.115.143:12' can't be established.
ecdsa-sha2-nistp256 key fingerprint is SHA256:MgIL01Bxop23VnARGMM0Ouz9ufLRnJb78vLTiUule3M
ssh-ed25519 key fingerprint is SHA256:pr+YBhrI0y822dBXTNoluQ6wULycpQEiGiyZeYU/s2o
ssh-rsa key fingerprint is SHA256:6n4F7GNw8SgLaSavF9sE/1bFCxRgvrDu7p2YYXHN4A8
Are you sure you want to continue connecting (yes/no/all/[fingerprint])? all
Warning: Permanently added '10.161.115.143:12' (ssh-ed25519) to the list of known hosts.
Warning: Permanently added '10.161.115.143:12' (ssh-rsa) to the list of known hosts.
Warning: Permanently added '10.161.115.143:12' (ecdsa-sha2-nistp256) to the list of known hosts.

3) Select by fingerprint

debug: login to [12], addr: 10.161.115.143:12
The authenticity of host '10.161.115.143:12' can't be established.
ecdsa-sha2-nistp256 key fingerprint is SHA256:MgIL01Bxop23VnARGMM0Ouz9ufLRnJb78vLTiUule3M
ssh-rsa key fingerprint is SHA256:6n4F7GNw8SgLaSavF9sE/1bFCxRgvrDu7p2YYXHN4A8
ssh-ed25519 key fingerprint is SHA256:pr+YBhrI0y822dBXTNoluQ6wULycpQEiGiyZeYU/s2o
Are you sure you want to continue connecting (yes/no/all/[fingerprint])? SHA256:6n4F7GNw8SgLaSavF9sE/1bFCxRgvrDu7p2YYXHN4A8
Warning: Permanently added '10.161.115.143:12' (ssh-rsa) to the list of known hosts.

addHostKey https://github.com/abakum/dssh/commit/27659300ee79366bce4f1b10f9fa3663566067da

func addHostKey(path, host string, remote net.Addr, key ssh.PublicKey, ask bool) error {
    keyNormalizedLine := knownhosts.Line([]string{host}, key)
    for _, acceptKey := range acceptHostKeys {
        if acceptKey == keyNormalizedLine {
            return nil
        }
    }

    if ask {
        if sshLoginSuccess.Load() {
            fmt.Fprintf(os.Stderr, "\r\n\033[0;31mThe public key of the remote server has changed after login.\033[0m\r\n")
            return fmt.Errorf("host key changed")
        }

        fingerprint := ssh.FingerprintSHA256(key)
        fmt.Fprintf(os.Stderr, "The authenticity of host '%s' can't be established.\r\n"+
            "%s key fingerprint is %s\r\n", host, key.Type(), fingerprint)
        keys := goScanHostKeys(host, key)

        // List other keys for select by fingerprint. Without dot at the end for copyPaste.
        for _, key := range keys {
            fingerprint := ssh.FingerprintSHA256(key)
            fmt.Fprintf(os.Stderr,
                "%s key fingerprint is %s\r\n", key.Type(), fingerprint)
        }

        stdin, closer, err := getKeyboardInput()
        if err != nil {
            return err
        }
        defer closer()

        reader := bufio.NewReader(stdin)
        fmt.Fprintf(os.Stderr, "Are you sure you want to continue connecting (yes/no/all/[fingerprint])? ")

    readInput:
        for {
            input, err := reader.ReadString('\n')
            if err != nil {
                return err
            }
            input = strings.TrimSpace(input)

            for _, keyByFingerprint := range append(keys, key) {
                if input == ssh.FingerprintSHA256(keyByFingerprint) {
                    key = keyByFingerprint
                    break readInput
                }
            }
            input = strings.ToLower(input)
            if input == "yes" {
                break
            } else if input == "no" {
                return fmt.Errorf("host key not trusted")
            } else if input == "all" {
                for _, otherKey := range keys {
                    acceptHostKeys = append(acceptHostKeys, knownhosts.Line([]string{host}, otherKey))

                    if err := writeKnownHost(path, host, remote, otherKey); err != nil {
                        warning("Failed to add the host to the list of known hosts (%s): %v", path, err)
                        return nil
                    }

                    warning("Permanently added '%s' (%s) to the list of known hosts.", host, otherKey.Type())
                }
                break
            }
            fmt.Fprintf(os.Stderr, "Please type 'yes', 'no', 'all' or the fingerprint: ")
        }
    }

    acceptHostKeys = append(acceptHostKeys, keyNormalizedLine)

    if err := writeKnownHost(path, host, remote, key); err != nil {
        warning("Failed to add the host to the list of known hosts (%s): %v", path, err)
        return nil
    }

    warning("Permanently added '%s' (%s) to the list of known hosts.", host, key.Type())

    return nil
}
lonnywong commented 3 months ago

I don't think dialing another connection is a good idea. If someone really needs it all, just log in once with openssh.

lonnywong commented 3 months ago

Thanks for your advice. But the user may not understand the risks here and choose yes blindly. Even if the user understands it and will confirm it one by one, I don't want to increase the user's psychological burden because of it.