summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiklós Fazekas <mfazekas@szemafor.com>2021-08-05 08:53:06 +0200
committerMiklós Fazekas <mfazekas@szemafor.com>2021-08-05 08:53:06 +0200
commit3f55af876caf797e89e9288aa4084897cded41bb (patch)
treede09927ef2dcf12e5f9cb8f63f29968c767a2262
parent52d16fbe353b8eb9669e967d1922cbaa4c904c21 (diff)
downloadnet-ssh-3f55af876caf797e89e9288aa4084897cded41bb.tar.gz
Initial version of cert base host auth
-rw-r--r--lib/net/ssh/known_hosts.rb43
-rw-r--r--lib/net/ssh/verifiers/always.rb5
-rw-r--r--test/integration/common.rb19
-rw-r--r--test/integration/test_cert_host_auth.rb97
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