Skip to content
This repository has been archived by the owner on Oct 28, 2024. It is now read-only.

Add SHA224, SHA384, SHA512, AES192, AES256 support #70

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,18 @@ services:
- --v3-priv-key=maplesyrup
- --v3-priv-proto=DES
- --v3-user=unsafe
- --v3-user=authprivsha224aes
- --v3-auth-key=maplesyrup
- --v3-auth-proto=SHA224
- --v3-priv-key=maplesyrup
- --v3-priv-proto=AES
- --v3-user=authprivsha384aes192
- --v3-auth-key=maplesyrup
- --v3-auth-proto=SHA384
- --v3-priv-key=maplesyrup
- --v3-priv-proto=AES192
- --v3-user=authprivsha512aes256
- --v3-auth-key=maplesyrup
- --v3-auth-proto=SHA512
- --v3-priv-key=maplesyrup
- --v3-priv-proto=AES256
41 changes: 35 additions & 6 deletions lib/netsnmp/encryption/aes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,31 @@

module NETSNMP
module Encryption
# https://www.rfc-editor.org/rfc/rfc3826
# https://snmp.com/snmpv3/snmpv3_aes256.shtml
# Note: AES Blumental is not supported and not widely used
class AES
def initialize(priv_key, local: 0)
def initialize(priv_key, cipher: , local: 0)
@priv_key = priv_key
@local = local
@cipher = cipher
end

def encrypt(decrypted_data, engine_boots:, engine_time:)
cipher = OpenSSL::Cipher.new("aes-128-cfb")
cipher = case @cipher
when :aes, :aes128 then OpenSSL::Cipher.new("aes-128-cfb")
when :aes192 then OpenSSL::Cipher.new("aes-192-cfb")
when :aes256 then OpenSSL::Cipher.new("aes-256-cfb")
end

iv, salt = generate_encryption_key(engine_boots, engine_time)

cipher.encrypt
cipher.iv = iv
cipher.iv = case @cipher
when :aes, :aes128 then iv[0, 16]
when :aes192 then iv[0, 24]
when :aes256 then iv[0, 32]
end
cipher.key = aes_key

if (diff = decrypted_data.length % 8) != 0
Expand All @@ -29,14 +41,22 @@ def encrypt(decrypted_data, engine_boots:, engine_time:)
def decrypt(encrypted_data, salt:, engine_boots:, engine_time:)
raise Error, "invalid priv salt received" unless !salt.empty? && (salt.length % 8).zero?

cipher = OpenSSL::Cipher.new("aes-128-cfb")
cipher = case @cipher
when :aes, :aes128 then OpenSSL::Cipher.new("aes-128-cfb")
when :aes192 then OpenSSL::Cipher.new("aes-192-cfb")
when :aes256 then OpenSSL::Cipher.new("aes-256-cfb")
end
cipher.padding = 0

iv = generate_decryption_key(engine_boots, engine_time, salt)

cipher.decrypt
cipher.key = aes_key
cipher.iv = iv
cipher.iv = case @cipher
when :aes, :aes128 then iv[0..16]
when :aes192 then iv[0..24]
when :aes256 then iv[0..32]
end
decrypted_data = cipher.update(encrypted_data) + cipher.final

hlen, bodylen = OpenSSL::ASN1.traverse(decrypted_data) { |_, _, x, y, *| break x, y }
Expand All @@ -58,6 +78,11 @@ def generate_encryption_key(boots, time)
@local = @local == 0xffffffffffffffff ? 0 : @local + 1

iv = generate_decryption_key(boots, time, salt)
iv = case @cipher
when :aes, :aes128 then iv[0, 16]
when :aes192 then iv[0, 24]
when :aes256 then iv[0, 32]
end

[iv, salt]
end
Expand All @@ -74,7 +99,11 @@ def generate_decryption_key(boots, time, salt)
end

def aes_key
@priv_key[0, 16]
case @cipher
when :aes, :aes128 then @priv_key[0, 16]
when :aes192 then @priv_key[0, 24]
when :aes256 then @priv_key[0, 32]
end
end
end
end
Expand Down
15 changes: 6 additions & 9 deletions lib/netsnmp/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def initialize(**args)
end

def verify(stream, auth_param, security_level, security_parameters:)
security_parameters.verify(stream.sub(auth_param, authnone(security_parameters.auth_protocol).value), auth_param, security_level: security_level)
security_parameters.verify(stream.sub(auth_param, authnone(security_parameters).value), auth_param, security_level: security_level)
end

# @param [String] payload of an snmp v3 message which can be decoded
Expand Down Expand Up @@ -119,7 +119,7 @@ def encode(pdu, security_parameters:, engine_boots: 0, engine_time: 0)
log { "signing V3 message..." }
auth_salt = OpenSSL::ASN1::OctetString.new(signature).with_label(:auth)
log(level: 2) { auth_salt.to_hex }
none_der = authnone(security_parameters.auth_protocol).to_der
none_der = authnone(security_parameters).to_der
encoded[encoded.index(none_der), none_der.size] = auth_salt.to_der
log { Hexdump.dump(encoded) }
end
Expand All @@ -130,15 +130,12 @@ def encode(pdu, security_parameters:, engine_boots: 0, engine_time: 0)

# https://datatracker.ietf.org/doc/html/rfc7860#section-4.2.2 part 3
# https://datatracker.ietf.org/doc/html/rfc3414#section-6.3.2 part 3
def authnone(auth_protocol)
def authnone(security_parameters)
# https://datatracker.ietf.org/doc/html/rfc3414#section-3.1 part 8b
return OpenSSL::ASN1::OctetString.new("").with_label(:auth_mask) unless auth_protocol
return OpenSSL::ASN1::OctetString.new("").with_label(:auth_mask) unless security_parameters&.auth_protocol

# The digest in the msgAuthenticationParameters field is replaced by the 12 zero octets.
# 24 octets for sha256
number_of_octets = auth_protocol == :sha256 ? 24 : 12

OpenSSL::ASN1::OctetString.new("\x00" * number_of_octets).with_label(:auth_mask)
# The digest in the msgAuthenticationParameters field is replaced by zero octets.
OpenSSL::ASN1::OctetString.new("\x00" * security_parameters.digest_length).with_label(:auth_mask)
end
end
end
84 changes: 74 additions & 10 deletions lib/netsnmp/security_parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,36 @@ class SecurityParameters
IPAD = "\x36" * 64
OPAD = "\x5c" * 64

DIGEST_LENGTH = {
md5: 12,
sha: 12,
sha1: 12,
sha224: 16,
sha256: 24,
sha384: 32,
sha512: 48
}.freeze

AUTH_KEY_LENGTH = {
nil: 16,
md5: 16,
sha: 20,
sha1: 20,
sha224: 28,
sha256: 32,
sha384: 48,
sha512: 64
}.freeze

PRIV_KEY_LENGTH = {
des: 16,
aes: 16,
aes128: 16,
aes192: 24,
aes256: 32,
nil: 16
}.freeze

# Timeliness is part of SNMP V3 Security
# The topic is described very nice here https://www.snmpsharpnet.com/?page_id=28
# https://www.ietf.org/rfc/rfc2574.txt 1.4.1 Timeliness
Expand Down Expand Up @@ -129,11 +159,17 @@ def sign(message)
return unless @auth_protocol

key = auth_key.dup
case @auth_protocol
when :sha224, :sha256, :sha384, :sha512 then sign_sha2(key, message)
when :md5, :sha, :sha1 then sign_md5_or_sha(key, message)
end
end

# SHA256 => https://datatracker.ietf.org/doc/html/rfc7860#section-4.2.2
# The 24 first octets of HMAC are taken as the computed MAC value
return OpenSSL::HMAC.digest("SHA256", key, message)[0, 24] if @auth_protocol == :sha256
def sign_sha2(key, message)
OpenSSL::HMAC.digest(@auth_protocol.to_s.upcase, key, message)[0, digest_length]
end

def sign_md5_or_sha(key, message)
# MD5 => https://datatracker.ietf.org/doc/html/rfc3414#section-6.3.2
# SHA1 => https://datatracker.ietf.org/doc/html/rfc3414#section-7.3.2
key << ("\x00" * (@auth_protocol == :md5 ? 48 : 44))
Expand All @@ -147,7 +183,7 @@ def sign(message)
digest.reset
digest << (k2 + d1)
# The 12 first octets of the digest are taken as the computed MAC value
digest.digest[0, 12]
digest.digest[0, digest_length]
end

# @param [String] stream the encoded incoming payload
Expand All @@ -170,17 +206,29 @@ def must_revalidate?
(Process.clock_gettime(Process::CLOCK_MONOTONIC, :second) - @timeliness) >= TIMELINESS_THRESHOLD
end

def digest_length
DIGEST_LENGTH[@auth_protocol] || raise(Error, "unknown digest length for #{@auth_protocol}")
end

private

def auth_key_length
AUTH_KEY_LENGTH[@auth_protocol]
end

def priv_key_length
PRIV_KEY_LENGTH[@priv_protocol]
end

def auth_key
@auth_key ||= localize_key(@auth_pass_key)
@auth_key ||= localize_auth_key(@auth_pass_key)
end

def priv_key
@priv_key ||= localize_key(@priv_pass_key)
@priv_key ||= localize_priv_key(@priv_pass_key)
end

def localize_key(key)
def localize_auth_key(key)
digest.reset
digest << key
digest << @engine_id
Expand All @@ -189,6 +237,15 @@ def localize_key(key)
digest.digest
end

# AES-192, AES-256 require longer localized keys,
# which require adding of subsequent localized_priv_keys based on the previous until the length is satisfied
# The only hint to this is available in the python implementation called pysnmp
def localize_priv_key(key)
dig = localize_auth_key(key)
dig += localize_auth_key(passkey(dig)) while dig.length < priv_key_length
dig
end

def passkey(password)
digest.reset
password_index = 0
Expand All @@ -205,24 +262,31 @@ def passkey(password)
end

dig = digest.digest
dig = dig[0, 16] if @auth_protocol == :md5
dig = dig[0, auth_key_length] if @auth_protocol
dig || ""
end

def digest
@digest ||= case @auth_protocol
when :md5 then OpenSSL::Digest.new("MD5")
when :sha then OpenSSL::Digest.new("SHA1")
when :sha, :sha1 then OpenSSL::Digest.new("SHA1")
when :sha224 then OpenSSL::Digest.new("SHA224")
when :sha256 then OpenSSL::Digest.new("SHA256")
when :sha384 then OpenSSL::Digest.new("SHA384")
when :sha512 then OpenSSL::Digest.new("SHA512")
else
raise Error, "unsupported auth protocol: #{@auth_protocol}"
end
end



def encryption
@encryption ||= case @priv_protocol
when :des then Encryption::DES.new(priv_key)
when :aes then Encryption::AES.new(priv_key)
when :aes then Encryption::AES.new(priv_key, cipher: @priv_protocol)
when :aes192 then Encryption::AES.new(priv_key, cipher: @priv_protocol)
when :aes256 then Encryption::AES.new(priv_key, cipher: @priv_protocol)
end
end

Expand Down
30 changes: 30 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,36 @@
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
end
end
context "auth in sha224, encrypting in aes" do
let(:user_options) do
{ username: "authprivsha224aes", auth_password: "maplesyrup",
auth_protocol: :sha224, priv_password: "maplesyrup",
priv_protocol: :aes }
end
it_behaves_like "an snmp client" do
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
end
end
context "auth in sha384, encrypting in aes192" do
let(:user_options) do
{ username: "authprivsha384aes192", auth_password: "maplesyrup",
auth_protocol: :sha384, priv_password: "maplesyrup",
priv_protocol: :aes192 }
end
it_behaves_like "an snmp client" do
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
end
end
context "auth in sha512, encrypting in aes256" do
let(:user_options) do
{ username: "authprivsha512aes256", auth_password: "maplesyrup",
auth_protocol: :sha512, priv_password: "maplesyrup",
priv_protocol: :aes256 }
end
it_behaves_like "an snmp client" do
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
end
end
end
end
end
Loading