diff options
author | Miklós Fazekas <mfazekas@szemafor.com> | 2021-08-05 08:53:06 +0200 |
---|---|---|
committer | Miklós Fazekas <mfazekas@szemafor.com> | 2021-08-05 08:53:06 +0200 |
commit | 3f55af876caf797e89e9288aa4084897cded41bb (patch) | |
tree | de09927ef2dcf12e5f9cb8f63f29968c767a2262 | |
parent | 52d16fbe353b8eb9669e967d1922cbaa4c904c21 (diff) | |
download | net-ssh-3f55af876caf797e89e9288aa4084897cded41bb.tar.gz |
Initial version of cert base host auth
-rw-r--r-- | lib/net/ssh/known_hosts.rb | 43 | ||||
-rw-r--r-- | lib/net/ssh/verifiers/always.rb | 5 | ||||
-rw-r--r-- | test/integration/common.rb | 19 | ||||
-rw-r--r-- | test/integration/test_cert_host_auth.rb | 97 |
4 files changed, 144 insertions, 20 deletions
diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index f3d773a..7fbda9b 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -6,6 +6,21 @@ require 'net/ssh/authentication/ed25519_loader' module Net module SSH + class HostCertPub + def ssh_type + "ecdsa-sha2-nistp256-cert-v01@openssh.com" + end + + def initialize(content) + @content = content + end + + def matches?(server_key) + certblob = Buffer.new(server_key).read_key + certblob.signature_valid? && (certblob.signature_key.to_blob == @content.to_blob) + end + end + # Represents the result of a search in known hosts # see search_for class HostKeys @@ -127,22 +142,28 @@ module Net File.open(source) do |file| file.each_line do |line| - hosts, type, key_content = line.split(' ') - # Skip empty line or one that is commented - next if hosts.nil? || hosts.start_with?('#') + if line.start_with?("@cert-authority ") + cert_auth, hosts, type, key_content = line.split(' ') + blob = key_content.unpack("m*").first + keys << HostCertPub.new(Net::SSH::Buffer.new(blob).read_key ) + else + hosts, type, key_content = line.split(' ') + # Skip empty line or one that is commented + next if hosts.nil? || hosts.start_with?('#') - hostlist = hosts.split(',') + hostlist = hosts.split(',') - next unless SUPPORTED_TYPE.include?(type) + next unless SUPPORTED_TYPE.include?(type) - found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries) - next unless found + found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries) + next unless found - found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1 - next unless found + found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1 + next unless found - blob = key_content.unpack("m*").first - keys << Net::SSH::Buffer.new(blob).read_key + blob = key_content.unpack("m*").first + keys << Net::SSH::Buffer.new(blob).read_key + end end end diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb index 0f52a29..48c70bb 100644 --- a/lib/net/ssh/verifiers/always.rb +++ b/lib/net/ssh/verifiers/always.rb @@ -23,7 +23,10 @@ module Net # blob also match. found = host_keys.any? do |key| key.ssh_type == arguments[:key].ssh_type && - key.to_blob == arguments[:key].to_blob + ( + (key.respond_to?(:to_blob) && key.to_blob == arguments[:key].to_blob) || + (key.respond_to?(:matches?) && key.matches?(arguments[:key].to_blob)) + ) end # If a match was found, return true. Otherwise, raise an exception diff --git a/test/integration/common.rb b/test/integration/common.rb index 73eadf8..791106d 100644 --- a/test/integration/common.rb +++ b/test/integration/common.rb @@ -87,16 +87,17 @@ module IntegrationTestHelpers end end - def with_lines_as_tempfile(lines = [], add_pid = true, &block) + def with_lines_as_tempfile(lines = [], add_pid: true, debug: false, &block) Tempfile.open('sshd_config') do |f| - f.write(lines) + f.write(lines.join("\n")) pidpath = nil if add_pid pidpath = f.path + '.pid' - f.write("\nPIDFILE #{pidpath}") + f.write("\nPidFile #{pidpath}\n") end - # f.write("\nLogLevel DEBUG3") + f.write("\nLogLevel DEBUG3\n") if debug f.close + puts "CONFIG: #{f.path} PID: #{pidpath}" yield(f.path, pidpath) end end @@ -106,15 +107,17 @@ module IntegrationTestHelpers end # @yield [pid, port] - def start_sshd_7_or_later(port = '2200', config: nil) + def start_sshd_7_or_later(port = '2200', config: nil, debug: false) pid = nil sshpidfile = nil if config - with_lines_as_tempfile(config) do |path, pidpath| - # puts "DEBUG - SSH LOG: #{path}-log.txt" + with_lines_as_tempfile(config, debug: debug) do |path, pidpath| + puts "DEBUG - SSH LOG: #{path}-log.txt config: #{path}" if debug raise "A leftover sshd is already running" if port_open?(port) - pid = spawn('sudo', '/opt/net-ssh-openssh/sbin/sshd', '-D', '-f', path, '-p', port) # '-E', "#{path}-log.txt") + extra_params = [] + extra_params = ['-E', "#{path}-log.txt"] if debug + pid = spawn('sudo', '/opt/net-ssh-openssh/sbin/sshd', '-D', '-f', path, '-p', port, *extra_params) sshpidfile = pidpath yield pid, port end diff --git a/test/integration/test_cert_host_auth.rb b/test/integration/test_cert_host_auth.rb new file mode 100644 index 0000000..97aed81 --- /dev/null +++ b/test/integration/test_cert_host_auth.rb @@ -0,0 +1,97 @@ +require_relative 'common' +require 'fileutils' +require 'tmpdir' +require 'byebug' +require 'net/ssh' + +require 'timeout' + +# see Vagrantfile,playbook for env. +# we're running as net_ssh_1 user password foo +# and usually connecting to net_ssh_2 user password foo2pwd +class TestCertHostAuth < NetSSHTest + include IntegrationTestHelpers + + def setup_ssh_env(&block) + tmpdir do |dir| + @badcert = "#{dir}/badca" + sh "rm -rf #{@badcert} #{@badcert}.pub" + sh "ssh-keygen -t rsa -N '' -C 'ca@hosts.netssh' -f #{@badcert}" + + @cert = "#{dir}/ca" + sh "rm -rf #{@cert} #{@cert}.pub" + sh "ssh-keygen -t rsa -N '' -C 'ca@hosts.netssh' -f #{@cert}" + FileUtils.cp "/etc/ssh/ssh_host_ecdsa_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 #{dir}/one.hosts.netssh.pub" + sh "ssh-keygen -L -f one.hosts.netssh-cert.pub" + end + # FileUtils.cp "#{dir}/cloud.jameshfisher.com-cert.pub", "/etc/ssh/ssh_host_ecdsa_key-cert.pub" + sh "sudo cp -f #{dir}/one.hosts.netssh-cert.pub /etc/ssh/ssh_host_ecdsa_key-cert.pub" + yield(cert_pub: "#{@cert}.pub", badcert_pub: "#{@badcert}.pub") + end + end + + def test_smoke + config_lines = [] + config_lines.push("HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub") + + Tempfile.open('cert_kh') do |f| + setup_ssh_env do |params| + data = File.read(params[:cert_pub]) + puts "Data: #{data}" + f.write("@cert-authority *.hosts.netssh #{data}") + f.close + + start_sshd_7_or_later(config: config_lines, debug: true) do |_pid, port| + Timeout.timeout(400) do + # We have our own sshd, give it a chance to come up before + # listening. + ret = Net::SSH.start("one.hosts.netssh", "net_ssh_1", password: 'foopwd', port: port, verify_host_key: :always, user_known_hosts_file: [f.path], verbose: :debug) do |ssh| + # assert_equal ssh.transport.algorithms.kex, "curve25519-sha256" + 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_failure + config_lines = [] + config_lines.push("HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub") + + Tempfile.open('empty_kh') do |f| + setup_ssh_env do |params| + data = File.read(params[:badcert_pub]) + + puts "Data: #{data}" + f.write("@cert-authority *.hosts.netssh #{data}") + f.close + + start_sshd_7_or_later(config: config_lines, debug: true) do |_pid, port| + Timeout.timeout(400) do + # We have our own sshd, give it a chance to come up before + # listening. + #sh "ssh net_ssh_1@one.hosts.netssh -p #{port} -o UserKnownHostsFile=#{f.path}" + + sleep 0.2 + 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], verbose: :debug) do |ssh| + # assert_equal ssh.transport.algorithms.kex, "curve25519-sha256" + ssh.exec! "echo 'foo'" + end + end + rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH + sleep 0.25 + retry + end + end + end + end + end +end |