@@ -6,7 +6,6 @@ package ssh
66import (
77 "bytes"
88 "context"
9- "errors"
109 "io"
1110 "net"
1211 "os"
@@ -16,6 +15,7 @@ import (
1615 "time"
1716
1817 awsconf "github.com/aws/aws-sdk-go-v2/config"
18+ "github.com/cockroachdb/errors"
1919 "github.com/kevinburke/ssh_config"
2020 "github.com/mitchellh/go-homedir"
2121 rawsftp "github.com/pkg/sftp"
@@ -33,7 +33,6 @@ import (
3333 "go.mondoo.com/cnquery/v11/providers/os/connection/ssh/signers"
3434 "go.mondoo.com/cnquery/v11/utils/multierr"
3535 "golang.org/x/crypto/ssh"
36- "golang.org/x/crypto/ssh/agent"
3736 "golang.org/x/crypto/ssh/knownhosts"
3837)
3938
@@ -493,27 +492,59 @@ func establishClientConnection(pCfg *inventory.Config, hostKeyCallback ssh.HostK
493492 }
494493 }
495494
496- log . Debug (). Int ( "methods" , len ( authMethods )). Str ( "user" , user ). Msg ( "connect to remote ssh" )
497- conn , err := ssh . Dial ( "tcp" , pCfg . Host + ":" + strconv . Itoa ( int ( pCfg . Port )), & ssh.ClientConfig {
495+ addr := pCfg . Host + ":" + strconv . Itoa ( int ( pCfg . Port ) )
496+ sshClientConfig := & ssh.ClientConfig {
498497 User : user ,
499498 Auth : authMethods ,
500499 HostKeyCallback : hostKeyCallback ,
501- })
500+ }
501+
502+ supportsHybrid , err := serverSupportsHybridKEX (addr )
503+ if supportsHybrid {
504+ // force the Key Exchange Algorithm to a compatible one
505+ sshClientConfig .Config = ssh.Config {
506+ KeyExchanges : []string {
507+ "curve25519-sha256" ,
508+ "curve25519-sha256@libssh.org" ,
509+ "ecdh-sha2-nistp256" ,
510+ "diffie-hellman-group14-sha1" ,
511+ },
512+ }
513+ }
514+
515+ log .Debug ().
516+ Int ("methods" , len (authMethods )).
517+ Str ("user" , user ).
518+ Bool ("hybrid_key_exchange" , supportsHybrid ).
519+ Msg ("connect to remote ssh" )
520+ conn , err := ssh .Dial ("tcp" , addr , sshClientConfig )
502521 return conn , closer , err
503522}
504523
505- // hasAgentLoadedKey returns if the ssh agent has loaded the key file
506- // This may not be 100% accurate. The key can be stored in multiple locations with the
507- // same fingerprint. We cannot determine the fingerprint without decoding the encrypted
508- // key, `ssh-keygen -lf /Users/chartmann/.ssh/id_rsa` seems to use the ssh agent to
509- // determine the fingerprint without prompting for the password
510- func hasAgentLoadedKey (list []* agent.Key , filename string ) bool {
511- for i := range list {
512- if list [i ].Comment == filename {
513- return true
514- }
524+ // Detects if the remote server offers hybrid PQ KEX algorithms
525+ func serverSupportsHybridKEX (addr string ) (bool , error ) {
526+ conn , err := net .DialTimeout ("tcp" , addr , 5 * time .Second )
527+ if err != nil {
528+ return false , errors .Wrap (err , "failed to dial" )
515529 }
516- return false
530+ defer conn .Close ()
531+
532+ // Read the server's version string
533+ var buf [256 ]byte
534+ n , err := conn .Read (buf [:])
535+ if err != nil {
536+ return false , errors .Wrap (err , "failed to read banner" )
537+ }
538+ banner := string (buf [:n ])
539+
540+ // We'll stop here. Full KEXINIT parsing requires building a custom packet reader.
541+ // For now, assume OpenSSH 9.9+ includes PQ KEX unless we detect otherwise.
542+ if strings .Contains (banner , "OpenSSH_9.9" ) {
543+ // Naively assume 9.9+ offers hybrid KEX
544+ return true , nil
545+ }
546+
547+ return false , nil
517548}
518549
519550// prepareConnection determines the auth methods required for a ssh connection and also prepares any other
0 commit comments