From: Zdenek Zambersky <zzambers@redhat.com>
Date: Thu, 6 May 2021 13:50:20 +0200
Subject: Added support for RSA client authentication with SHA-2

Source: https://github.com/net-ssh/net-ssh/pull/838
Backported-By: Antonio Terceiro <terceiro@debian.org>
---
 lib/net/ssh/authentication/certificate.rb       |  4 +-
 lib/net/ssh/authentication/ed25519.rb           |  2 +-
 lib/net/ssh/authentication/key_manager.rb       | 22 ++++++--
 lib/net/ssh/authentication/methods/abstract.rb  | 10 ++++
 lib/net/ssh/authentication/methods/publickey.rb | 66 ++++++++++++++++++-----
 lib/net/ssh/authentication/session.rb           |  5 +-
 lib/net/ssh/transport/openssl.rb                | 16 ++++--
 test/authentication/methods/test_publickey.rb   | 71 ++++++++++++++++++++++---
 test/authentication/test_session.rb             |  6 ++-
 9 files changed, 169 insertions(+), 33 deletions(-)

diff --git a/lib/net/ssh/authentication/certificate.rb b/lib/net/ssh/authentication/certificate.rb
index 82e37e9..95b01ff 100644
--- a/lib/net/ssh/authentication/certificate.rb
+++ b/lib/net/ssh/authentication/certificate.rb
@@ -65,8 +65,8 @@ module Net
           ).to_s
         end
 
-        def ssh_do_sign(data)
-          key.ssh_do_sign(data)
+        def ssh_do_sign(data, sig_alg = nil)
+          key.ssh_do_sign(data, sig_alg)
         end
 
         def ssh_do_verify(sig, data)
diff --git a/lib/net/ssh/authentication/ed25519.rb b/lib/net/ssh/authentication/ed25519.rb
index 0c5530c..1989d1f 100644
--- a/lib/net/ssh/authentication/ed25519.rb
+++ b/lib/net/ssh/authentication/ed25519.rb
@@ -167,7 +167,7 @@ module Net
             PubKey.new(@pk)
           end
 
-          def ssh_do_sign(data)
+          def ssh_do_sign(data, sig_alg = nil)
             @sign_key.sign(data)
           end
 
diff --git a/lib/net/ssh/authentication/key_manager.rb b/lib/net/ssh/authentication/key_manager.rb
index 242d5d5..3624550 100644
--- a/lib/net/ssh/authentication/key_manager.rb
+++ b/lib/net/ssh/authentication/key_manager.rb
@@ -159,7 +159,7 @@ module Net
         # Regardless of the identity's origin or who does the signing, this
         # will always return the signature in an SSH2-specified "signature
         # blob" format.
-        def sign(identity, data)
+        def sign(identity, data, sig_alg = nil)
           info = known_identities[identity] or raise KeyManagerError, "the given identity is unknown to the key manager"
 
           if info[:key].nil? && info[:from] == :file
@@ -171,13 +171,27 @@ module Net
           end
 
           if info[:key]
-            return Net::SSH::Buffer.from(:string, identity.ssh_signature_type,
-              :mstring, info[:key].ssh_do_sign(data.to_s)).to_s
+            if sig_alg.nil?
+              signed = info[:key].ssh_do_sign(data.to_s)
+              sig_alg = identity.ssh_signature_type
+            else
+              signed = info[:key].ssh_do_sign(data.to_s, sig_alg)
+            end
+            return Net::SSH::Buffer.from(:string, sig_alg,
+                                         :mstring, signed).to_s
           end
 
           if info[:from] == :agent
             raise KeyManagerError, "the agent is no longer available" unless agent
-            return agent.sign(info[:identity], data.to_s)
+
+            case sig_alg
+            when "rsa-sha2-512"
+              return agent.sign(info[:identity], data.to_s, Net::SSH::Authentication::Agent::SSH_AGENT_RSA_SHA2_512)
+            when "rsa-sha2-256"
+              return agent.sign(info[:identity], data.to_s, Net::SSH::Authentication::Agent::SSH_AGENT_RSA_SHA2_256)
+            else
+              return agent.sign(info[:identity], data.to_s)
+            end
           end
 
           raise KeyManagerError, "[BUG] can't determine identity origin (#{info.inspect})"
diff --git a/lib/net/ssh/authentication/methods/abstract.rb b/lib/net/ssh/authentication/methods/abstract.rb
index bcddd4f..8ec4d22 100644
--- a/lib/net/ssh/authentication/methods/abstract.rb
+++ b/lib/net/ssh/authentication/methods/abstract.rb
@@ -21,12 +21,22 @@ module Net
           # this.
           attr_reader :key_manager
 
+          # So far only affects algorithms used for rsa keys, but can be
+          # extended to other keys, e.g after reading of
+          # PubkeyAcceptedAlgorithms option from ssh_config file is implemented.
+          attr_reader :pubkey_algorithms
+
           # Instantiates a new authentication method.
           def initialize(session, options={})
             @session = session
             @key_manager = options[:key_manager]
             @options = options
             @prompt = options[:password_prompt]
+            @pubkey_algorithms = options[:pubkey_algorithms] \
+              || %w[rsa-sha2-256-cert-v01@openssh.com
+                    ssh-rsa-cert-v01@openssh.com
+                    rsa-sha2-256
+                    ssh-rsa]
             self.logger = session.logger
           end
 
diff --git a/lib/net/ssh/authentication/methods/publickey.rb b/lib/net/ssh/authentication/methods/publickey.rb
index bff9ffd..7e9e422 100644
--- a/lib/net/ssh/authentication/methods/publickey.rb
+++ b/lib/net/ssh/authentication/methods/publickey.rb
@@ -27,41 +27,40 @@ module Net
 
           # Builds a packet that contains the request formatted for sending
           # a public-key request to the server.
-          def build_request(pub_key, username, next_service, has_sig)
+          def build_request(pub_key, username, next_service, alg, has_sig)
             blob = Net::SSH::Buffer.new
             blob.write_key pub_key
 
             userauth_request(username, next_service, "publickey", has_sig,
-              pub_key.ssh_type, blob.to_s)
+                             alg, blob.to_s)
           end
 
           # Builds and sends a request formatted for a public-key
           # authentication request.
-          def send_request(pub_key, username, next_service, signature=nil)
-            msg = build_request(pub_key, username, next_service, !signature.nil?)
+          def send_request(pub_key, username, next_service, alg, signature = nil)
+            msg = build_request(pub_key, username, next_service, alg,
+                                !signature.nil?)
             msg.write_string(signature) if signature
             send_message(msg)
           end
 
-          # Attempts to perform public-key authentication for the given
-          # username, with the given identity (public key). Returns +true+ if
-          # successful, or +false+ otherwise.
-          def authenticate_with(identity, next_service, username)
+          def authenticate_with_alg(identity, next_service, username, alg, sig_alg = nil)
             debug { "trying publickey (#{identity.fingerprint})" }
-            send_request(identity, username, next_service)
+            send_request(identity, username, next_service, alg)
 
             message = session.next_message
 
             case message.type
             when USERAUTH_PK_OK
-              buffer = build_request(identity, username, next_service, true)
+              buffer = build_request(identity, username, next_service, alg,
+                                     true)
               sig_data = Net::SSH::Buffer.new
               sig_data.write_string(session_id)
               sig_data.append(buffer.to_s)
 
-              sig_blob = key_manager.sign(identity, sig_data)
+              sig_blob = key_manager.sign(identity, sig_data, sig_alg)
 
-              send_request(identity, username, next_service, sig_blob.to_s)
+              send_request(identity, username, next_service, alg, sig_blob.to_s)
               message = session.next_message
 
               case message.type
@@ -89,6 +88,49 @@ module Net
               raise Net::SSH::Exception, "unexpected reply to USERAUTH_REQUEST: #{message.type} (#{message.inspect})"
             end
           end
+
+          # Attempts to perform public-key authentication for the given
+          # username, with the given identity (public key). Returns +true+ if
+          # successful, or +false+ otherwise.
+          def authenticate_with(identity, next_service, username)
+            type = identity.ssh_type
+            if type == "ssh-rsa"
+              pubkey_algorithms.each do |pk_alg|
+                case pk_alg
+                when "rsa-sha2-512", "rsa-sha2-256", "ssh-rsa"
+                  if authenticate_with_alg(identity, next_service, username, pk_alg, pk_alg)
+                    # success
+                    return true
+                  end
+                end
+              end
+            elsif type == "ssh-rsa-cert-v01@openssh.com"
+              pubkey_algorithms.each do |pk_alg|
+                case pk_alg
+                when "rsa-sha2-512-cert-v01@openssh.com"
+                  if authenticate_with_alg(identity, next_service, username, pk_alg, "rsa-sha2-512")
+                    # success
+                    return true
+                  end
+                when "rsa-sha2-256-cert-v01@openssh.com"
+                  if authenticate_with_alg(identity, next_service, username, pk_alg, "rsa-sha2-256")
+                    # success
+                    return true
+                  end
+                when "ssh-rsa-cert-v01@openssh.com"
+                  if authenticate_with_alg(identity, next_service, username, pk_alg)
+                    # success
+                    return true
+                  end
+                end
+              end
+            elsif authenticate_with_alg(identity, next_service, username, type)
+              # success
+              return true
+            end
+            # failure
+            return false
+          end
         end
 
       end
diff --git a/lib/net/ssh/authentication/session.rb b/lib/net/ssh/authentication/session.rb
index dfc5c06..a524c52 100644
--- a/lib/net/ssh/authentication/session.rb
+++ b/lib/net/ssh/authentication/session.rb
@@ -77,7 +77,10 @@ module Net
               debug { "trying #{name}" }
               begin
                 auth_class = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join)
-                method = auth_class.new(self, key_manager: key_manager, password_prompt: options[:password_prompt])
+                method = auth_class.new(self,
+                                        key_manager: key_manager, password_prompt: options[:password_prompt],
+                                        pubkey_algorithms: options[:pubkey_algorithms] || nil)
+
               rescue NameError
                 debug {"Mechanism #{name} was requested, but isn't a known type.  Ignoring it."}
                 next
diff --git a/lib/net/ssh/transport/openssl.rb b/lib/net/ssh/transport/openssl.rb
index d2d7117..3f10b02 100644
--- a/lib/net/ssh/transport/openssl.rb
+++ b/lib/net/ssh/transport/openssl.rb
@@ -68,8 +68,16 @@ module OpenSSL
       end
 
       # Returns the signature for the given data.
-      def ssh_do_sign(data)
-        sign(OpenSSL::Digest::SHA1.new, data)
+      def ssh_do_sign(data, sig_alg = nil)
+        digester =
+          if sig_alg == "rsa-sha2-512"
+            OpenSSL::Digest::SHA512.new
+          elsif sig_alg == "rsa-sha2-256"
+            OpenSSL::Digest::SHA256.new
+          else
+            OpenSSL::Digest::SHA1.new
+          end
+        sign(digester, data)
       end
     end
 
@@ -105,7 +113,7 @@ module OpenSSL
       end
 
       # Signs the given data.
-      def ssh_do_sign(data)
+      def ssh_do_sign(data, sig_alg = nil)
         sig = sign(OpenSSL::Digest::SHA1.new, data)
         a1sig = OpenSSL::ASN1.decode(sig)
 
@@ -218,7 +226,7 @@ module OpenSSL
       end
 
       # Returns the signature for the given data.
-      def ssh_do_sign(data)
+      def ssh_do_sign(data, sig_alg = nil)
         digest = digester.digest(data)
         sig = dsa_sign_asn1(digest)
         a1sig = OpenSSL::ASN1.decode(sig)
diff --git a/test/authentication/methods/test_publickey.rb b/test/authentication/methods/test_publickey.rb
index 8f2cc73..05510f2 100644
--- a/test/authentication/methods/test_publickey.rb
+++ b/test/authentication/methods/test_publickey.rb
@@ -105,6 +105,50 @@ module Authentication
         assert subject.authenticate("ssh-connection", "jamis")
       end
 
+      def test_authenticate_rsa_sha2
+        key_manager.expects(:sign).with(&signature_parameters_with_alg(keys.first, "rsa-sha2-256")).returns("sig-one")
+
+        transport.expect do |t, packet|
+          assert_equal USERAUTH_REQUEST, packet.type
+          assert verify_userauth_request_packet(packet, keys.first, false, "rsa-sha2-256")
+          t.return(USERAUTH_PK_OK, :string, "rsa-sha2-256", :string, Net::SSH::Buffer.from(:key, keys.first))
+
+          t.expect do |t2, packet2|
+            assert_equal USERAUTH_REQUEST, packet2.type
+            assert verify_userauth_request_packet(packet2, keys.first, true, "rsa-sha2-256")
+            assert_equal "sig-one", packet2.read_string
+            t2.return(USERAUTH_SUCCESS)
+          end
+        end
+
+        assert subject(pubkey_algorithms: %w[rsa-sha2-256]).authenticate("ssh-connection", "jamis")
+      end
+
+      def test_authenticate_rsa_sha2_fallback
+        key_manager.expects(:sign).with(&signature_parameters(keys.first)).returns("sig-one")
+
+        transport.expect do |t, packet|
+          assert_equal USERAUTH_REQUEST, packet.type
+          assert verify_userauth_request_packet(packet, keys.first, false, "rsa-sha2-256")
+          t.return(USERAUTH_FAILURE, :string, "publickey")
+
+          t.expect do |t2, packet2|
+            assert_equal USERAUTH_REQUEST, packet2.type
+            assert verify_userauth_request_packet(packet2, keys.first, false)
+            t2.return(USERAUTH_PK_OK, :string, keys.first.ssh_type, :string, Net::SSH::Buffer.from(:key, keys.first))
+
+            t2.expect do |t3, packet3|
+              assert_equal USERAUTH_REQUEST, packet3.type
+              assert verify_userauth_request_packet(packet3, keys.first, true)
+              assert_equal "sig-one", packet3.read_string
+              t3.return(USERAUTH_SUCCESS)
+            end
+          end
+        end
+
+        assert subject(pubkey_algorithms: %w[rsa-sha2-256 ssh-rsa]).authenticate("ssh-connection", "jamis")
+      end
+
       private
 
       def signature_parameters(key)
@@ -117,13 +161,25 @@ module Authentication
         end
       end
 
-      def verify_userauth_request_packet(packet, key, has_sig)
-        packet.read_string == "jamis"          && # user-name
-        packet.read_string == "ssh-connection" && # next service
-        packet.read_string == "publickey"      && # auth-method
-        packet.read_bool   == has_sig          && # whether a signature is appended
-        packet.read_string == key.ssh_type     && # ssh key type
-        packet.read_buffer.read_key.to_blob == key.to_blob # key
+      def signature_parameters_with_alg(key, alg)
+        Proc.new do |given_key, data, given_alg|
+          next false unless given_key.to_blob == key.to_blob
+          next false unless given_alg == alg
+
+          buffer = Net::SSH::Buffer.new(data)
+          buffer.read_string == "abcxyz123"      && # session-id
+            buffer.read_byte == USERAUTH_REQUEST && # type
+            verify_userauth_request_packet(buffer, key, true, alg)
+        end
+      end
+
+      def verify_userauth_request_packet(packet, key, has_sig, alg = nil)
+        packet.read_string == "jamis" && # user-name
+          packet.read_string == "ssh-connection" && # next service
+          packet.read_string == "publickey"      && # auth-method
+          packet.read_bool   == has_sig          && # whether a signature is appended
+          packet.read_string == (alg || key.ssh_type) && # ssh key type
+          packet.read_buffer.read_key.to_blob == key.to_blob # key
       end
 
       @@keys = nil
@@ -141,6 +197,7 @@ module Authentication
 
       def subject(options={})
         options[:key_manager] = key_manager(options) unless options.key?(:key_manager)
+        options[:pubkey_algorithms] = %w[ssh-rsa] unless options.key?(:pubkey_algorithms)
         @subject ||= Net::SSH::Authentication::Methods::Publickey.new(session(options), options)
       end
     end
diff --git a/test/authentication/test_session.rb b/test/authentication/test_session.rb
index db37467..19b9f8a 100644
--- a/test/authentication/test_session.rb
+++ b/test/authentication/test_session.rb
@@ -168,8 +168,10 @@ module Authentication
 
     private
 
-    def session(options={})
-      @session ||= Net::SSH::Authentication::Session.new(transport(options), options)
+    def session(options = {})
+      session_opts = options.clone
+      session_opts[:pubkey_algorithms] = %w[ssh-rsa]
+      @session ||= Net::SSH::Authentication::Session.new(transport(options), session_opts)
     end
 
     def transport(options={})
