Skip to content

Commit

Permalink
HMAC-SHA-96 authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
dvolodin7 committed Jan 17, 2024
1 parent 04b0ddc commit ddaa75a
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 11 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ md5 = "0.7"
nom = "7.1"
pyo3 = {version = "0.20", features = ["extension-module"]}
rand = "0.8"
sha1 = "0.10"
socket2 = {version = "0.5", features = ["all"]}

[dev-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ async with Snmpd(), SnmpSession(addr="127.0.0.1", port=10161) as session:
* Clean async API.
* SNMP v1/v2c/v3 support.
* SNMP v3 User Security Model:
* Authentication: HMAC-MD5-96, HMAC-SHA-96 (work in progress).
* Authentication: HMAC-MD5-96, HMAC-SHA-96.
* Privacy: work in progress.
* High-performance.
* Zero-copy BER parsing.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ async with Snmpd(), SnmpSession(addr="127.0.0.1", port=10161) as session:
* Clean async API.
* SNMP v1/v2c/v3 support.
* SNMP v3 User Security Model:
* Authentication: HMAC-MD5-96, HMAC-SHA-96 (work in progress).
* Authentication: HMAC-MD5-96, HMAC-SHA-96.
* Privacy: work in progress.
* High-performance.
* Zero-copy BER parsing.
Expand Down
17 changes: 10 additions & 7 deletions src/auth/md5.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// ------------------------------------------------------------------------

use super::SnmpAuth;
use md5::Context;
use md5::{compute, Context};

pub struct Md5(Vec<u8>);

Expand All @@ -16,10 +16,13 @@ impl Md5 {
}
}

const KEY_LENGTH: usize = 16;
const PADDED_LENGTH: usize = 64;
const REST_LENGTH: usize = PADDED_LENGTH - KEY_LENGTH;
const IPAD_VALUE: u8 = 0x36;
const OPAD_VALUE: u8 = 0x5c;
const IPAD_REST: [u8; 48] = [IPAD_VALUE; 48];
const OPAD_REST: [u8; 48] = [OPAD_VALUE; 48];
const IPAD_REST: [u8; REST_LENGTH] = [IPAD_VALUE; REST_LENGTH];
const OPAD_REST: [u8; REST_LENGTH] = [OPAD_VALUE; REST_LENGTH];
const SIGN_SIZE: usize = 12;

impl SnmpAuth for Md5 {
Expand All @@ -28,8 +31,8 @@ impl SnmpAuth for Md5 {
result.extend_from_slice(self.0.as_ref());
result.extend_from_slice(engine_id.as_ref());
result.extend_from_slice(self.0.as_ref());
let digest: [u8; 16] = md5::compute(result).into();
self.0.resize(16, 0);
let digest: [u8; KEY_LENGTH] = compute(result).into();
self.0.resize(KEY_LENGTH, 0);
self.0.clone_from_slice(&digest);
}
fn sign(&self, whole_msg: &mut [u8], offset: usize) {
Expand All @@ -51,7 +54,7 @@ impl SnmpAuth for Md5 {
// * append whole message
ctx1.consume(&whole_msg);
// get MD5
let d1: [u8; 16] = ctx1.compute().into();
let d1: [u8; KEY_LENGTH] = ctx1.compute().into();
// d) obtain OPAD by replicating the octet 0x5C 64 times;
// >>> Really not necessary
// e) obtain K2 by XORing extendedAuthKey with OPAD.
Expand All @@ -67,7 +70,7 @@ impl SnmpAuth for Md5 {
ctx2.consume(OPAD_REST);
// * append previous digest
ctx2.consume(d1);
let d2: [u8; 16] = ctx2.compute().into(); // use only 12 octets
let d2: [u8; KEY_LENGTH] = ctx2.compute().into(); // use only 12 octets
whole_msg[offset..offset + SIGN_SIZE].copy_from_slice(&d2[0..SIGN_SIZE]);
}
}
Expand Down
41 changes: 41 additions & 0 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@
// ------------------------------------------------------------------------

mod md5;
mod sha1;

pub use crate::error::{SnmpError, SnmpResult};
pub use md5::Md5;
pub use sha1::Sha1;

pub const NO_AUTH: u8 = 0;
pub const MD5_AUTH: u8 = 1;
pub const SHA1_AUTH: u8 = 2;

pub enum AuthKey {
NoAuth,
Md5(Md5),
Sha1(Sha1),
}

pub trait SnmpAuth {
Expand All @@ -34,6 +38,7 @@ impl AuthKey {
match code {
NO_AUTH => Ok(AuthKey::NoAuth),
MD5_AUTH => Ok(AuthKey::Md5(Md5::new(key))),
SHA1_AUTH => Ok(AuthKey::Sha1(Sha1::new(key))),
_ => Err(SnmpError::InvalidVersion(code)),
}
}
Expand All @@ -44,12 +49,14 @@ impl AuthKey {
match self {
AuthKey::NoAuth => &PLACEHOLDER0,
AuthKey::Md5(_) => &PLACEHOLDER12,
AuthKey::Sha1(_) => &PLACEHOLDER12,
}
}
pub fn localize(&mut self, engine_id: &[u8]) {
match self {
AuthKey::NoAuth => {}
AuthKey::Md5(x) => x.localize(engine_id),
AuthKey::Sha1(x) => x.localize(engine_id),
}
}
pub fn sign(&self, whole_msg: &mut [u8]) -> SnmpResult<()> {
Expand All @@ -62,6 +69,13 @@ impl AuthKey {
}
None => Err(SnmpError::InvalidData),
},
AuthKey::Sha1(x) => match AuthKey::find_placeholder12_offset(whole_msg) {
Some(idx) => {
x.sign(whole_msg, idx + 2);
Ok(())
}
None => Err(SnmpError::InvalidData),
},
}
}
fn find_placeholder12_offset(input: &[u8]) -> Option<usize> {
Expand Down Expand Up @@ -105,4 +119,31 @@ mod tests {
assert_eq!(whole_msg, expected);
Ok(())
}
#[test]
fn test_sha1_sign() -> SnmpResult<()> {
let mut whole_msg = [
48, 119, 2, 1, 3, 48, 16, 2, 4, 31, 120, 150, 153, 2, 2, 5, 220, 4, 1, 1, 2, 1, 3, 4,
47, 48, 45, 4, 13, 128, 0, 31, 136, 4, 50, 55, 103, 83, 56, 54, 116, 100, 2, 1, 0, 2,
1, 0, 4, 6, 117, 115, 101, 114, 50, 48, 4, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
0, 48, 47, 4, 13, 128, 0, 31, 136, 4, 50, 55, 103, 83, 56, 54, 116, 100, 4, 0, 160, 28,
2, 4, 80, 85, 225, 64, 2, 1, 0, 2, 1, 0, 48, 14, 48, 12, 6, 8, 43, 6, 1, 2, 1, 1, 4, 0,
5, 0,
];
let expected = [
48, 119, 2, 1, 3, 48, 16, 2, 4, 31, 120, 150, 153, 2, 2, 5, 220, 4, 1, 1, 2, 1, 3, 4,
47, 48, 45, 4, 13, 128, 0, 31, 136, 4, 50, 55, 103, 83, 56, 54, 116, 100, 2, 1, 0, 2,
1, 0, 4, 6, 117, 115, 101, 114, 50, 48, 4, 12, 8, 126, 173, 253, 67, 91, 150, 217, 19,
212, 52, 193, 4, 0, 48, 47, 4, 13, 128, 0, 31, 136, 4, 50, 55, 103, 83, 56, 54, 116,
100, 4, 0, 160, 28, 2, 4, 80, 85, 225, 64, 2, 1, 0, 2, 1, 0, 48, 14, 48, 12, 6, 8, 43,
6, 1, 2, 1, 1, 4, 0, 5, 0,
];
let master_key = vec![117u8, 115, 101, 114, 50, 48, 107, 101, 121]; // user20key
let engine_id = [128, 0, 31, 136, 4, 50, 55, 103, 83, 56, 54, 116, 100];
let mut key = Sha1::new(master_key);
key.localize(&engine_id);
let auth_key = AuthKey::Sha1(key);
auth_key.sign(&mut whole_msg)?;
assert_eq!(whole_msg, expected);
Ok(())
}
}
103 changes: 103 additions & 0 deletions src/auth/sha1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// ------------------------------------------------------------------------
// Gufo SNMP: SNMP v3 Sha1 Auth
// ------------------------------------------------------------------------
// Copyright (C) 2023-24, Gufo Labs
// See LICENSE.md for details
// ------------------------------------------------------------------------

use super::SnmpAuth;
use sha1::Digest;

pub struct Sha1(Vec<u8>);

impl Sha1 {
pub fn new(key: Vec<u8>) -> Sha1 {
Sha1(key)
}
}

const KEY_LENGTH: usize = 20;
const PADDED_LENGTH: usize = 64;
const REST_LENGTH: usize = PADDED_LENGTH - KEY_LENGTH;
const IPAD_VALUE: u8 = 0x36;
const OPAD_VALUE: u8 = 0x5c;
const IPAD_REST: [u8; REST_LENGTH] = [IPAD_VALUE; REST_LENGTH];
const OPAD_REST: [u8; REST_LENGTH] = [OPAD_VALUE; REST_LENGTH];
const SIGN_SIZE: usize = 12;

impl SnmpAuth for Sha1 {
fn localize(&mut self, engine_id: &[u8]) {
let mut result = Vec::with_capacity(2 * self.0.len() + engine_id.len());
result.extend_from_slice(self.0.as_ref());
result.extend_from_slice(engine_id.as_ref());
result.extend_from_slice(self.0.as_ref());
let mut hasher = sha1::Sha1::new();
hasher.update(result);
let digest: [u8; KEY_LENGTH] = hasher.finalize().into();
self.0.resize(KEY_LENGTH, 0);
self.0.clone_from_slice(&digest);
}
fn sign(&self, whole_msg: &mut [u8], offset: usize) {
let mut ctx1 = sha1::Sha1::new();
// RFC-3414, pp. 7.3.1. Processing an outgoing message
// a) extend the authKey to 64 octets by appending 44 zero octets;
// save it as extendedAuthKey
// >>> Really not necessary
// b) obtain IPAD by replicating the octet 0x36 64 times;
// >>> need only rest
// c) obtain K1 by XORing extendedAuthKey with IPAD;
// 3) Prepend K1 to the wholeMsg and calculate MD5 digest over it according to [RFC1321].
// Instead:
// * append xored key
let k1: Vec<u8> = self.0.iter().map(|&x| x ^ IPAD_VALUE).collect();
ctx1.update(k1);
// * append precalculated rest of IPAD
ctx1.update(IPAD_REST);
// * append whole message
ctx1.update(&whole_msg);
// get Sha1
let d1: [u8; KEY_LENGTH] = ctx1.finalize().into();
// d) obtain OPAD by replicating the octet 0x5C 64 times;
// >>> Really not necessary
// e) obtain K2 by XORing extendedAuthKey with OPAD.
// 4) Prepend K2 to the result of the step 4 and calculate MD5 digest
// over it according to [RFC1321]. Take the first 12 octets of the
// final digest - this is Message Authentication Code (MAC).
// Instead:
// * append xored key
let mut ctx2 = sha1::Sha1::new();
let k2: Vec<u8> = self.0.iter().map(|&x| x ^ OPAD_VALUE).collect();
ctx2.update(k2);
// * append precalculated rest of OPAD
ctx2.update(OPAD_REST);
// * append previous digest
ctx2.update(d1);
let d2: [u8; KEY_LENGTH] = ctx2.finalize().into(); // use only 12 octets
whole_msg[offset..offset + SIGN_SIZE].copy_from_slice(&d2[0..SIGN_SIZE]);
}
}

#[cfg(test)]
mod tests {
use super::*;

// #[test]
// fn test() -> SnmpResult<()> {
// let mut whole_msg = [
// 48u8, 119, 2, 1, 3, 48, 16, 2, 4, 31, 120, 150, 153, 2, 2, 5, 220, 4, 1, 1, 2, 1, 3, 4,
// 47, 48, 45, 4, 13, 128, 0, 31, 136, 4, 50, 55, 103, 83, 56, 54, 116, 100, 2, 1, 0, 2,
// 1, 0, 4, 6, 117, 115, 101, 114, 49, 48, 4, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
// 0, 48, 47, 4, 13, 128, 0, 31, 136, 4, 50, 55, 103, 83, 56, 54, 116, 100, 4, 0, 160, 28,
// 2, 4, 80, 85, 225, 64, 2, 1, 0, 2, 1, 0, 48, 14, 48, 12, 6, 8, 43, 6, 1, 2, 1, 1, 4, 0,
// 5, 0,
// ];
// let expected = [18, 138, 173, 156, 223, 188, 26, 178, 137, 113, 25, 22];
// let master_key = vec![117u8, 115, 101, 114, 49, 48, 107, 101, 121]; // user10key
// let engine_id = [128, 0, 31, 136, 4, 50, 55, 103, 83, 56, 54, 116, 100];
// let mut key = Md5::new(master_key);
// key.localize(&engine_id);
// let r = key.get_signature(&whole_msg);
// assert_eq!(r, expected);
// Ok(())
// }
}
2 changes: 2 additions & 0 deletions src/gufo/snmp/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ class Md5Key(BaseAuthKey):
"""MD5 Key."""

AUTH_ALG = 1
KEY_LENGTH = 16


class Sha1Key(BaseAuthKey):
"""SHA-1 Key."""

AUTH_ALG = 2
KEY_LENGTH = 20


@dataclass
Expand Down
4 changes: 2 additions & 2 deletions tests/test_snmp.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# Gufo Labs modules
from gufo.snmp import NoSuchInstance, SnmpSession, SnmpVersion, ValueType
from gufo.snmp.snmpd import Snmpd
from gufo.snmp.user import Md5Key, User
from gufo.snmp.user import Md5Key, Sha1Key, User

SNMPD_ADDRESS = "127.0.0.1"
SNMPD_PORT = random.randint(52000, 53999)
Expand All @@ -28,7 +28,7 @@
SNMP_USERS = [
User(name="user00"),
User(name="user10", auth_key=Md5Key(b"user10key")),
# User(name="user20", auth_key=Sha1Key(b"user20key")),
User(name="user20", auth_key=Sha1Key(b"user20key")),
]

V1 = [{"version": SnmpVersion.v1, "community": SNMP_COMMUNITY}]
Expand Down

0 comments on commit ddaa75a

Please sign in to comment.