diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index 5aae7f0eb..a69d11c65 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -56,9 +56,26 @@ def initialize(key, comment: nil) end def matches_principal?(server_key, hostname) + pp [:debug, :valid_principals, server_key.valid_principals] server_key.valid_principals.empty? || server_key.valid_principals.include?(hostname) end + def matches_validity?(server_key) + # If valid_after is in the future, fail + if server_key.valid_after && server_key.valid_after > Time.now + pp [:debug, :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 + pp [:debug, :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) diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb index 904ce0921..52f409dc0 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -19,6 +19,7 @@ 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_keys = host_keys.find do |key| @@ -33,6 +34,14 @@ def verify(arguments) # indicating that the key was not recognized. 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]) + binding.break + # 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) diff --git a/test/integration/test_cert_host_auth.rb b/test/integration/test_cert_host_auth.rb index 777f82794..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" @@ -92,6 +93,32 @@ def test_with_other_pub_key_host_key_should_not_match 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| @@ -117,6 +144,31 @@ def test_host_should_match_when_host_key_was_signed_by_key_and_matching_principa 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|