From a45f54fe1de434605af0b7195dd9a91bccd2cec5 Mon Sep 17 00:00:00 2001 From: Zdenek Zambersky Date: Thu, 6 May 2021 13:50:20 +0200 Subject: Added support for RSA client authentication with SHA-2 --- lib/net/ssh/authentication/certificate.rb | 4 +- lib/net/ssh/authentication/ed25519.rb | 2 +- lib/net/ssh/authentication/key_manager.rb | 21 ++++++-- lib/net/ssh/authentication/methods/abstract.rb | 10 ++++ lib/net/ssh/authentication/methods/publickey.rb | 66 ++++++++++++++++++++----- lib/net/ssh/authentication/session.rb | 4 +- lib/net/ssh/transport/openssl.rb | 16 ++++-- test/authentication/methods/test_publickey.rb | 61 ++++++++++++++++++++++- test/authentication/test_session.rb | 4 +- 9 files changed, 161 insertions(+), 27 deletions(-) diff --git a/lib/net/ssh/authentication/certificate.rb b/lib/net/ssh/authentication/certificate.rb index c9619fa..5250789 100644 --- a/lib/net/ssh/authentication/certificate.rb +++ b/lib/net/ssh/authentication/certificate.rb @@ -66,8 +66,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, options = {}) diff --git a/lib/net/ssh/authentication/ed25519.rb b/lib/net/ssh/authentication/ed25519.rb index c2a117a..dccc64f 100644 --- a/lib/net/ssh/authentication/ed25519.rb +++ b/lib/net/ssh/authentication/ed25519.rb @@ -171,7 +171,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 ac86cc8..563702c 100644 --- a/lib/net/ssh/authentication/key_manager.rb +++ b/lib/net/ssh/authentication/key_manager.rb @@ -158,7 +158,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 @@ -170,14 +170,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 07aa5f3..f023011 100644 --- a/lib/net/ssh/authentication/methods/abstract.rb +++ b/lib/net/ssh/authentication/methods/abstract.rb @@ -20,12 +20,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 eb4d740..48a56ab 100644 --- a/lib/net/ssh/authentication/methods/publickey.rb +++ b/lib/net/ssh/authentication/methods/publickey.rb @@ -26,41 +26,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 @@ -88,6 +87,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 end diff --git a/lib/net/ssh/authentication/session.rb b/lib/net/ssh/authentication/session.rb index e4fc648..773d704 100644 --- a/lib/net/ssh/authentication/session.rb +++ b/lib/net/ssh/authentication/session.rb @@ -76,7 +76,9 @@ 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 4ff58cf..1c23651 100644 --- a/lib/net/ssh/transport/openssl.rb +++ b/lib/net/ssh/transport/openssl.rb @@ -74,8 +74,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 @@ -109,7 +117,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) @@ -220,7 +228,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 db5e62f..5e76ca0 100644 --- a/test/authentication/methods/test_publickey.rb +++ b/test/authentication/methods/test_publickey.rb @@ -104,6 +104,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,12 +161,24 @@ module Authentication end end - def verify_userauth_request_packet(packet, key, has_sig) + 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 == key.ssh_type && # ssh key type + packet.read_string == (alg || key.ssh_type) && # ssh key type packet.read_buffer.read_key.to_blob == key.to_blob # key end @@ -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 0f73a97..4f1d482 100644 --- a/test/authentication/test_session.rb +++ b/test/authentication/test_session.rb @@ -168,7 +168,9 @@ module Authentication private def session(options = {}) - @session ||= Net::SSH::Authentication::Session.new(transport(options), 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 = {}) -- cgit v1.2.1