Skip to content

Commit

Permalink
Merge pull request net-ssh#946 from flux-benj/aes-gcm
Browse files Browse the repository at this point in the history
feat: Add support for aes(128|256)gcm
  • Loading branch information
mfazekas authored May 29, 2024
2 parents 4691445 + a280273 commit 6dde359
Show file tree
Hide file tree
Showing 15 changed files with 444 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ Lint/UselessTimes:
# Offense count: 205
# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 74
Max: 75

# Offense count: 16
# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
Expand Down
40 changes: 40 additions & 0 deletions lib/net/ssh/transport/aes128_gcm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'net/ssh/transport/hmac/abstract'
require 'net/ssh/transport/gcm_cipher'

module Net::SSH::Transport
## Implements the aes128-gcm@openssh cipher
class AES128_GCM
extend ::Net::SSH::Transport::GCMCipher

## Implicit HMAC, do need to do anything
class ImplicitHMac < ::Net::SSH::Transport::HMAC::Abstract
def aead
true
end

def key_length
16
end
end

def implicit_mac
ImplicitHMac.new
end

def algo_name
'aes-128-gcm'
end

def name
'aes128-gcm@openssh.com'
end

#
# --- RFC 5647 ---
# K_LEN AES key length 16 octets
#
def self.key_length
16
end
end
end
40 changes: 40 additions & 0 deletions lib/net/ssh/transport/aes256_gcm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
require 'net/ssh/transport/hmac/abstract'
require 'net/ssh/transport/gcm_cipher'

module Net::SSH::Transport
## Implements the aes256-gcm@openssh cipher
class AES256_GCM
extend ::Net::SSH::Transport::GCMCipher

## Implicit HMAC, do need to do anything
class ImplicitHMac < ::Net::SSH::Transport::HMAC::Abstract
def aead
true
end

def key_length
32
end
end

def implicit_mac
ImplicitHMac.new
end

def algo_name
'aes-256-gcm'
end

def name
'aes256-gcm@openssh.com'
end

#
# --- RFC 5647 ---
# K_LEN AES key length 32 octets
#
def self.key_length
32
end
end
end
9 changes: 8 additions & 1 deletion lib/net/ssh/transport/algorithms.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ class Algorithms
diffie-hellman-group14-sha256
diffie-hellman-group14-sha1],

encryption: %w[aes256-ctr aes192-ctr aes128-ctr],
encryption: %w[aes256-ctr
aes192-ctr
aes128-ctr
aes256-gcm@openssh.com
aes128-gcm@openssh.com],

hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com
hmac-sha2-512 hmac-sha2-256
Expand Down Expand Up @@ -492,6 +496,9 @@ def exchange_keys
HMAC.get(hmac_server, mac_key_server, parameters)
end

cipher_client.nonce = iv_client if mac_client.respond_to?(:aead) && mac_client.aead
cipher_server.nonce = iv_server if mac_server.respond_to?(:aead) && mac_client.aead

session.configure_client cipher: cipher_client, hmac: mac_client,
compression: normalize_compression_name(compression_client),
compression_level: options[:compression_level],
Expand Down
16 changes: 9 additions & 7 deletions lib/net/ssh/transport/cipher_factory.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'openssl'
require 'net/ssh/transport/ctr.rb'
require 'net/ssh/transport/aes128_gcm'
require 'net/ssh/transport/aes256_gcm'
require 'net/ssh/transport/key_expander'
require 'net/ssh/transport/identity_cipher'
require 'net/ssh/transport/chacha20_poly1305_cipher_loader'
Expand Down Expand Up @@ -31,15 +33,15 @@ class CipherFactory
'none' => 'none'
}

SSH_TO_CLASS =
SSH_TO_CLASS = {
'aes256-gcm@openssh.com' => Net::SSH::Transport::AES256_GCM,
'aes128-gcm@openssh.com' => Net::SSH::Transport::AES128_GCM
}.tap do |hash|
if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED
{
'chacha20-poly1305@openssh.com' => Net::SSH::Transport::ChaCha20Poly1305Cipher
}
else
{
}
hash['chacha20-poly1305@openssh.com'] =
Net::SSH::Transport::ChaCha20Poly1305Cipher
end
end

# Returns true if the underlying OpenSSL library supports the given cipher,
# and false otherwise.
Expand Down
207 changes: 207 additions & 0 deletions lib/net/ssh/transport/gcm_cipher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
require 'net/ssh/loggable'

module Net
module SSH
module Transport
## Extension module for aes(128|256)gcm ciphers
module GCMCipher
# rubocop:disable Metrics/AbcSize
def self.extended(orig)
# rubocop:disable Metrics/BlockLength
orig.class_eval do
include Net::SSH::Loggable

attr_reader :cipher
attr_reader :key
attr_accessor :nonce

#
# Semantically gcm cipher supplies the OpenSSL iv interface with a nonce
# as it is not randomly generated due to being supplied from a counter.
# The RFC's use IV and nonce interchangeably.
#
def initialize(encrypt:, key:)
@cipher = OpenSSL::Cipher.new(algo_name)
@key = key
key_len = @cipher.key_len
if key.size != key_len
error_message = "#{cipher_name}: keylength does not match"
error { error_message }
raise error_message
end
encrypt ? @cipher.encrypt : @cipher.decrypt
@cipher.key = key

@nonce = {
fixed: nil,
invocation_counter: 0
}
end

def update_cipher_mac(payload, _sequence_number)
#
# --- RFC 5647 7.3 ---
# When using AES-GCM with secure shell, the packet_length field is to
# be treated as additional authenticated data, not as plaintext.
#
length_data = [payload.bytesize].pack('N')

cipher.auth_data = length_data

encrypted_data = cipher.update(payload) << cipher.final

mac = cipher.auth_tag

incr_nonce
length_data + encrypted_data + mac
end

#
# --- RFC 5647 ---
# uint32 packet_length; // 0 <= packet_length < 2^32
#
def read_length(data, _sequence_number)
data.unpack1('N')
end

#
# --- RFC 5647 ---
# In AES-GCM secure shell, the inputs to the authenticated encryption
# are:
# PT (Plain Text)
# byte padding_length; // 4 <= padding_length < 256
# byte[n1] payload; // n1 = packet_length-padding_length-1
# byte[n2] random_padding; // n2 = padding_length
# AAD (Additional Authenticated Data)
# uint32 packet_length; // 0 <= packet_length < 2^32
# IV (Initialization Vector)
# As described in section 7.1.
# BK (Block Cipher Key)
# The appropriate Encryption Key formed during the Key Exchange.
#
def read_and_mac(data, mac, _sequence_number)
# The authentication tag will be placed in the MAC field at the end of the packet

# OpenSSL does not verify auth tag length
# GCM mode allows arbitrary sizes for the auth_tag up to 128 bytes and a single
# byte allows authentication to pass. If single byte auth tags are possible
# an attacker would require no more than 256 attempts to forge a valid tag.
#
raise 'incorrect auth_tag length' unless mac.to_s.length == mac_length

packet_length = data.unpack1('N')

cipher.auth_tag = mac.to_s
cipher.auth_data = [packet_length].pack('N')

result = cipher.update(data[4...]) << cipher.final
incr_nonce
result
end

def mac_length
16
end

def block_size
16
end

def self.block_size
16
end

#
# --- RFC 5647 ---
# N_MIN minimum nonce (IV) length 12 octets
# N_MAX maximum nonce (IV) length 12 octets
#
def iv_len
12
end

#
# --- RFC 5288 ---
# Each value of the nonce_explicit MUST be distinct for each distinct
# invocation of the GCM encrypt function for any fixed key. Failure to
# meet this uniqueness requirement can significantly degrade security.
# The nonce_explicit MAY be the 64-bit sequence number.
#
# --- RFC 5116 ---
# (2.1) Applications that can generate distinct nonces SHOULD use the nonce
# formation method defined in Section 3.2, and MAY use any
# other method that meets the uniqueness requirement.
#
# (3.2) The following method to construct nonces is RECOMMENDED.
#
# <- variable -> <- variable ->
# - - - - - - - - - - - - - -
# | fixed | counter |
#
# Initial octets consist of a fixed field and final octets consist of a
# Counter field. Implementations SHOULD support 12-octet nonces in which
# the Counter field is four octets long.
# The Counter fields of successive nonces form a monotonically increasing
# sequence, when those fields are regarded as unsignd integers in network
# byte order.
# The Counter part SHOULD be equal to zero for the first nonce and increment
# by one for each successive nonce that is generated.
# The Fixed field MUST remain constant for all nonces that are generated for
# a given encryption device.
#
# --- RFC 5647 ---
# The invocation field is treated as a 64-bit integer and is increment after
# each invocation of AES-GCM to process a binary packet.
# AES-GCM produces a keystream in blocks of 16-octets that is used to
# encrypt the plaintext. This keystream is produced by encrypting the
# following 16-octet data structure:
#
# uint32 fixed; // 4 octets
# uint64 invocation_counter; // 8 octets
# unit32 block_counter; // 4 octets
#
# The block_counter is initially set to one (1) and increment as each block
# of key is produced.
#
# The reader is reminded that SSH requires that the data to be encrypted
# MUST be padded out to a multiple of the block size (16-octets for AES-GCM).
#
def incr_nonce
return if nonce[:fixed].nil?

nonce[:invocation_counter] = [nonce[:invocation_counter].to_s.unpack1('B*').to_i(2) + 1].pack('Q>*')

apply_nonce
end

def nonce=(iv_s)
return if nonce[:fixed]

nonce[:fixed] = iv_s[0...4]
nonce[:invocation_counter] = iv_s[4...12]

apply_nonce
end

def apply_nonce
cipher.iv = "#{nonce[:fixed]}#{nonce[:invocation_counter]}"
end

#
# --- RFC 5647 ---
# If AES-GCM is selected as the encryption algorithm for a given
# tunnel, AES-GCM MUST also be selected as the Message Authentication
# Code (MAC) algorithm. Conversely, if AES-GCM is selected as the MAC
# algorithm, it MUST also be selected as the encryption algorithm.
#
def implicit_mac?
true
end
end
end
# rubocop:enable Metrics/BlockLength
end
# rubocop:enable Metrics/AbcSize
end
end
end
16 changes: 16 additions & 0 deletions lib/net/ssh/transport/hmac/abstract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ module HMAC
# The base class of all OpenSSL-based HMAC algorithm wrappers.
class Abstract
class << self
def aead(*v)
@aead = false if !defined?(@aead)
if v.empty?
@aead = superclass.aead if @aead.nil? && superclass.respond_to?(:aead)
return @aead
elsif v.length == 1
@aead = v.first
else
raise ArgumentError, "wrong number of arguments (#{v.length} for 1)"
end
end

def etm(*v)
@etm = false if !defined?(@etm)
if v.empty?
Expand Down Expand Up @@ -57,6 +69,10 @@ def digest_class(*v)
end
end

def aead
self.class.aead
end

def etm
self.class.etm
end
Expand Down
Loading

0 comments on commit 6dde359

Please sign in to comment.