Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
May 3, 2023
Merged
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
6 changes: 1 addition & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@

name: Tests

on:
push:
branches: [ master ]
pull_request:
branches: [ '**' ]
on: [push]

jobs:
tests:
Expand Down
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,18 @@ services:
- --v3-user=authsha
- --v3-auth-key=maplesyrup
- --v3-auth-proto=SHA
- --v3-user=authsha224
- --v3-auth-key=maplesyrup
- --v3-auth-proto=SHA224
- --v3-user=authsha384
- --v3-auth-key=maplesyrup
- --v3-auth-proto=SHA384
- --v3-user=authsha256
- --v3-auth-key=maplesyrup
- --v3-auth-proto=SHA256
- --v3-user=authsha512
- --v3-auth-key=maplesyrup
- --v3-auth-proto=SHA512
- --v3-user=authprivshaaes
- --v3-auth-key=maplesyrup
- --v3-auth-proto=SHA
Expand All @@ -61,3 +70,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 @@ -3,18 +3,30 @@
module NETSNMP
module Encryption
class AES
def initialize(priv_key, local: 0)
def initialize(priv_key, cipher:, local: 0)
@priv_key = priv_key
@local = local
# https://www.rfc-editor.org/rfc/rfc3826
# https://snmp.com/snmpv3/snmpv3_aes256.shtml
# Note: AES Blumental is not supported and not widely used
@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
8 changes: 7 additions & 1 deletion lib/netsnmp/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,13 @@ def authnone(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
number_of_octets = case auth_protocol
when :sha512 then 48
when :sha384 then 32
when :sha256 then 24
when :sha224 then 16
else 12
end

OpenSSL::ASN1::OctetString.new("\x00" * number_of_octets).with_label(:auth_mask)
end
Expand Down
57 changes: 47 additions & 10 deletions lib/netsnmp/security_parameters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,25 @@ def sign(message)

key = auth_key.dup

# 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
case @auth_protocol
when :sha224
return OpenSSL::HMAC.digest("SHA224", key, message)[0, 16]
when :sha256
# The 24 first octets of HMAC are taken as the computed MAC value
# SHA256 => https://datatracker.ietf.org/doc/html/rfc7860#section-4.2.2
return OpenSSL::HMAC.digest("SHA256", key, message)[0, 24]
when :sha384
return OpenSSL::HMAC.digest("SHA384", key, message)[0, 32]
when :sha512
return OpenSSL::HMAC.digest("SHA512", key, message)[0, 48]
when :md5
# MD5 => https://datatracker.ietf.org/doc/html/rfc3414#section-6.3.2
key << ("\x00" * 48)
when :sha, :sha1
# SHA1 => https://datatracker.ietf.org/doc/html/rfc3414#section-7.3.2
key << ("\x00" * 44)
end

# 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))
k1 = key.xor(IPAD)
k2 = key.xor(OPAD)

Expand Down Expand Up @@ -177,7 +189,20 @@ def auth_key
end

def priv_key
@priv_key ||= localize_key(@priv_pass_key)
@priv_key ||= begin
key = localize_key(@priv_pass_key)
# 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
priv_key_size = case @priv_protocol
when :aes256 then 32
when :aes192 then 24
else 16
end

key += localize_key(passkey(key)) while key.size < priv_key_size
key
end
end

def localize_key(key)
Expand Down Expand Up @@ -205,15 +230,27 @@ def passkey(password)
end

dig = digest.digest
dig = dig[0, 16] if @auth_protocol == :md5
dig_size = case @auth_protocol
when :sha512 then 64
when :sha384 then 48
when :sha256 then 32
when :sha224 then 28
when :sha1, :sha then 20
else 16
end

dig[0, dig_size]
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
Expand All @@ -222,7 +259,7 @@ def digest
def encryption
@encryption ||= case @priv_protocol
when :des then Encryption::DES.new(priv_key)
when :aes then Encryption::AES.new(priv_key)
when :aes, :aes192, :aes256 then Encryption::AES.new(priv_key, cipher: @priv_protocol)
end
end

Expand Down
2 changes: 1 addition & 1 deletion netsnmp.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Gem::Specification.new do |gem|
"documentation_uri" => "https://www.rubydoc.info/github/HoneyryderChuck/ruby-netsnmp",
"source_code_uri" => "https://github.com/HoneyryderChuck/ruby-netsnmp",
"homepage_uri" => "https://github.com/HoneyryderChuck/ruby-netsnmp",
"rubygems_mfa_required" => "true",
"rubygems_mfa_required" => "true"
}

# Manifest
Expand Down
3 changes: 2 additions & 1 deletion sig/encryption/aes.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ module NETSNMP

@priv_key: String
@local: Integer
@cipher: Symbol

def encrypt: (String decrypted_data, engine_boots: Integer, engine_time: Integer) -> [String, String]

def decrypt: (String encrypted_data, salt: String, engine_boots: Integer, engine_time: Integer) -> String

private

def initialize: (String priv_key, ?local: Integer) -> untyped
def initialize: (String priv_key, ?local: Integer, ?cipher: Symbol) -> untyped

def generate_encryption_key: (Integer boots, Integer time) -> [String, String]

Expand Down
57 changes: 57 additions & 0 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,24 @@
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
end
end
context "speaking sha224" do
let(:user_options) do
{ username: "authsha224", security_level: :auth_no_priv,
auth_password: "maplesyrup", auth_protocol: :sha224 }
end
it_behaves_like "an snmp client" do
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
end
end
context "speaking sha384" do
let(:user_options) do
{ username: "authsha384", security_level: :auth_no_priv,
auth_password: "maplesyrup", auth_protocol: :sha384 }
end
it_behaves_like "an snmp client" do
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
end
end
context "speaking sha256" do
let(:user_options) do
{ username: "authsha256", security_level: :auth_no_priv,
Expand All @@ -166,6 +184,15 @@
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
end
end
context "speaking sha512" do
let(:user_options) do
{ username: "authsha512", security_level: :auth_no_priv,
auth_password: "maplesyrup", auth_protocol: :sha512 }
end
it_behaves_like "an snmp client" do
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
end
end
end
context "with an auth priv policy" do
context "auth in md5, encrypting in des" do
Expand Down Expand Up @@ -220,6 +247,36 @@
it_behaves_like "an snmp client" do
let(:protocol_options) { version_options.merge(user_options).merge(extra_options) }
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
Expand Down
Loading