From 178612021e901f1d84f5f33213f7e23760cabe42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Tue, 30 May 2023 16:46:00 +0200 Subject: [PATCH 01/13] Initial CertAuthority prinical verification code --- lib/net/ssh/known_hosts.rb | 4 ++++ lib/net/ssh/verifiers/always.rb | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index 95b024da4..dc95e156c 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -55,6 +55,10 @@ def initialize(key, comment: nil) @comment = comment end + def matches_principal?(server_key, host) + server_key.valid_principals.empty? || server_key.valid_principals.include?(host) + 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 0c86589ae..fed3aa335 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -24,7 +24,20 @@ def verify(arguments) found = host_keys.any? do |key| if key.respond_to?(:matches_key?) - key.matches_key?(arguments[:key]) + unless key.matches_key?(arguments[:key]) + next false + end + end + + if key.respond_to?(:matches_principal?) + hostname_to_verify = host_keys.host.split(",").first + principal_match = key.matches_principal?(arguments[:key], hostname_to_verify) + + if principal_match + true + else + process_cache_miss(host_keys, arguments, HostKeyUnknown, "name is not a listed principal") + end else key.ssh_type == arguments[:key].ssh_type && key.to_blob == arguments[:key].to_blob end From e84fb2e03ee85be7a9b31100d0506bc364b8f989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Thu, 1 Jun 2023 09:29:34 +0200 Subject: [PATCH 02/13] Further work on host principal verification --- lib/net/ssh/known_hosts.rb | 7 +++- lib/net/ssh/verifiers/always.rb | 31 ++++++++------- test/integration/playbook.yml | 3 ++ test/integration/test_cert_host_auth.rb | 51 +++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index dc95e156c..113ab54b4 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -120,7 +120,12 @@ def search_for(host, options = {}) # Search for all known keys for the given host, in every file given in # the +files+ array. Returns the list of keys. def search_in(files, host, options = {}) - files.flat_map { |file| KnownHosts.new(file).keys_for(host, options) } + # one.hosts.netssh + # one.hosts.netssh,127.0.0.1 + # [one.hosts.netssh]:2200 + # [one.hosts.netssh]:2200,[127.0.0.1]:2200 + host_to_search_for = host.split(",").first.gsub(/\[|\]:\d+/, "") + files.flat_map { |file| KnownHosts.new(file).keys_for(host_to_search_for, options) } end # Looks in the given +options+ hash for the :user_known_hosts_file and diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb index fed3aa335..16793f0fc 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -21,25 +21,24 @@ def verify(arguments) # If we found any matches, check to see that the key type and # blob also match. - - found = host_keys.any? do |key| + found = host_keys.find do |key| if key.respond_to?(:matches_key?) - unless key.matches_key?(arguments[:key]) - next false - end + key.matches_key?(arguments[:key]) + else + key.ssh_type == arguments[:key].ssh_type && key.to_blob == arguments[:key].to_blob end + end - if key.respond_to?(:matches_principal?) - hostname_to_verify = host_keys.host.split(",").first - principal_match = key.matches_principal?(arguments[:key], hostname_to_verify) + if found && found.respond_to?(:matches_principal?) + # one.hosts.netssh + # one.hosts.netssh,127.0.0.1 + # [one.hosts.netssh]:2200 + # [one.hosts.netssh]:2200,[127.0.0.1]:2200 + hostname_to_verify = host_keys.host.split(",").first.gsub(/\[|\]:\d+/, "") + principal_match = found.matches_principal?(arguments[:key], hostname_to_verify) - if principal_match - true - else - process_cache_miss(host_keys, arguments, HostKeyUnknown, "name is not a listed principal") - end - else - key.ssh_type == arguments[:key].ssh_type && key.to_blob == arguments[:key].to_blob + unless principal_match + process_cache_miss(host_keys, arguments, HostKeyUnknown, "name is not a listed principal") end end @@ -47,7 +46,7 @@ def verify(arguments) # indicating that the key was not recognized. process_cache_miss(host_keys, arguments, HostKeyMismatch, "does not match") unless found - found + true end def verify_signature(&block) diff --git a/test/integration/playbook.yml b/test/integration/playbook.yml index 02d358aa7..f1c6142f8 100644 --- a/test/integration/playbook.yml +++ b/test/integration/playbook.yml @@ -141,6 +141,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..777f82794 100644 --- a/test/integration/test_cert_host_auth.rb +++ b/test/integration/test_cert_host_auth.rb @@ -91,4 +91,55 @@ def test_with_other_pub_key_host_key_should_not_match 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_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 From 027f6ae5776c3e04771ae6e201126448d1723c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Fri, 2 Jun 2023 10:12:28 +0200 Subject: [PATCH 03/13] Improve error message --- lib/net/ssh/verifiers/always.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb index 16793f0fc..8c607f0ea 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -38,7 +38,7 @@ def verify(arguments) principal_match = found.matches_principal?(arguments[:key], hostname_to_verify) unless principal_match - process_cache_miss(host_keys, arguments, HostKeyUnknown, "name is not a listed principal") + process_cache_miss(host_keys, arguments, HostKeyUnknown, "Certificate invalid: name is not a listed principal") end end From a71b280fec41504eb21dafd44dd764a8b10b5e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Thu, 15 Jun 2023 13:32:03 +0200 Subject: [PATCH 04/13] Must keep all hostnames/ips to not break "check_host_ip" option Make sure check_host_ip option works as expected The host scrubbing issue was specific to the new matches_principal --- lib/net/ssh/known_hosts.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index 113ab54b4..ff884f3ce 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -56,7 +56,12 @@ def initialize(key, comment: nil) end def matches_principal?(server_key, host) - server_key.valid_principals.empty? || server_key.valid_principals.include?(host) + # one.hosts.netssh + # one.hosts.netssh,127.0.0.1 + # [one.hosts.netssh]:2200 + # [one.hosts.netssh]:2200,[127.0.0.1]:2200 + host_to_match = host.split(",").first.gsub(/\[|\]:\d+/, "") + server_key.valid_principals.empty? || server_key.valid_principals.include?(host_to_match) end def matches_key?(server_key) @@ -120,12 +125,7 @@ def search_for(host, options = {}) # Search for all known keys for the given host, in every file given in # the +files+ array. Returns the list of keys. def search_in(files, host, options = {}) - # one.hosts.netssh - # one.hosts.netssh,127.0.0.1 - # [one.hosts.netssh]:2200 - # [one.hosts.netssh]:2200,[127.0.0.1]:2200 - host_to_search_for = host.split(",").first.gsub(/\[|\]:\d+/, "") - files.flat_map { |file| KnownHosts.new(file).keys_for(host_to_search_for, options) } + files.flat_map { |file| KnownHosts.new(file).keys_for(host, options) } end # Looks in the given +options+ hash for the :user_known_hosts_file and From 0d4d6ac75d75b14c71458208b3ec6ac63b14192b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Thu, 15 Jun 2023 13:57:10 +0200 Subject: [PATCH 05/13] Make rubocop happy --- lib/net/ssh/verifiers/always.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb index 8c607f0ea..7b6b6c67f 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -29,17 +29,16 @@ def verify(arguments) end end - if found && found.respond_to?(:matches_principal?) + if found.respond_to?(:matches_principal?) # one.hosts.netssh # one.hosts.netssh,127.0.0.1 # [one.hosts.netssh]:2200 # [one.hosts.netssh]:2200,[127.0.0.1]:2200 hostname_to_verify = host_keys.host.split(",").first.gsub(/\[|\]:\d+/, "") - principal_match = found.matches_principal?(arguments[:key], hostname_to_verify) - unless principal_match - process_cache_miss(host_keys, arguments, HostKeyUnknown, "Certificate invalid: name is not a listed principal") - end + return true if found.matches_principal?(arguments[:key], hostname_to_verify) + + process_cache_miss(host_keys, arguments, HostKeyUnknown, "Certificate invalid: name is not a listed principal") end # If a match was found, return true. Otherwise, raise an exception From be2012780d1248b1e62692bc1a53c4e80d2ef9bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Thu, 15 Jun 2023 14:10:04 +0200 Subject: [PATCH 06/13] Fix Dockerfile: Specify netcat package Would fail otherwise, as there's two netcat's Package netcat is a virtual package provided by: netcat-openbsd 1.219-1 netcat-traditional 1.10-47 E: Package 'netcat' has no installation candidate --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 921f72bf3..4d16f24f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ARG RUBY_VERSION=3.1 FROM ruby:${RUBY_VERSION} -RUN apt update && apt install -y openssh-server sudo netcat \ +RUN apt update && apt install -y openssh-server sudo netcat-openbsd \ && useradd --create-home --shell '/bin/bash' --comment 'NetSSH' 'net_ssh_1' \ && useradd --create-home --shell '/bin/bash' --comment 'NetSSH' 'net_ssh_2' \ && echo net_ssh_1:foopwd | chpasswd \ From aa171f00ec3cf696212e95c596e0425d2ff0c3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Thu, 15 Jun 2023 14:45:44 +0200 Subject: [PATCH 07/13] Refactor --- lib/net/ssh/known_hosts.rb | 18 +++++++++++------- lib/net/ssh/verifiers/always.rb | 10 ++-------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index ff884f3ce..5aae7f0eb 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -55,13 +55,8 @@ def initialize(key, comment: nil) @comment = comment end - def matches_principal?(server_key, host) - # one.hosts.netssh - # one.hosts.netssh,127.0.0.1 - # [one.hosts.netssh]:2200 - # [one.hosts.netssh]:2200,[127.0.0.1]:2200 - host_to_match = host.split(",").first.gsub(/\[|\]:\d+/, "") - server_key.valid_principals.empty? || server_key.valid_principals.include?(host_to_match) + def matches_principal?(server_key, hostname) + server_key.valid_principals.empty? || server_key.valid_principals.include?(hostname) end def matches_key?(server_key) @@ -99,6 +94,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 7b6b6c67f..0c749525a 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -29,15 +29,9 @@ def verify(arguments) end end + # If found host_key has principal support (CertAuthority), it must match if found.respond_to?(:matches_principal?) - # one.hosts.netssh - # one.hosts.netssh,127.0.0.1 - # [one.hosts.netssh]:2200 - # [one.hosts.netssh]:2200,[127.0.0.1]:2200 - hostname_to_verify = host_keys.host.split(",").first.gsub(/\[|\]:\d+/, "") - - return true if found.matches_principal?(arguments[:key], hostname_to_verify) - + return true if found.matches_principal?(arguments[:key], host_keys.hostname) process_cache_miss(host_keys, arguments, HostKeyUnknown, "Certificate invalid: name is not a listed principal") end From 4897e18f4f8ee4a92e5dc8a4696e1f9ed25e3ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Fri, 16 Jun 2023 10:55:42 +0200 Subject: [PATCH 08/13] Lint fix --- lib/net/ssh/verifiers/always.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb index 0c749525a..12717140a 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -32,6 +32,7 @@ def verify(arguments) # If found host_key has principal support (CertAuthority), it must match if found.respond_to?(:matches_principal?) return true if found.matches_principal?(arguments[:key], host_keys.hostname) + process_cache_miss(host_keys, arguments, HostKeyUnknown, "Certificate invalid: name is not a listed principal") end From d3dbaa8d9c48c310a54f73caf5527c0d45cbfec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Mon, 19 Jun 2023 13:50:03 +0200 Subject: [PATCH 09/13] Refactor: Improve readability of verifier --- lib/net/ssh/verifiers/always.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb index 12717140a..904ce0921 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -21,7 +21,7 @@ def verify(arguments) # If we found any matches, check to see that the key type and # blob also match. - found = host_keys.find do |key| + found_keys = host_keys.find do |key| if key.respond_to?(:matches_key?) key.matches_key?(arguments[:key]) else @@ -29,17 +29,18 @@ def verify(arguments) end end + # 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_keys + # If found host_key has principal support (CertAuthority), it must match - if found.respond_to?(:matches_principal?) - return true if found.matches_principal?(arguments[:key], host_keys.hostname) + 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 - # 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 - + # If we passed all checks, it's verified true end From e92a22ba6b677fc9cde8367a684bc929c25407f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Thu, 6 Jul 2023 15:13:25 +0200 Subject: [PATCH 10/13] Implement validity check --- lib/net/ssh/known_hosts.rb | 17 ++++++++ lib/net/ssh/verifiers/always.rb | 9 ++++ test/integration/test_cert_host_auth.rb | 56 ++++++++++++++++++++++++- 3 files changed, 80 insertions(+), 2 deletions(-) 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| From 5af2e07e1bf0f33253a74317e9ab3df9aaec094a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Tue, 6 Feb 2024 11:52:02 +0100 Subject: [PATCH 11/13] Fix: Remove debug statements --- lib/net/ssh/known_hosts.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index a69d11c65..f8b28f48e 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -56,20 +56,17 @@ 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 From a2353dde7fd7846cfeded10f64a4a05b9e39bba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Wed, 7 Feb 2024 19:54:24 +0100 Subject: [PATCH 12/13] Remove breakpoint --- lib/net/ssh/verifiers/always.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb index 52f409dc0..8417ecc97 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -36,7 +36,6 @@ def verify(arguments) 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 From 3ba4c06b7326fe9c19e4244daebd9849a9d596b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Eckerstr=C3=B6m?= Date: Fri, 15 Mar 2024 12:58:15 +0100 Subject: [PATCH 13/13] TODO test matches_validity? --- lib/net/ssh/known_hosts.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index f8b28f48e..1c03bfa69 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -59,6 +59,7 @@ 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