summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiklós Fazekas <mfazekas@szemafor.com>2021-08-10 10:04:46 +0200
committerGitHub <noreply@github.com>2021-08-10 10:04:46 +0200
commit355670ee82932608817c2e686ed7d10b1fca9d1c (patch)
tree394dce856b86a737ba5b2e4aa4e8432d35a32629
parentba3a689713a4635c16eb9a35c5c62be34be60e8b (diff)
parent344cc90c83eda9d2e1f4d5d272b1c325717db633 (diff)
downloadnet-ssh-355670ee82932608817c2e686ed7d10b1fca9d1c.tar.gz
Merge pull request #833 from net-ssh/mfazekas/cert-base-auth
Cert based host auth
-rw-r--r--.rubocop.yml7
-rw-r--r--CHANGES.txt5
-rw-r--r--lib/net/ssh.rb2
-rw-r--r--lib/net/ssh/known_hosts.rb81
-rw-r--r--lib/net/ssh/transport/algorithms.rb2
-rw-r--r--lib/net/ssh/verifiers/always.rb8
-rw-r--r--test/integration/common.rb21
-rw-r--r--test/integration/playbook.yml10
-rw-r--r--test/integration/test_cert_host_auth.rb94
-rw-r--r--test/integration/test_channel.rb2
-rw-r--r--test/test_known_hosts.rb38
-rw-r--r--test/transport/test_algorithms.rb12
12 files changed, 257 insertions, 25 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 973c53d..6ac7e73 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,3 +1,10 @@
+AllCops:
+ Exclude:
+ - 'tryout/**/*'
+ - "vendor/**/.*"
+ - "vendor/**/*"
+ NewCops: enable
+
inherit_from: .rubocop_todo.yml
Style/DoubleNegation:
diff --git a/CHANGES.txt b/CHANGES.txt
index d511bf9..8fb56bf 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,8 @@
+=== 6.3.0 beta1
+
+ * Support cert based host key auth, fix asterisk in known_hosts [#833]
+ * Support kex dh-group14-sha256 [#795]
+
=== 6.2.0 rc1
=== 6.2.0 beta1
diff --git a/lib/net/ssh.rb b/lib/net/ssh.rb
index 8b8b7b9..bbae909 100644
--- a/lib/net/ssh.rb
+++ b/lib/net/ssh.rb
@@ -121,7 +121,7 @@ module Net
# * :forward_agent => set to true if you want the SSH agent connection to
# be forwarded
# * :known_hosts => a custom object holding known hosts records.
- # It must implement #search_for and add in a similiar manner as KnownHosts.
+ # It must implement #search_for and `add` in a similiar manner as KnownHosts.
# * :global_known_hosts_file => the location of the global known hosts
# file. Set to an array if you want to specify multiple global known
# hosts files. Defaults to %w(/etc/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts2).
diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb
index f3d773a..eb7d7a7 100644
--- a/lib/net/ssh/known_hosts.rb
+++ b/lib/net/ssh/known_hosts.rb
@@ -6,6 +6,64 @@ require 'net/ssh/authentication/ed25519_loader'
module Net
module SSH
+ module HostKeyEntries
+ # regular public key entry
+ class PubKey < Delegator
+ def initialize(key, comment: nil) # rubocop:disable Lint/MissingSuper
+ @key = key
+ @comment = comment
+ end
+
+ def ssh_type
+ @key.ssh_type
+ end
+
+ def ssh_types
+ [ssh_type]
+ end
+
+ def to_blob
+ @key.to_blob
+ end
+
+ def __getobj__
+ Kernel.warn("Calling Net::SSH::Buffer methods on HostKeyEntries PubKey is deprecated")
+ @key
+ end
+
+ def matches_key?(server_key)
+ @key.ssh_type == server_key.ssh_type && @key.to_blob == server_key.to_blob
+ end
+ end
+
+ # @cert-authority entry
+ class CertAuthority
+ def ssh_types
+ %w[
+ ecdsa-sha2-nistp256-cert-v01@openssh.com
+ ecdsa-sha2-nistp384-cert-v01@openssh.com
+ ecdsa-sha2-nistp521-cert-v01@openssh.com
+ ssh-ed25519-cert-v01@openssh.com
+ ssh-rsa-cert-v01@openssh.com
+ ssh-rsa-cert-v00@openssh.com
+ ]
+ end
+
+ def initialize(key, comment: nil)
+ @key = key
+ @comment = comment
+ 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)
+ else
+ false
+ end
+ end
+ end
+ end
+
# Represents the result of a search in known hosts
# see search_for
class HostKeys
@@ -127,7 +185,13 @@ module Net
File.open(source) do |file|
file.each_line do |line|
- hosts, type, key_content = line.split(' ')
+ if line.start_with?('@')
+ marker, hosts, type, key_content, comment = line.split(' ')
+ else
+ marker = nil
+ hosts, type, key_content, comment = line.split(' ')
+ end
+
# Skip empty line or one that is commented
next if hosts.nil? || hosts.start_with?('#')
@@ -142,7 +206,14 @@ module Net
next unless found
blob = key_content.unpack("m*").first
- keys << Net::SSH::Buffer.new(blob).read_key
+ raw_key = Net::SSH::Buffer.new(blob).read_key
+
+ keys <<
+ if marker == "@cert-authority"
+ HostKeyEntries::CertAuthority.new(raw_key, comment: comment)
+ else
+ HostKeyEntries::PubKey.new(raw_key, comment: comment)
+ end
end
end
@@ -152,11 +223,11 @@ module Net
def match(host, pattern)
if pattern.include?('*') || pattern.include?('?')
# see man 8 sshd for pattern details
- pattern_regexp = pattern.split('*').map do |x|
- x.split('?').map do |y|
+ pattern_regexp = pattern.split('*', -1).map do |x|
+ x.split('?', -1).map do |y|
Regexp.escape(y)
end.join('.')
- end.join('[^.]*')
+ end.join('.*')
host =~ Regexp.new("\\A#{pattern_regexp}\\z")
else
diff --git a/lib/net/ssh/transport/algorithms.rb b/lib/net/ssh/transport/algorithms.rb
index 7408d40..f0d8d15 100644
--- a/lib/net/ssh/transport/algorithms.rb
+++ b/lib/net/ssh/transport/algorithms.rb
@@ -278,7 +278,7 @@ module Net
# existing known key for the host has preference.
existing_keys = session.host_keys
- host_keys = existing_keys.map { |key| key.ssh_type }.uniq
+ host_keys = existing_keys.flat_map { |key| key.respond_to?(:ssh_types) ? key.ssh_types : [key.ssh_type] }.uniq
algorithms[:host_key].each do |name|
host_keys << name unless host_keys.include?(name)
end
diff --git a/lib/net/ssh/verifiers/always.rb b/lib/net/ssh/verifiers/always.rb
index 0f52a29..0c86589 100644
--- a/lib/net/ssh/verifiers/always.rb
+++ b/lib/net/ssh/verifiers/always.rb
@@ -21,9 +21,13 @@ module Net
# If we found any matches, check to see that the key type and
# blob also match.
+
found = host_keys.any? do |key|
- key.ssh_type == arguments[:key].ssh_type &&
- key.to_blob == arguments[:key].to_blob
+ if key.respond_to?(:matches_key?)
+ key.matches_key?(arguments[:key])
+ else
+ key.ssh_type == arguments[:key].ssh_type && key.to_blob == arguments[:key].to_blob
+ end
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..b897ece 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}" if debug
yield(f.path, pidpath)
end
end
@@ -106,20 +107,22 @@ 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
else
- with_lines_as_tempfile('') do |path, pidpath|
+ with_lines_as_tempfile(['']) do |path, pidpath|
pid = spawn('sudo', '/opt/net-ssh-openssh/sbin/sshd', '-D', '-f', path, '-p', port)
sshpidfile = pidpath
yield pid, port
diff --git a/test/integration/playbook.yml b/test/integration/playbook.yml
index 75a1ee2..828dda2 100644
--- a/test/integration/playbook.yml
+++ b/test/integration/playbook.yml
@@ -92,16 +92,22 @@
lineinfile: dest='/etc/ssh/sshd_config' line='X11Forwarding no' regexp=X11Forwarding
notify: restart sshd
- name: sshd allow forward
- lineinfile: dest='/etc/ssh/sshd_config' line='PasswordAuthentication=yes' regexp=PasswordAuthentication
+ lineinfile: dest='/etc/ssh/sshd_config' line='#PasswordAuthentication no' regexp='#?PasswordAuthentication.+no'
+ notify: restart sshd
+ - name: sshd allow forward
+ lineinfile: dest='/etc/ssh/sshd_config' line='PasswordAuthentication yes' regexp=PasswordAuthentication
notify: restart sshd
- name: put NET_SSH_RUN_INTEGRATION_TESTS=YES environment
lineinfile: dest='/etc/environment' line='NET_SSH_RUN_INTEGRATION_TESTS=YES'
- name: change dir in bashrc
lineinfile: dest="{{homedir}}/.bashrc" owner="{{myuser}}" mode=0644
regexp='^cd ' line='cd /net-ssh'
- - name: add host aliases
+ - name: add host aliases1
lineinfile: dest='/etc/hosts' owner='root' group='root' mode=0644
regexp='^127\.0\.0\.1\s+gateway.netssh' line='127.0.0.1 gateway.netssh'
+ - 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: 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
new file mode 100644
index 0000000..fee5b2b
--- /dev/null
+++ b/test/integration/test_cert_host_auth.rb
@@ -0,0 +1,94 @@
+require_relative 'common'
+require 'fileutils'
+require 'tmpdir'
+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|
+ cert_type = "rsa"
+ # cert_type = "ssh-ed25519"
+ host_key_type = "ecdsa"
+ # host_key_type = "ed25519"
+
+ # create a cert, and sign the host key
+ @cert = "#{dir}/ca"
+ sh "rm -rf #{@cert} #{@cert}.pub"
+ 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"
+ 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"
+ sh "sudo cp -f #{dir}/one.hosts.netssh-cert.pub #{signed_host_key}"
+
+ # we don't use this for signing the cert
+ @badcert = "#{dir}/badca"
+ sh "rm -rf #{@badcert} #{@badcert}.pub"
+ sh "ssh-keygen -t #{cert_type} -N '' -C 'ca@hosts.netssh' -f #{@badcert} #{debug ? '' : '-q'}"
+ yield(cert_pub: "#{@cert}.pub", badcert_pub: "#{@badcert}.pub", signed_host_key: signed_host_key)
+ end
+ end
+
+ def debug
+ false
+ end
+
+ def test_host_should_match_when_host_key_was_signed_by_key
+ 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_with_other_pub_key_host_key_should_not_match
+ Tempfile.open('cert_kh') do |f|
+ setup_ssh_env do |params|
+ data = File.read(params[:badcert_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(100) do
+ 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]) do |ssh|
+ ssh.exec! "echo 'foo'"
+ end
+ end
+ rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
+ sleep 0.25
+ retry
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/test/integration/test_channel.rb b/test/integration/test_channel.rb
index 46f5fd5..4d55b75 100644
--- a/test/integration/test_channel.rb
+++ b/test/integration/test_channel.rb
@@ -108,7 +108,7 @@ class TestChannel < NetSSHTest
def test_channel_should_set_environment_variables_on_remote
setup_ssh_env do
- start_sshd_7_or_later(config: 'AcceptEnv foo baz') do |_pid, port|
+ start_sshd_7_or_later(config: ['AcceptEnv foo baz']) do |_pid, port|
Timeout.timeout(20) do
# We have our own sshd, give it a chance to come up before
# listening.
diff --git a/test/test_known_hosts.rb b/test/test_known_hosts.rb
index fcd64fa..5bd7668 100644
--- a/test/test_known_hosts.rb
+++ b/test/test_known_hosts.rb
@@ -1,4 +1,4 @@
-require 'common'
+require_relative './common'
class TestKnownHosts < NetSSHTest
def perform_test(source)
@@ -109,7 +109,29 @@ class TestKnownHosts < NetSSHTest
def test_search_for_with_hostname_not_matching_pattern_3
options = { user_known_hosts_file: path("known_hosts/misc") }
keys = Net::SSH::KnownHosts.search_for('subsubdomain.subdomain.gitfoo.com',options)
- assert_equal(0, keys.count)
+ assert_equal(1, keys.count)
+ end
+
+ def test_asterisk_matches_multiple_dots
+ with_config_file(lines: ["*.git???.com #{sample_key}"]) do |path|
+ options = { user_known_hosts_file: path }
+ keys = Net::SSH::KnownHosts.search_for('subsubdomain.subdomain.gitfoo.com',options)
+ assert_equal(1, keys.count)
+
+ keys = Net::SSH::KnownHosts.search_for('subsubdomain.subdomain.gitfoo2.com',options)
+ assert_equal(0, keys.count)
+ end
+ end
+
+ def test_asterisk_matches_everything
+ with_config_file(lines: ["* #{sample_key}"]) do |path|
+ options = { user_known_hosts_file: path }
+ keys = Net::SSH::KnownHosts.search_for('subsubdomain.subdomain.gitfoo.com',options)
+ assert_equal(1, keys.count)
+
+ keys = Net::SSH::KnownHosts.search_for('subsubdomain.subdomain.gitfoo2.com',options)
+ assert_equal(1, keys.count)
+ end
end
def test_search_for_then_add
@@ -131,6 +153,18 @@ class TestKnownHosts < NetSSHTest
File.join(File.dirname(__FILE__), relative_path)
end
+ def sample_key
+ "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="
+ end
+
+ def with_config_file(lines: [], &block)
+ Tempfile.open('known_hosts') do |f|
+ f.write(lines.join("\n"))
+ f.close
+ yield(f.path)
+ end
+ end
+
def rsa_key
key = OpenSSL::PKey::RSA.new
if key.respond_to?(:set_key)
diff --git a/test/transport/test_algorithms.rb b/test/transport/test_algorithms.rb
index d79d446..cef1b19 100644
--- a/test/transport/test_algorithms.rb
+++ b/test/transport/test_algorithms.rb
@@ -46,7 +46,10 @@ module Transport
end
def test_constructor_with_known_hosts_reporting_known_host_key_should_use_that_host_key_type
- Net::SSH::KnownHosts.expects(:search_for).with("net.ssh.test,127.0.0.1", {}).returns([stub("key", ssh_type: "ssh-dss")])
+ Net::SSH::KnownHosts.expects(:search_for).with(
+ "net.ssh.test,127.0.0.1",
+ { user_known_hosts_file: "/dev/null", global_known_hosts_file: "/dev/null" }
+ ).returns([stub("key", ssh_type: "ssh-dss")])
assert_equal %w[ssh-dss] + ed_ec_host_keys + %w[ssh-rsa-cert-v01@openssh.com ssh-rsa-cert-v00@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512], algorithms[:host_key]
end
@@ -438,7 +441,12 @@ module Transport
end
def transport(transport_options={})
- @transport ||= MockTransport.new(transport_options)
+ @transport ||= MockTransport.new(
+ {
+ user_known_hosts_file: '/dev/null',
+ global_known_hosts_file: '/dev/null'
+ }.merge(transport_options)
+ )
end
end
end