diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index 13b24a392..263fe3071 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -54,6 +54,25 @@ def initialize(key, comment: nil) @comment = comment end + def matches_principal?(server_key, hostname) + server_key.valid_principals.empty? || server_key.valid_principals.include?(hostname) + end + + # TODO: this should be unit tested + def matches_validity?(server_key) + # If valid_after is in the future, fail + if server_key.valid_after && server_key.valid_after > Time.now + return false + end + + # if valid_before is in the past, fail + if server_key.valid_before && server_key.valid_before < Time.now + return false + end + + true + end + def matches_key?(server_key) if ssh_types.include?(server_key.ssh_type) server_key.signature_valid? && (server_key.signature_key.to_blob == @key.to_blob) @@ -89,6 +108,15 @@ def each(&block) def empty? @host_keys.empty? end + + def hostname + # host can be any of these, we want the first variant + # one.hosts.netssh + # one.hosts.netssh,127.0.0.1 + # [one.hosts.netssh]:2200 + # [one.hosts.netssh]:2200,[127.0.0.1]:2200 + @host.split(",").first.gsub(/\[|\]:\d+/, "") + end end # Searches an OpenSSH-style known-host file for a given host, and returns all diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb index 0c86589ae..8417ecc97 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -19,10 +19,10 @@ def verify(arguments) # We've never seen this host before, so raise an exception. process_cache_miss(host_keys, arguments, HostKeyUnknown, "is unknown") if host_keys.empty? + # If we found any matches, check to see that the key type and # blob also match. - - found = host_keys.any? do |key| + found_keys = host_keys.find do |key| if key.respond_to?(:matches_key?) key.matches_key?(arguments[:key]) else @@ -32,9 +32,24 @@ def verify(arguments) # If a match was found, return true. Otherwise, raise an exception # indicating that the key was not recognized. - process_cache_miss(host_keys, arguments, HostKeyMismatch, "does not match") unless found + process_cache_miss(host_keys, arguments, HostKeyMismatch, "does not match") unless found_keys + + if found_keys.respond_to?(:matches_validity?) + unless found_keys.matches_validity?(arguments[:key]) + # TODO why not valid? + process_cache_miss(host_keys, arguments, HostKeyUnknown, "Certificate not valid") + end + end + + # If found host_key has principal support (CertAuthority), it must match + if found_keys.respond_to?(:matches_principal?) + return true if found_keys.matches_principal?(arguments[:key], host_keys.hostname) + + process_cache_miss(host_keys, arguments, HostKeyUnknown, "Certificate invalid: name is not a listed principal") + end - found + # If we passed all checks, it's verified + true end def verify_signature(&block) diff --git a/test/integration/playbook.yml b/test/integration/playbook.yml index 69a2f61b0..820c69fd1 100644 --- a/test/integration/playbook.yml +++ b/test/integration/playbook.yml @@ -143,6 +143,9 @@ - name: add host aliases2 lineinfile: dest='/etc/hosts' owner='root' group='root' mode=0644 regexp='^127\.0\.0\.1\s+one.hosts.netssh' line='127.0.0.1 one.hosts.netssh' + - name: add host aliases3 + lineinfile: dest='/etc/hosts' owner='root' group='root' mode=0644 + regexp='^127\.0\.0\.1\s+anotherone.hosts.netssh' line='127.0.0.1 anotherone.hosts.netssh' - name: Update APT Cache apt: update_cache: yes diff --git a/test/integration/test_cert_host_auth.rb b/test/integration/test_cert_host_auth.rb index fee5b2b1d..7d47d3e42 100644 --- a/test/integration/test_cert_host_auth.rb +++ b/test/integration/test_cert_host_auth.rb @@ -11,7 +11,7 @@ class TestCertHostAuth < NetSSHTest include IntegrationTestHelpers - def setup_ssh_env(&block) + def setup_ssh_env(principals: "one.hosts.netssh", validity: "+30d", &block) tmpdir do |dir| cert_type = "rsa" # cert_type = "ssh-ed25519" @@ -24,7 +24,8 @@ def setup_ssh_env(&block) sh "ssh-keygen -t #{cert_type} -N '' -C 'ca@hosts.netssh' -f #{@cert} #{debug ? '' : '-q'}" FileUtils.cp "/etc/ssh/ssh_host_#{host_key_type}_key.pub", "#{dir}/one.hosts.netssh.pub" Dir.chdir(dir) do - sh "ssh-keygen -s #{@cert} -h -I one.hosts.netssh -n one.hosts.netssh #{debug ? '' : '-q'} #{dir}/one.hosts.netssh.pub" + principals_arg = principals.to_s.empty? ? "" : "-n #{principals}" + sh "ssh-keygen -s #{@cert} -h -I one.hosts.netssh -V #{validity} #{principals_arg} #{debug ? '' : '-q'} #{dir}/one.hosts.netssh.pub" sh "ssh-keygen -L -f one.hosts.netssh-cert.pub" if debug end signed_host_key = "/etc/ssh/ssh_host_#{host_key_type}_key-cert.pub" @@ -91,4 +92,106 @@ def test_with_other_pub_key_host_key_should_not_match end end end + + def test_with_expired_certificate_should_fail + Tempfile.open('cert_kh') do |f| + setup_ssh_env(validity: "-30d:-1d") do |params| + data = File.read(params[:cert_pub]) + f.write("@cert-authority [*.hosts.netssh]:2200 #{data}") + f.close + + config_lines = ["HostCertificate #{params[:signed_host_key]}"] + start_sshd_7_or_later(config: config_lines) do |_pid, port| + Timeout.timeout(500) do + # sleep 0.2 + # sh "ssh -v -i ~/.ssh/id_ed25519 one.hosts.netssh -o UserKnownHostsFile=#{f.path} -p 2200" + assert_raises(Net::SSH::HostKeyMismatch) do + Net::SSH.start("one.hosts.netssh", "net_ssh_1", password: 'foopwd', port: port, verify_host_key: :always, user_known_hosts_file: [f.path]) do |ssh| + ssh.exec! "echo 'foo'" + end + end + rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH + sleep 0.25 + retry + end + end + end + end + end + + def test_host_should_match_when_host_key_was_signed_by_key_and_matching_principal + Tempfile.open('cert_kh') do |f| + setup_ssh_env do |params| + data = File.read(params[:cert_pub]) + f.write("@cert-authority [*.hosts.netssh]:2200 #{data}") + f.close + + config_lines = ["HostCertificate #{params[:signed_host_key]}"] + start_sshd_7_or_later(config: config_lines) do |_pid, port| + Timeout.timeout(500) do + # sleep 0.2 + # sh "ssh -v -i ~/.ssh/id_ed25519 one.hosts.netssh -o UserKnownHostsFile=#{f.path} -p 2200" + ret = Net::SSH.start("one.hosts.netssh", "net_ssh_1", password: 'foopwd', port: port, verify_host_key: :always, user_known_hosts_file: [f.path]) do |ssh| + ssh.exec! "echo 'foo'" + end + assert_equal "foo\n", ret + rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH + sleep 0.25 + retry + end + end + end + end + end + + def test_host_should_match_when_host_key_was_signed_by_key_and_no_principal_in_certificate + Tempfile.open('cert_kh') do |f| + setup_ssh_env(principals: "") do |params| + data = File.read(params[:cert_pub]) + f.write("@cert-authority [*.hosts.netssh]:2200 #{data}") + f.close + + config_lines = ["HostCertificate #{params[:signed_host_key]}"] + start_sshd_7_or_later(config: config_lines) do |_pid, port| + Timeout.timeout(500) do + # sleep 0.2 + # sh "ssh -v -i ~/.ssh/id_ed25519 one.hosts.netssh -o UserKnownHostsFile=#{f.path} -p 2200" + ret = Net::SSH.start("one.hosts.netssh", "net_ssh_1", password: 'foopwd', port: port, verify_host_key: :always, user_known_hosts_file: [f.path]) do |ssh| + ssh.exec! "echo 'foo'" + end + assert_equal "foo\n", ret + rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH + sleep 0.25 + retry + end + end + end + end + end + + def test_host_should_not_match_when_host_key_was_signed_by_key_not_not_matching_principal + Tempfile.open('cert_kh') do |f| + setup_ssh_env do |params| + data = File.read(params[:cert_pub]) + f.write("@cert-authority [*.hosts.netssh]:2200 #{data}") + f.close + + config_lines = ["HostCertificate #{params[:signed_host_key]}"] + start_sshd_7_or_later(config: config_lines) do |_pid, port| + Timeout.timeout(500) do + sleep 0.2 + # sh "ssh -v -i ~/.ssh/id_ed25519 anotherone.hosts.netssh -o UserKnownHostsFile=#{f.path} -p 2200" + assert_raises(Net::SSH::HostKeyUnknown) do + Net::SSH.start("anotherone.hosts.netssh", "net_ssh_1", password: 'foopwd', port: port, verify_host_key: :always, user_known_hosts_file: [f.path]) do |ssh| + ssh.exec! "echo 'foo'" + end + end + rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH + sleep 0.25 + retry + end + end + end + end + end end