summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiklós Fazekas <mfazekas@szemafor.com>2023-05-15 10:29:32 +0200
committerMiklós Fazekas <mfazekas@szemafor.com>2023-05-15 10:39:54 +0200
commit868ed01e9f64338812c1f67e10f04e4805bdbc33 (patch)
tree4802e94b27892932b90e3917a15ef6c2c2a6c0ba
parent47deccf6ce14fceca677483c1ae4cb1227dcd562 (diff)
downloadnet-ssh-mfazekas/cacha20-poly1305.tar.gz
POC: chacha20-poly1305 with rbnacl poly1305mfazekas/cacha20-poly1305
-rw-r--r--DEVELOPMENT.md23
-rw-r--r--lib/net/ssh/transport/algorithms.rb18
-rw-r--r--lib/net/ssh/transport/chacha20_poly1305_cipher.rb136
-rw-r--r--lib/net/ssh/transport/cipher_factory.rb13
-rw-r--r--lib/net/ssh/transport/identity_cipher.rb4
-rw-r--r--lib/net/ssh/transport/packet_stream.rb90
-rw-r--r--test/integration/test_chacha20_poly1305_cipher.rb45
7 files changed, 291 insertions, 38 deletions
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 0000000..35727b4
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,23 @@
+### Development notes
+
+## Building/running ssh server in debug mode
+
+clone the openssh server from `https://github.com/openssh/openssh-portable`
+
+```sh
+brew install openssl
+/usr/local/Cellar/openssl@3/3.1.0/bin/openssl
+
+autoreconf
+./configure --with-ssl-dir=/usr/local/Cellar/openssl@3/3.1.0/ --with-audit=debug --enable-debug CPPFLAGS="-DDEBUG -DPACKET_DEBUG" CFLAGS="-g -O0"
+make
+```
+
+To run server in debug mode:
+```sh
+echo '#' > /tmp/sshd_config
+ssh-keygen -t rsa -f /tmp/ssh_host_rsa_key
+# /Users/boga/Work/OSS/NetSSH/openssh-portable/sshd -p 2222 -D -d -d -d -e -f /tmp/sshd_config
+/Users/boga/Work/OSS/NetSSH/openssh-portable/sshd -p 2222 -D -d -d -d -e -f /tmp/sshd_config -h /tmp/ssh_host_rsa_key
+
+```
diff --git a/lib/net/ssh/transport/algorithms.rb b/lib/net/ssh/transport/algorithms.rb
index cc12d95..cdb63bf 100644
--- a/lib/net/ssh/transport/algorithms.rb
+++ b/lib/net/ssh/transport/algorithms.rb
@@ -44,7 +44,7 @@ module Net
diffie-hellman-group14-sha256
diffie-hellman-group14-sha1],
- encryption: %w[aes256-ctr aes192-ctr aes128-ctr],
+ encryption: %w[aes256-ctr aes192-ctr aes128-ctr chacha20-poly1305@openssh.com],
hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com
hmac-sha2-512 hmac-sha2-256
@@ -430,6 +430,15 @@ module Net
sizes.max
end
+ def expand_key(key, need_size, digester, secret, hash)
+ result = key
+ while result.size < need_size
+ new_part = digester.digest(secret + hash + result)
+ result += new_part
+ end
+ return result
+ end
+
# Instantiates one of the Transport::Kex classes (based on the negotiated
# kex algorithm), and uses it to exchange keys. Then, the ciphers and
# HMACs are initialized and fed to the transport layer, to be used in
@@ -437,12 +446,13 @@ module Net
def exchange_keys
debug { "exchanging keys" }
+ need_bytes = kex_byte_requirement
algorithm = Kex::MAP[kex].new(self, session,
client_version_string: Net::SSH::Transport::ServerVersion::PROTO_VERSION,
server_version_string: session.server_version.version,
server_algorithm_packet: @server_packet,
client_algorithm_packet: @client_packet,
- need_bytes: kex_byte_requirement,
+ need_bytes: need_bytes,
minimum_dh_bits: options[:minimum_dh_bits],
logger: logger)
result = algorithm.exchange_keys
@@ -464,8 +474,8 @@ module Net
parameters = { shared: secret, hash: hash, digester: digester }
- cipher_client = CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: key_client, encrypt: true))
- cipher_server = CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: key_server, decrypt: true))
+ cipher_client = CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: expand_key(key_client, need_bytes, digester, secret, hash), encrypt: true))
+ cipher_server = CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: expand_key(key_server, need_bytes, digester, secret, hash), decrypt: true))
mac_client = HMAC.get(hmac_client, mac_key_client, parameters)
mac_server = HMAC.get(hmac_server, mac_key_server, parameters)
diff --git a/lib/net/ssh/transport/chacha20_poly1305_cipher.rb b/lib/net/ssh/transport/chacha20_poly1305_cipher.rb
new file mode 100644
index 0000000..5e04a81
--- /dev/null
+++ b/lib/net/ssh/transport/chacha20_poly1305_cipher.rb
@@ -0,0 +1,136 @@
+require 'rbnacl'
+
+module Net
+ module SSH
+ module Transport
+ class ChaCha20Poly1305Cipher
+ def initialize(encrypt:, key:)
+ @chacha_hdr = OpenSSL::Cipher.new("chacha20")
+ key_len = @chacha_hdr.key_len
+ @chacha_main = OpenSSL::Cipher.new("chacha20")
+ @poly = RbNaCl::OneTimeAuths::Poly1305
+ if key.size != key_len * 2
+ error { "chacha20_poly1305: keylength doesn't match" }
+ raise "chacha20_poly1305: keylength doesn't match"
+ end
+ if encrypt
+ @chacha_hdr.encrypt
+ @chacha_main.encrypt
+ else
+ @chacha_hdr.decrypt
+ @chacha_main.decrypt
+ end
+ main_key = key[0...key_len]
+ @chacha_main.key = main_key # k2
+ hdr_key = key[key_len...(2 * key_len)]
+ @chacha_hdr.key = hdr_key # k1
+ end
+
+ def update_cipher_mac(payload, sequence_number)
+ iv_data = [0, 0, 0, sequence_number].pack("NNNN")
+ @chacha_main.iv = iv_data
+ poly_key = @chacha_main.update(([0] * 32).pack('C32'))
+
+ packet_length = payload.size
+ length_data = [packet_length].pack("N")
+ @chacha_hdr.iv = iv_data
+ packet = @chacha_hdr.update(length_data)
+
+ iv_data[0] = 1.chr
+ @chacha_main.iv = iv_data
+ unencrypted_data = payload
+ packet += @chacha_main.update(unencrypted_data)
+
+ packet += @poly.auth(poly_key, packet)
+ return packet
+ end
+
+ def read_length(data, sequence_number)
+ iv_data = [0, 0, 0, sequence_number].pack("NNNN")
+ @chacha_hdr.iv = iv_data
+ length_data = @chacha_hdr.update(data).unpack("N").first
+ end
+
+ def read_and_mac(data, mac, sequence_number)
+ iv_data = [0, 0, 0, sequence_number].pack("NNNN")
+ @chacha_main.iv = iv_data
+ poly_key = @chacha_main.update(([0] * 32).pack('C32'))
+
+ iv_data[0] = 1.chr
+ @chacha_main.iv = iv_data
+ unencrypted_data = @chacha_main.update(data[4..-1])
+ begin
+ ok = @poly.verify(poly_key, mac, data[0..-1])
+ raise Net::SSH::Exception, "corrupted hmac detected #{name}" unless ok
+ rescue RbNaCl::BadAuthenticatorError => e
+ raise Net::SSH::Exception, "corrupted hmac detected #{name}"
+ end
+ return unencrypted_data
+ end
+
+ def mac_length
+ 16
+ end
+
+ def self.key_length
+ 64
+ end
+
+ def block_size
+ 8
+ end
+
+ def name
+ "chacha20-poly1305@openssh.com"
+ end
+
+ class << self
+ # A default block size of 8 is required by the SSH2 protocol.
+ def block_size
+ 8
+ end
+
+ # Returns an arbitrary integer.
+ def iv_len
+ 4
+ end
+
+ # Does nothing. Returns self.
+ def encrypt
+ self
+ end
+
+ # Does nothing. Returns self.
+ def decrypt
+ self
+ end
+
+ # Passes its single argument through unchanged.
+ def update(text)
+ text
+ end
+
+ # Returns the empty string.
+ def final
+ ""
+ end
+
+ # The name of this cipher, which is "chacha20-poly1305@openssh.com".
+ def name
+ "chacha20-poly1305@openssh.com"
+ end
+
+ # Does nothing. Returns nil.
+ def iv=(v)
+ nil
+ end
+
+ # Does nothing. Returns self.
+ def reset
+ self
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/net/ssh/transport/cipher_factory.rb b/lib/net/ssh/transport/cipher_factory.rb
index 4dde239..8c61721 100644
--- a/lib/net/ssh/transport/cipher_factory.rb
+++ b/lib/net/ssh/transport/cipher_factory.rb
@@ -2,6 +2,7 @@ require 'openssl'
require 'net/ssh/transport/ctr.rb'
require 'net/ssh/transport/key_expander'
require 'net/ssh/transport/identity_cipher'
+require 'net/ssh/transport/chacha20_poly1305_cipher'
module Net
module SSH
@@ -29,13 +30,17 @@ module Net
'none' => 'none'
}
+ SSH_TO_CLASS = {
+ 'chacha20-poly1305@openssh.com' => Net::SSH::Transport::ChaCha20Poly1305Cipher
+ }
+
# Returns true if the underlying OpenSSL library supports the given cipher,
# and false otherwise.
def self.supported?(name)
ossl_name = SSH_TO_OSSL[name] or raise NotImplementedError, "unimplemented cipher `#{name}'"
return true if ossl_name == "none"
- return OpenSSL::Cipher.ciphers.include?(ossl_name)
+ return SSH_TO_CLASS.key?(name) || OpenSSL::Cipher.ciphers.include?(ossl_name)
end
# Retrieves a new instance of the named algorithm. The new instance
@@ -44,6 +49,9 @@ module Net
# cipher will be put into encryption or decryption mode, based on the
# value of the +encrypt+ parameter.
def self.get(name, options = {})
+ klass = SSH_TO_CLASS[name]
+ return klass.new(encrypt: options[:encrypt], key: options[:key]) unless klass.nil?
+
ossl_name = SSH_TO_OSSL[name] or raise NotImplementedError, "unimplemented cipher `#{name}'"
return IdentityCipher if ossl_name == "none"
@@ -75,6 +83,9 @@ module Net
# of the tuple.
# if :iv_len option is supplied the third return value will be ivlen
def self.get_lengths(name, options = {})
+ klass = SSH_TO_CLASS[name]
+ return [klass.key_length, klass.block_size] unless klass.nil?
+
ossl_name = SSH_TO_OSSL[name]
if ossl_name.nil? || ossl_name == "none"
result = [0, 0]
diff --git a/lib/net/ssh/transport/identity_cipher.rb b/lib/net/ssh/transport/identity_cipher.rb
index ad1a764..fdf161e 100644
--- a/lib/net/ssh/transport/identity_cipher.rb
+++ b/lib/net/ssh/transport/identity_cipher.rb
@@ -11,6 +11,10 @@ module Net
8
end
+ def key_length
+ 0
+ end
+
# Returns an arbitrary integer.
def iv_len
4
diff --git a/lib/net/ssh/transport/packet_stream.rb b/lib/net/ssh/transport/packet_stream.rb
index a4120d4..9466096 100644
--- a/lib/net/ssh/transport/packet_stream.rb
+++ b/lib/net/ssh/transport/packet_stream.rb
@@ -144,31 +144,36 @@ module Net
padding = Array.new(padding_length) { rand(256) }.pack("C*")
- if client.hmac.etm
- debug { "using encrypt-then-mac" }
+ if client.cipher.name == "chacha20-poly1305@openssh.com"
+ unencrypted_data = [padding_length, payload, padding].pack("CA*A*")
+ message = client.cipher.update_cipher_mac(unencrypted_data, client.sequence_number)
+ else
+ if client.hmac.etm
+ debug { "using encrypt-then-mac" }
- # Encrypt padding_length, payload, and padding. Take MAC
- # from the unencrypted packet_lenght and the encrypted
- # data.
- length_data = [packet_length].pack("N")
+ # Encrypt padding_length, payload, and padding. Take MAC
+ # from the unencrypted packet_lenght and the encrypted
+ # data.
+ length_data = [packet_length].pack("N")
- unencrypted_data = [padding_length, payload, padding].pack("CA*A*")
+ unencrypted_data = [padding_length, payload, padding].pack("CA*A*")
- encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher
+ encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher
- mac_data = length_data + encrypted_data
+ mac_data = length_data + encrypted_data
- mac = client.hmac.digest([client.sequence_number, mac_data].pack("NA*"))
+ mac = client.hmac.digest([client.sequence_number, mac_data].pack("NA*"))
- message = mac_data + mac
- else
- unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*")
+ message = mac_data + mac
+ else
+ unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*")
- mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*"))
+ mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*"))
- encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher
+ encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher
- message = encrypted_data + mac
+ message = encrypted_data + mac
+ end
end
debug { "queueing packet nr #{client.sequence_number} type #{payload.getbyte(0)} len #{packet_length}" }
@@ -225,44 +230,63 @@ module Net
data = read_available(minimum + aad_length)
# decipher it
- if server.hmac.etm
- @packet_length = data.unpack("N").first
+ if server.cipher.name == "chacha20-poly1305@openssh.com"
+ @packet_length = server.cipher.read_length(data[0...4], server.sequence_number)
+ @packet = Net::SSH::Buffer.new()
@mac_data = data
- @packet = Net::SSH::Buffer.new(server.update_cipher(data[aad_length..-1]))
else
- @packet = Net::SSH::Buffer.new(server.update_cipher(data))
- @packet_length = @packet.read_long
+ if server.hmac.etm
+ @packet_length = data.unpack("N").first
+ @mac_data = data
+ @packet = Net::SSH::Buffer.new(server.update_cipher(data[aad_length..-1]))
+ else
+ @packet = Net::SSH::Buffer.new(server.update_cipher(data))
+ @packet_length = @packet.read_long
+ end
end
end
need = @packet_length + 4 - aad_length - server.block_size
raise Net::SSH::Exception, "padding error, need #{need} block #{server.block_size}" if need % server.block_size != 0
- return nil if available < need + server.hmac.mac_length
+ if server.cipher.name == "chacha20-poly1305@openssh.com"
+ return nil if available < need + server.cipher.mac_length
+ else
+ return nil if available < need + server.hmac.mac_length
+ end
if need > 0
# read the remainder of the packet and decrypt it.
data = read_available(need)
- @mac_data += data if server.hmac.etm
- @packet.append(server.update_cipher(data))
+ @mac_data += data if server.hmac.etm || (server.cipher.name == "chacha20-poly1305@openssh.com")
+ if server.cipher.name != "chacha20-poly1305@openssh.com"
+ @packet.append(server.update_cipher(data))
+ end
end
- # get the hmac from the tail of the packet (if one exists), and
- # then validate it.
- real_hmac = read_available(server.hmac.mac_length) || ""
+ if server.cipher.name != "chacha20-poly1305@openssh.com"
+ # get the hmac from the tail of the packet (if one exists), and
+ # then validate it.
+ real_hmac = read_available(server.hmac.mac_length) || ""
- @packet.append(server.final_cipher)
- padding_length = @packet.read_byte
+ @packet.append(server.final_cipher)
+ padding_length = @packet.read_byte
- payload = @packet.read(@packet_length - padding_length - 1)
+ payload = @packet.read(@packet_length - padding_length - 1)
+
- my_computed_hmac = if server.hmac.etm
+ my_computed_hmac = if server.hmac.etm
server.hmac.digest([server.sequence_number, @mac_data].pack("NA*"))
else
server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
end
- raise Net::SSH::Exception, "corrupted hmac detected #{server.hmac.class}" if real_hmac != my_computed_hmac
-
+ raise Net::SSH::Exception, "corrupted hmac detected #{server.hmac.class}" if real_hmac != my_computed_hmac
+ else
+ real_hmac = read_available(server.cipher.mac_length) || ""
+ @packet = Net::SSH::Buffer.new(server.cipher.read_and_mac(@mac_data, real_hmac, server.sequence_number))
+ padding_length = @packet.read_byte
+ payload = @packet.read(@packet_length - padding_length - 1)
+ end
# try to decompress the payload, in case compression is active
payload = server.decompress(payload)
diff --git a/test/integration/test_chacha20_poly1305_cipher.rb b/test/integration/test_chacha20_poly1305_cipher.rb
new file mode 100644
index 0000000..27fc98a
--- /dev/null
+++ b/test/integration/test_chacha20_poly1305_cipher.rb
@@ -0,0 +1,45 @@
+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 TestChacha20Poly1305Cipher < NetSSHTest
+ include IntegrationTestHelpers
+
+ def test_with_only_chacha20_cipher
+ config_lines = File.read('/etc/ssh/sshd_config').split("\n")
+ config_lines = config_lines.map do |line|
+ if line =~ /^Ciphers/
+ "##{line}"
+ else
+ line
+ end
+ end
+ config_lines.push("Ciphers chacha20-poly1305@openssh.com")
+
+ Tempfile.open('empty_kh') do |f|
+ f.close
+ start_sshd_7_or_later(config: config_lines, debug: true) do |_pid, port|
+ Timeout.timeout(4) do
+ # We have our own sshd, give it a chance to come up before
+ # listening.
+ ret = Net::SSH.start("localhost", "net_ssh_1", encryption: "chacha20-poly1305@openssh.com", password: 'foopwd', port: port, user_known_hosts_file: [f.path], verbose: :debug) do |ssh|
+ byebug
+ #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