diff --git a/Cargo.toml b/Cargo.toml index c7ffb42..1837cee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "snmp2" -version = "0.4.14" +version = "0.4.15" edition = "2021" description = "SNMP v1/v2/v3 sync/async client library with traps and MIB support" authors = ["Serhij S. "] @@ -21,12 +21,29 @@ snmptools = { version = "^0.1.2", optional = true } tokio = { version = "1.47", features = ["net"], optional = true } openssl = { version = "0.10", optional = true } +md-5 = { version = "0.10", optional = true } +sha1 = { version = "0.10", optional = true } +digest = { version = "0.10", optional = true } +sha2 = { version = "0.10", optional = true } +aes = { version = "0.8", optional = true } +cipher = { version = "0.4", optional = true } +des = { version = "0.8", optional = true } +hmac = { version = "0.12", optional = true } +cbc = { version = "0.1", optional = true } +cfb-mode = { version = "0.8", optional = true } +rand = { version = "0.9", optional = true } + [dev-dependencies] -tokio = { version = "=1.47" } +tokio = { version = "1" } [features] +default = ["tokio", "v3_rust"] + mibs = ["dep:snmptools"] tokio = ["dep:tokio"] -v3 = ["openssl"] +v3 = [] heap_buffers = [] -full = ["mibs", "tokio", "v3"] +full = ["mibs", "tokio", "v3_rust"] + +v3_openssl = ["v3", "openssl"] +v3_rust = ["v3", "md-5", "sha1", "digest", "sha2", "aes", "cipher", "des", "hmac", "cbc", "cfb-mode", "rand"] diff --git a/README.md b/README.md index 75593a8..7cd060a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Supports: - Synchronous/Asynchronous requests - UDP transport - MIBs (with `mibs` feature, requires `libnetsnmp`) -- SNMP v3 (requires `v3` feature) +- SNMP v3 (enable `v3_openssl` or `v3_rust` feature) # Examples @@ -139,7 +139,7 @@ let socket = UdpSocket::bind("0.0.0.0:1161").unwrap(); socket.send_to(&bytes, target_addr).unwrap(); ``` -### With SNMPv3 (requires `v3` feature) +### With SNMPv3 (enable `v3_openssl` or `v3_rust`) When using SNMPv3, you need to provide the security context to convert the PDU to bytes: @@ -210,9 +210,13 @@ assert_eq!(snmp_oid, snmp_oid2); # SNMPv3 -- Requires `v3` crate feature. +- Requires enabling one of the features: `v3_openssl` or `v3_rust`. + - `v3_openssl`: uses OpenSSL for hashing/HMAC and symmetric encryption. + - `v3_rust`: uses pure Rust crypto crates for hashing/HMAC and encryption. -- All cryptographic algorithms are provided by [openssl](https://www.openssl.org/). +- Cryptographic algorithms are provided by the selected backend: + - `v3_openssl`: [openssl](https://www.openssl.org/) + - `v3_rust`: pure Rust crates [Rust Crypto](https://github.com/RustCrypto): (`md-5`, `sha1`, `sha2`, `hmac`, `aes`, `des`, etc.) - For authentication, supports: MD5 (RFC3414), SHA1 (RFC3414) and non-standard SHA224, SHA256, SHA384, SHA512. @@ -221,8 +225,22 @@ assert_eq!(snmp_oid, snmp_oid2); AES192-CFB, AES256-CFB. Additional/different AES modes are not supported and may require patching the crate. -Note: DES legacy encryption may be disabled in openssl by default or even not -supported at all. Refer to the library documentation how to enable it. +Note: For `v3_openssl`, DES legacy encryption may be disabled in OpenSSL by default +or not supported at all. Refer to the library documentation how to enable it. + +### Feature selection examples + +Pure Rust backend: + +```shell +cargo add snmp2 --features v3_rust +``` + +OpenSSL backend (Windows-friendly vendored build): + +```shell +cargo add snmp2 --features "v3_openssl,openssl/vendored" +``` ## Example @@ -261,21 +279,22 @@ loop { } ``` -## Building +## Building (`v3_openssl`) -In case of problems (e.g. with [cross-rs](https://github.com/cross-rs/cross)), -add `openssl` with `vendored` feature: +When using the `v3_openssl` backend, in case of problems (e.g. with +[cross-rs](https://github.com/cross-rs/cross)), add `openssl` with `vendored` feature: ```shell cargo add openssl --features vendored ``` -## FIPS-140 support +## FIPS-140 support (`v3_openssl`) -The crate uses openssl cryptography only and becomes FIPS-140 compliant as soon +When using the `v3_openssl` backend, the crate becomes FIPS-140 compliant as soon as FIPS mode is activated in `openssl`. Refer to the [openssl crate](https://docs.rs/openssl) crate and [openssl library](https://www.openssl.org/) documentation for more details. +The `v3_rust` backend does not rely on OpenSSL and is not FIPS-certified. ## MSRV diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..2ff266d --- /dev/null +++ b/build.rs @@ -0,0 +1,15 @@ +fn main() { + let has_v3 = std::env::var("CARGO_FEATURE_V3").is_ok(); + let has_v3_openssl = std::env::var("CARGO_FEATURE_V3_OPENSSL").is_ok(); + let has_v3_rust = std::env::var("CARGO_FEATURE_V3_RUST").is_ok(); + + match (has_v3_openssl, has_v3_rust) { + (true, false) | (false, true) => {} // OK + (true, true) => panic!("feature_v3_openssl and feature_v3_rust are mutually exclusive!"), + (false, false) => { + if has_v3 { + panic!("feature_v3_openssl or feature_v3_rust is required") + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 50510bb..8209c4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ pub mod snmp; mod syncsession; #[cfg(feature = "v3")] pub mod v3; -#[cfg(feature = "v3")] +#[cfg(feature = "v3_openssl")] pub use openssl; pub use syncsession::SyncSession; #[cfg(feature = "tokio")] @@ -141,7 +141,7 @@ impl fmt::Display for Error { } } -#[cfg(feature = "v3")] +#[cfg(feature = "v3_openssl")] impl From for Error { fn from(err: openssl::error::ErrorStack) -> Error { Error::Crypto(err.to_string()) diff --git a/src/v3.rs b/src/v3.rs index a829771..f8313a7 100644 --- a/src/v3.rs +++ b/src/v3.rs @@ -1,11 +1,23 @@ use std::{fmt, time::Instant}; +#[cfg(feature = "v3_openssl")] use openssl::{ hash::{Hasher, MessageDigest}, pkey::PKey, sign::Signer, }; +#[cfg(feature = "v3_rust")] +use { + aes::{Aes128, Aes192, Aes256}, + cbc::{Decryptor as CbcDecryptor, Encryptor as CbcEncryptor}, + cfb_mode::{Decryptor as CfbDecryptor, Encryptor as CfbEncryptor}, + cipher::{AsyncStreamCipher, BlockDecryptMut, BlockEncryptMut, KeyIvInit}, + des::Des, + digest::{Digest, DynDigest}, + hmac::{Hmac, Mac}, +}; + use crate::{ asn1, pdu::{self, Buf}, @@ -324,10 +336,55 @@ impl Security { if self.engine_id().is_empty() { return Err(Error::AuthFailure(AuthErrorKind::SecurityNotReady)); } - let pkey = PKey::hmac(&self.authoritative_state.auth_key)?; - let mut signer = Signer::new(self.auth_protocol.digest(), &pkey)?; - signer.update(data)?; - signer.sign_to_vec().map_err(Error::from) + #[cfg(feature = "v3_openssl")] + { + let pkey = PKey::hmac(&self.authoritative_state.auth_key)?; + let mut signer = Signer::new(self.auth_protocol.digest(), &pkey)?; + signer.update(data)?; + signer.sign_to_vec().map_err(Error::from) + } + #[cfg(feature = "v3_rust")] + { + let key = &self.authoritative_state.auth_key; + match self.auth_protocol { + AuthProtocol::Md5 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha1 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha224 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha256 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha384 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha512 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + } + } } pub(crate) fn update_key(&mut self) -> Result<()> { @@ -388,7 +445,13 @@ impl Security { fn encrypt_des(&self, data: &[u8]) -> Result<(Vec, Vec)> { let mut salt = [0; 8]; salt[..4].copy_from_slice(&u32::try_from(self.engine_boots())?.to_be_bytes()); + #[cfg(feature = "v3_openssl")] openssl::rand::rand_bytes(&mut salt[4..])?; + #[cfg(feature = "v3_rust")] + { + use rand::Rng; + rand::rng().fill(&mut salt[4..]); + } if data.is_empty() { return Ok((vec![], salt.to_vec())); @@ -400,65 +463,124 @@ impl Security { let des_key = &self.authoritative_state.priv_key[..8]; let pre_iv = &self.authoritative_state.priv_key[8..16]; - let cipher = openssl::symm::Cipher::des_cbc(); let mut iv = [0; 8]; for (i, (a, b)) in pre_iv.iter().zip(salt.iter()).enumerate() { iv[i] = a ^ b; } - let mut encrypted = vec![0; data.len() + cipher.block_size()]; - let mut crypter = - openssl::symm::Crypter::new(cipher, openssl::symm::Mode::Encrypt, des_key, Some(&iv))?; - let mut count = crypter.update(data, &mut encrypted)?; + #[cfg(feature = "v3_openssl")] + { + let cipher = openssl::symm::Cipher::des_cbc(); + let mut encrypted = vec![0; data.len() + cipher.block_size()]; + let mut crypter = openssl::symm::Crypter::new( + cipher, + openssl::symm::Mode::Encrypt, + des_key, + Some(&iv), + )?; + let mut count = crypter.update(data, &mut encrypted)?; - if count < encrypted.len() { - count += crypter.finalize(&mut encrypted[count..])?; + if count < encrypted.len() { + count += crypter.finalize(&mut encrypted[count..])?; + } + + encrypted.truncate(count); + Ok((encrypted, salt.to_vec())) } + #[cfg(feature = "v3_rust")] + { + let mut encryptor = CbcEncryptor::::new_from_slices(des_key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + + // PKCS#7 padding + let len = data.len(); + let pad_len = 8 - (len % 8); + let mut padded = Vec::with_capacity(len + pad_len); + padded.extend_from_slice(data); + padded.extend(std::iter::repeat(pad_len as u8).take(pad_len)); + + for chunk in padded.chunks_mut(8) { + let block = cipher::generic_array::GenericArray::from_mut_slice(chunk); + encryptor.encrypt_block_mut(block); + } - encrypted.truncate(count); - Ok((encrypted, salt.to_vec())) + Ok((padded, salt.to_vec())) + } } - fn encrypt_aes( - &self, - data: &[u8], - cipher: openssl::symm::Cipher, - block_size: usize, - ) -> Result<(Vec, Vec)> { - let iv_len = cipher - .iv_len() - .ok_or_else(|| Error::Crypto("no IV len".to_owned()))?; - + fn encrypt_aes(&self, data: &[u8], key_len: usize) -> Result<(Vec, Vec)> { + let iv_len = 16; let mut iv = Vec::with_capacity(iv_len); iv.extend_from_slice(&u32::try_from(self.engine_boots())?.to_be_bytes()); iv.extend_from_slice(&u32::try_from(self.engine_time())?.to_be_bytes()); let salt_pos = iv.len(); iv.resize(iv_len, 0); + #[cfg(feature = "v3_openssl")] openssl::rand::rand_bytes(&mut iv[salt_pos..])?; - let key_len = cipher.key_len(); + #[cfg(feature = "v3_rust")] + { + use rand::Rng; + rand::rng().fill(&mut iv[salt_pos..]); + } if self.authoritative_state.priv_key.len() < key_len { return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)); } - let mut crypter = openssl::symm::Crypter::new( - cipher, - openssl::symm::Mode::Encrypt, - &self.authoritative_state.priv_key[..key_len], - Some(&iv), - )?; + #[cfg(feature = "v3_openssl")] + { + let cipher = match key_len { + 16 => openssl::symm::Cipher::aes_128_cfb128(), + 24 => openssl::symm::Cipher::aes_192_cfb128(), + 32 => openssl::symm::Cipher::aes_256_cfb128(), + _ => return Err(Error::Crypto("Invalid key len".into())), + }; + + let mut crypter = openssl::symm::Crypter::new( + cipher, + openssl::symm::Mode::Encrypt, + &self.authoritative_state.priv_key[..key_len], + Some(&iv), + )?; + + let mut encrypted = vec![0; data.len() + 16]; + let mut count = crypter.update(data, &mut encrypted)?; - let mut encrypted = vec![0; data.len() + block_size]; - let mut count = crypter.update(data, &mut encrypted)?; + if count < encrypted.len() { + count += crypter.finalize(&mut encrypted[count..])?; + } - if count < encrypted.len() { - count += crypter.finalize(&mut encrypted[count..])?; + encrypted.truncate(count); + Ok((encrypted, iv[salt_pos..].to_vec())) } + #[cfg(feature = "v3_rust")] + { + let key = &self.authoritative_state.priv_key[..key_len]; + let mut encrypted = data.to_vec(); + + match key_len { + 16 => { + let encryptor = CfbEncryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + encryptor.encrypt(&mut encrypted); + } + 24 => { + let encryptor = CfbEncryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + encryptor.encrypt(&mut encrypted); + } + 32 => { + let encryptor = CfbEncryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + encryptor.encrypt(&mut encrypted); + } + _ => return Err(Error::Crypto("Invalid key len".into())), + } - encrypted.truncate(count); - Ok((encrypted, iv[salt_pos..].to_vec())) + Ok((encrypted, iv[salt_pos..].to_vec())) + } } /// encrypts the data @@ -477,12 +599,13 @@ impl Security { match cipher_kind { Cipher::Des => self.encrypt_des(data), - Cipher::Aes128 => self.encrypt_aes(data, openssl::symm::Cipher::aes_128_cfb128(), 16), - Cipher::Aes192 => self.encrypt_aes(data, openssl::symm::Cipher::aes_192_cfb128(), 24), - Cipher::Aes256 => self.encrypt_aes(data, openssl::symm::Cipher::aes_256_cfb128(), 32), + Cipher::Aes128 => self.encrypt_aes(data, 16), + Cipher::Aes192 => self.encrypt_aes(data, 24), + Cipher::Aes256 => self.encrypt_aes(data, 32), } } + #[cfg(feature = "v3_openssl")] fn decrypt_data_to_plain_buf( &mut self, mut crypter: openssl::symm::Crypter, @@ -509,13 +632,6 @@ impl Security { return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)); } - let cipher = openssl::symm::Cipher::des_cbc(); - let block_size = 8; - - if encrypted.len() % block_size > 0 { - return Err(Error::AuthFailure(AuthErrorKind::PayloadLengthMismatch)); - } - let des_key = &self.authoritative_state.priv_key[..8]; let pre_iv = &self.authoritative_state.priv_key[8..16]; let mut iv = [0; 8]; @@ -524,22 +640,58 @@ impl Security { iv[i] = a ^ b; } - let crypter = - openssl::symm::Crypter::new(cipher, openssl::symm::Mode::Decrypt, des_key, Some(&iv))?; - self.decrypt_data_to_plain_buf(crypter, block_size, encrypted) - } + #[cfg(feature = "v3_openssl")] + { + let cipher = openssl::symm::Cipher::des_cbc(); + let block_size = 8; - fn decrypt_aes( - &mut self, - encrypted: &[u8], - priv_params: &[u8], - cipher: openssl::symm::Cipher, - block_size: usize, - ) -> Result<()> { - let iv_len = cipher - .iv_len() - .ok_or_else(|| Error::Crypto("no IV len".to_owned()))?; + if encrypted.len() % block_size > 0 { + return Err(Error::AuthFailure(AuthErrorKind::PayloadLengthMismatch)); + } + + let crypter = openssl::symm::Crypter::new( + cipher, + openssl::symm::Mode::Decrypt, + des_key, + Some(&iv), + )?; + self.decrypt_data_to_plain_buf(crypter, block_size, encrypted) + } + #[cfg(feature = "v3_rust")] + { + let block_size = 8; + if encrypted.len() % block_size > 0 { + return Err(Error::AuthFailure(AuthErrorKind::PayloadLengthMismatch)); + } + + let mut decryptor = CbcDecryptor::::new_from_slices(des_key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + + self.plain_buf.clear(); + self.plain_buf.extend_from_slice(encrypted); + for chunk in self.plain_buf.chunks_mut(8) { + let block = cipher::generic_array::GenericArray::from_mut_slice(chunk); + decryptor.decrypt_block_mut(block); + } + + if let Some(&pad_len) = self.plain_buf.last() { + let pad_len = pad_len as usize; + if pad_len > 0 && pad_len <= block_size && pad_len <= self.plain_buf.len() { + let new_len = self.plain_buf.len() - pad_len; + self.plain_buf.truncate(new_len); + Ok(()) + } else { + Err(Error::Crypto("Invalid padding".into())) + } + } else { + Ok(()) + } + } + } + + fn decrypt_aes(&mut self, encrypted: &[u8], priv_params: &[u8], key_len: usize) -> Result<()> { + let iv_len = 16; let mut iv = Vec::with_capacity(iv_len); iv.extend_from_slice(&u32::try_from(self.engine_boots())?.to_be_bytes()); iv.extend_from_slice(&u32::try_from(self.engine_time())?.to_be_bytes()); @@ -549,19 +701,54 @@ impl Security { return Err(Error::AuthFailure(AuthErrorKind::PrivLengthMismatch)); } - let key_len = cipher.key_len(); if self.authoritative_state.priv_key.len() < key_len { return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)); } - let crypter = openssl::symm::Crypter::new( - cipher, - openssl::symm::Mode::Decrypt, - &self.authoritative_state.priv_key[..key_len], - Some(&iv), - )?; + #[cfg(feature = "v3_openssl")] + { + let cipher = match key_len { + 16 => openssl::symm::Cipher::aes_128_cfb128(), + 24 => openssl::symm::Cipher::aes_192_cfb128(), + 32 => openssl::symm::Cipher::aes_256_cfb128(), + _ => return Err(Error::Crypto("Invalid key len".into())), + }; + + let crypter = openssl::symm::Crypter::new( + cipher, + openssl::symm::Mode::Decrypt, + &self.authoritative_state.priv_key[..key_len], + Some(&iv), + )?; - self.decrypt_data_to_plain_buf(crypter, block_size, encrypted) + self.decrypt_data_to_plain_buf(crypter, 16, encrypted) + } + #[cfg(feature = "v3_rust")] + { + let key = &self.authoritative_state.priv_key[..key_len]; + self.plain_buf.clear(); + self.plain_buf.extend_from_slice(encrypted); + + match key_len { + 16 => { + let decryptor = CfbDecryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + decryptor.decrypt(&mut self.plain_buf); + } + 24 => { + let decryptor = CfbDecryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + decryptor.decrypt(&mut self.plain_buf); + } + 32 => { + let decryptor = CfbDecryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + decryptor.decrypt(&mut self.plain_buf); + } + _ => return Err(Error::Crypto("Invalid key len".into())), + } + Ok(()) + } } /// decrypts the data, the result is stored in `self.plain_buf` @@ -576,24 +763,9 @@ impl Security { match cipher_kind { Cipher::Des => self.decrypt_des(encrypted, priv_params), - Cipher::Aes128 => self.decrypt_aes( - encrypted, - priv_params, - openssl::symm::Cipher::aes_128_cfb128(), - 16, - ), - Cipher::Aes192 => self.decrypt_aes( - encrypted, - priv_params, - openssl::symm::Cipher::aes_192_cfb128(), - 24, - ), - Cipher::Aes256 => self.decrypt_aes( - encrypted, - priv_params, - openssl::symm::Cipher::aes_256_cfb128(), - 32, - ), + Cipher::Aes128 => self.decrypt_aes(encrypted, priv_params, 16), + Cipher::Aes192 => self.decrypt_aes(encrypted, priv_params, 24), + Cipher::Aes256 => self.decrypt_aes(encrypted, priv_params, 32), } } } @@ -610,6 +782,25 @@ pub enum Auth { }, } +#[cfg(feature = "v3_rust")] +pub struct Hasher(Box); + +#[cfg(feature = "v3_rust")] +impl Hasher { + pub fn new(d: Box) -> Result { + Ok(Self(d)) + } + + pub fn update(&mut self, data: &[u8]) -> Result<()> { + self.0.update(data); + Ok(()) + } + + pub fn finish(&mut self) -> Result> { + Ok(self.0.finalize_reset().to_vec()) + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum AuthProtocol { Md5, @@ -622,8 +813,24 @@ pub enum AuthProtocol { impl AuthProtocol { fn create_hasher(self) -> Result { - Hasher::new(self.digest()).map_err(Into::into) + #[cfg(feature = "v3_openssl")] + { + Hasher::new(self.digest()).map_err(Into::into) + } + #[cfg(feature = "v3_rust")] + { + let d: Box = match self { + AuthProtocol::Md5 => Box::new(md5::Md5::new()), + AuthProtocol::Sha1 => Box::new(sha1::Sha1::new()), + AuthProtocol::Sha224 => Box::new(sha2::Sha224::new()), + AuthProtocol::Sha256 => Box::new(sha2::Sha256::new()), + AuthProtocol::Sha384 => Box::new(sha2::Sha384::new()), + AuthProtocol::Sha512 => Box::new(sha2::Sha512::new()), + }; + Hasher::new(d) + } } + #[cfg(feature = "v3_openssl")] fn digest(self) -> MessageDigest { match self { AuthProtocol::Md5 => MessageDigest::md5(),