Skip to content
28 changes: 28 additions & 0 deletions lib/net/ssh/known_hosts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
23 changes: 19 additions & 4 deletions lib/net/ssh/verifiers/always.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions test/integration/playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
107 changes: 105 additions & 2 deletions test/integration/test_cert_host_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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