Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: CertAuthority principal support #1

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
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 '[email protected]' -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
Loading