diff --git a/Cargo.toml b/Cargo.toml index 34b590a..4407b18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ asn1-rs = "0.6" snmptools = { version = "^0.1.2", optional = true } tokio = { version = "1.47", features = ["net"], optional = true } openssl = { version = "0.10", optional = true } +aws-lc-rs = { version = "1", optional = true } [dev-dependencies] tokio = { version = "=1.47" } @@ -28,5 +29,7 @@ tokio = { version = "=1.47" } mibs = ["dep:snmptools"] tokio = ["dep:tokio"] v3 = ["openssl"] +v3_aws_lc_rs = ["aws-lc-rs"] heap_buffers = [] full = ["mibs", "tokio", "v3"] +full_aws_lc_rs = ["mibs", "tokio", "v3_aws_lc_rs"] diff --git a/README.md b/README.md index 988fc8e..8723ab5 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,16 @@ assert_eq!(snmp_oid, snmp_oid2); # SNMPv3 +There are two implementations of SNMPv3 available: + +- `v3` feature: Uses OpenSSL for cryptographic operations +- `v3_aws_lc_rs` feature: Uses aws-lc-rs (FIPS 140-3 certified) for cryptographic operations + +**Note:** The `v3` and `v3_aws_lc_rs` features are mutually exclusive. Only one can +be enabled at a time. + +## SNMPv3 with OpenSSL (`v3` feature) + * Requires `v3` crate feature. * All cryptographic algorithms are provided by [openssl](https://www.openssl.org/). @@ -175,7 +185,7 @@ assert_eq!(snmp_oid, snmp_oid2); 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. -## Example +### Example Authentication: SHA1, encryption: AES128-CFB @@ -212,7 +222,7 @@ loop { } ``` -## Building +### Building In case of problems (e.g. with [cross-rs](https://github.com/cross-rs/cross)), add `openssl` with `vendored` feature: @@ -221,13 +231,64 @@ add `openssl` with `vendored` feature: cargo add openssl --features vendored ``` -## FIPS-140 support +### FIPS-140 support (OpenSSL) The crate uses openssl cryptography only and 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. +## SNMPv3 with aws-lc-rs (`v3_aws_lc_rs` feature) + +* Requires `v3_aws_lc_rs` crate feature. + +* All cryptographic algorithms are provided by [aws-lc-rs](https://crates.io/crates/aws-lc-rs), + which is FIPS 140-3 certified (certificate #4816). + +* For authentication, supports: SHA1 (RFC3414) and non-standard SHA224, SHA256, + SHA384, SHA512. **MD5 is NOT supported** (not FIPS compliant). + +* For privacy, supports: AES128-CFB (RFC3826) and non-standard AES192-CFB, + AES256-CFB. **DES is NOT supported** (not FIPS compliant). + +**Note:** If you need MD5 or DES support for legacy devices, use the `v3` +feature (OpenSSL-based) instead. + +### Example + +Authentication: SHA256, encryption: AES256-CFB + +```rust,ignore +use snmp2::{SyncSession, v3_aws_lc_rs, Oid}; +use std::time::Duration; + +let security = v3_aws_lc_rs::Security::new(b"public", b"secure") + .with_auth_protocol(v3_aws_lc_rs::AuthProtocol::Sha256) + .with_auth(v3_aws_lc_rs::Auth::AuthPriv { + cipher: v3_aws_lc_rs::Cipher::Aes256, + privacy_password: b"secure-encrypt".to_vec(), + }); +let mut sess = + SyncSession::new_v3("192.168.1.1:161", Some(Duration::from_secs(2)), 0, security).unwrap(); +sess.init().unwrap(); +loop { + let res = match sess.get(&Oid::from(&[1, 3, 6, 1, 2, 1, 1, 3, 0]).unwrap()) { + Ok(r) => r, + Err(snmp2::Error::AuthUpdated) => continue, + Err(e) => panic!("{}", e), + }; + println!("{} {:?}", res.version().unwrap(), res.varbinds); + std::thread::sleep(Duration::from_secs(1)); +} +``` + +### FIPS 140-3 support (aws-lc-rs) + +The `v3_aws_lc_rs` feature uses aws-lc-rs which has FIPS 140-3 certification +(certificate #4816). This provides FIPS compliance without requiring OpenSSL +FIPS mode configuration. The aws-lc-rs library is maintained by AWS and is the +cryptographic foundation for AWS services. + ## MSRV 1.83.0 diff --git a/src/asyncsession.rs b/src/asyncsession.rs index daf27b0..a6cc3fa 100644 --- a/src/asyncsession.rs +++ b/src/asyncsession.rs @@ -13,6 +13,9 @@ use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket}; #[cfg(feature = "v3")] use crate::v3; +#[cfg(feature = "v3_aws_lc_rs")] +use crate::v3_aws_lc_rs; + /// Asynchronous SNMP client pub struct AsyncSession { version: Version, @@ -26,6 +29,8 @@ pub struct AsyncSession { recv_buf: Box<[u8]>, #[cfg(feature = "v3")] security: Option, + #[cfg(feature = "v3_aws_lc_rs")] + security: Option, } impl AsyncSession { @@ -66,6 +71,21 @@ impl AsyncSession { Ok(session) } + #[cfg(feature = "v3_aws_lc_rs")] + pub async fn new_v3( + destination: SA, + starting_req_id: i32, + security: v3_aws_lc_rs::Security, + ) -> io::Result + where + SA: ToSocketAddrs, + { + let mut session = Self::new(Version::V3, destination, &[], starting_req_id).await?; + session.community = security.username.clone(); + session.security = Some(security); + Ok(session) + } + async fn new( version: Version, destination: SA, @@ -105,10 +125,12 @@ impl AsyncSession { recv_buf: vec![0u8; BUFFER_SIZE].into_boxed_slice(), #[cfg(feature = "v3")] security: None, + #[cfg(feature = "v3_aws_lc_rs")] + security: None, }) } - #[cfg(not(feature = "v3"))] + #[cfg(not(any(feature = "v3", feature = "v3_aws_lc_rs")))] #[allow(clippy::unused_self, clippy::unused_async)] pub async fn init(&mut self) -> Result<()> { Ok(()) @@ -138,6 +160,32 @@ impl AsyncSession { Ok(()) } + #[cfg(feature = "v3_aws_lc_rs")] + pub async fn init(&mut self) -> Result<()> { + if let Some(ref mut security) = self.security { + security.reset_engine_id(); + security.reset_engine_counters(); + // send a request to get the engine id + let req_id = self.req_id.0; + v3_aws_lc_rs::build_init(req_id, &mut self.send_pdu); + self.req_id += Wrapping(1); + if let Err(e) = Pdu::from_bytes_inner( + Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf).await?, + Some(security), + ) { + if e != Error::AuthUpdated { + return Err(e); + } + } + if security.need_init() { + return Err(Error::AuthFailure( + v3_aws_lc_rs::AuthErrorKind::NotAuthenticated, + )); + } + } + Ok(()) + } + /// Checks if KeyExtension affects this session privacy and then re-inits session with different KeyExtension /// /// # Returns @@ -159,7 +207,30 @@ impl AsyncSession { Ok(None) } - #[cfg(not(feature = "v3"))] + /// Checks if KeyExtension affects this session privacy and then re-inits session with different KeyExtension + /// + /// # Returns + /// 'Ok(Some(new_key_extension))' When new_key_extension method was set + /// 'Ok(None)' When security disabled + /// or Auth type is not AuthPriv + /// or when Auth-Priv pair is not the one that needs key extension + /// or when KeyExtension was not set for the session. + /// 'Err(error)' when 'init()' failed with error returned from 'init()' + #[cfg(feature = "v3_aws_lc_rs")] + pub async fn try_another_key_extension_method( + &mut self, + ) -> Result> { + if let Some(ref mut security) = self.security { + if let Some(new_method) = security.another_key_extension_method() { + security.authoritative_state = v3_aws_lc_rs::AuthoritativeState::default(); + self.init().await?; + return Ok(Some(new_method)); + } + } + Ok(None) + } + + #[cfg(not(any(feature = "v3", feature = "v3_aws_lc_rs")))] #[allow(clippy::unused_self)] fn prepare(&mut self) {} @@ -170,6 +241,13 @@ impl AsyncSession { } } + #[cfg(feature = "v3_aws_lc_rs")] + fn prepare(&mut self) { + if let Some(ref mut security) = self.security { + security.correct_authoritative_engine_time(); + } + } + async fn send_and_recv<'a>( socket: &UdpSocket, pdu: &pdu::Buf, @@ -194,12 +272,12 @@ impl AsyncSession { req_id, oid, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf).await?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); @@ -216,12 +294,12 @@ impl AsyncSession { req_id, oids, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf).await?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); @@ -238,12 +316,12 @@ impl AsyncSession { req_id, oid, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf).await?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); @@ -267,12 +345,12 @@ impl AsyncSession { non_repeaters, max_repetitions, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf).await?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); @@ -289,12 +367,12 @@ impl AsyncSession { req_id, values, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf).await?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); diff --git a/src/lib.rs b/src/lib.rs index 2a360a5..d654af9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,10 @@ #![ doc = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/", "README.md" ) ) ] #![allow(unknown_lints, clippy::doc_markdown)] +// Ensure v3 and v3_aws_lc_rs features are mutually exclusive +#[cfg(all(feature = "v3", feature = "v3_aws_lc_rs"))] +compile_error!("Features `v3` and `v3_aws_lc_rs` are mutually exclusive. Choose one."); + use std::fmt; pub mod asn1; @@ -14,6 +18,10 @@ mod syncsession; pub mod v3; #[cfg(feature = "v3")] pub use openssl; +#[cfg(feature = "v3_aws_lc_rs")] +pub mod v3_aws_lc_rs; +#[cfg(feature = "v3_aws_lc_rs")] +pub use aws_lc_rs; pub use syncsession::SyncSession; #[cfg(feature = "tokio")] mod asyncsession; @@ -94,14 +102,17 @@ pub enum Error { /// Buffer overflow. BufferOverflow, - /// Authentication failure + /// Authentication failure (OpenSSL-based v3) #[cfg(feature = "v3")] AuthFailure(v3::AuthErrorKind), - /// OpenSSL errors - #[cfg(feature = "v3")] + /// Authentication failure (aws-lc-rs-based v3_aws_lc_rs) + #[cfg(feature = "v3_aws_lc_rs")] + AuthFailure(v3_aws_lc_rs::AuthErrorKind), + /// Cryptographic engine errors + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] Crypto(String), /// Security context has been updated, repeat the request - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] AuthUpdated, /// Socket send error. @@ -128,9 +139,11 @@ impl fmt::Display for Error { Error::BufferOverflow => write!(f, "Buffer overflow"), #[cfg(feature = "v3")] Error::AuthFailure(err) => write!(f, "Authentication failure: {}", err), - #[cfg(feature = "v3")] + #[cfg(feature = "v3_aws_lc_rs")] + Error::AuthFailure(err) => write!(f, "Authentication failure: {}", err), + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] Error::Crypto(e) => write!(f, "Cryptographic engine error: {}", e), - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] Error::AuthUpdated => { write!(f, "Security context has been updated, repeat the request") } diff --git a/src/pdu.rs b/src/pdu.rs index 59837ab..a6364da 100644 --- a/src/pdu.rs +++ b/src/pdu.rs @@ -1,5 +1,7 @@ #[cfg(feature = "v3")] use crate::v3; +#[cfg(feature = "v3_aws_lc_rs")] +use crate::v3_aws_lc_rs; use crate::{ asn1::{self, AsnReader}, snmp, Error, MessageType, Oid, Result, Value, Varbinds, Version, BUFFER_SIZE, @@ -311,6 +313,7 @@ pub(crate) fn build( max_repetitions: u32, buf: &mut Buf, #[cfg(feature = "v3")] security: Option<&v3::Security>, + #[cfg(feature = "v3_aws_lc_rs")] security: Option<&v3_aws_lc_rs::Security>, ) -> Result<()> { #[cfg(feature = "v3")] if version == Version::V3 { @@ -324,6 +327,18 @@ pub(crate) fn build( security, ); } + #[cfg(feature = "v3_aws_lc_rs")] + if version == Version::V3 { + return v3_aws_lc_rs::build( + ident, + req_id, + values, + non_repeaters, + max_repetitions, + buf, + security, + ); + } buf.reset(); buf.push_sequence(|buf| { build_inner(req_id, ident, values, max_repetitions, non_repeaters, buf); @@ -382,6 +397,7 @@ pub(crate) fn build_get( oid: &Oid, buf: &mut Buf, #[cfg(feature = "v3")] security: Option<&v3::Security>, + #[cfg(feature = "v3_aws_lc_rs")] security: Option<&v3_aws_lc_rs::Security>, ) -> Result<()> { build( version, @@ -394,6 +410,8 @@ pub(crate) fn build_get( buf, #[cfg(feature = "v3")] security, + #[cfg(feature = "v3_aws_lc_rs")] + security, ) } @@ -404,6 +422,7 @@ pub(crate) fn build_get_many( oids: &[&Oid], buf: &mut Buf, #[cfg(feature = "v3")] security: Option<&v3::Security>, + #[cfg(feature = "v3_aws_lc_rs")] security: Option<&v3_aws_lc_rs::Security>, ) -> Result<()> { build( version, @@ -419,6 +438,8 @@ pub(crate) fn build_get_many( buf, #[cfg(feature = "v3")] security, + #[cfg(feature = "v3_aws_lc_rs")] + security, ) } @@ -429,6 +450,7 @@ pub(crate) fn build_getnext( oid: &Oid, buf: &mut Buf, #[cfg(feature = "v3")] security: Option<&v3::Security>, + #[cfg(feature = "v3_aws_lc_rs")] security: Option<&v3_aws_lc_rs::Security>, ) -> Result<()> { build( version, @@ -441,6 +463,8 @@ pub(crate) fn build_getnext( buf, #[cfg(feature = "v3")] security, + #[cfg(feature = "v3_aws_lc_rs")] + security, ) } @@ -454,6 +478,7 @@ pub(crate) fn build_getbulk( max_repetitions: u32, buf: &mut Buf, #[cfg(feature = "v3")] security: Option<&v3::Security>, + #[cfg(feature = "v3_aws_lc_rs")] security: Option<&v3_aws_lc_rs::Security>, ) -> Result<()> { build( version, @@ -469,6 +494,8 @@ pub(crate) fn build_getbulk( buf, #[cfg(feature = "v3")] security, + #[cfg(feature = "v3_aws_lc_rs")] + security, ) } @@ -479,6 +506,7 @@ pub(crate) fn build_set( values: &[(&Oid, Value)], buf: &mut Buf, #[cfg(feature = "v3")] security: Option<&v3::Security>, + #[cfg(feature = "v3_aws_lc_rs")] security: Option<&v3_aws_lc_rs::Security>, ) -> Result<()> { build( version, @@ -491,6 +519,8 @@ pub(crate) fn build_set( buf, #[cfg(feature = "v3")] security, + #[cfg(feature = "v3_aws_lc_rs")] + security, ) } @@ -504,7 +534,7 @@ pub struct Pdu<'a> { pub error_index: u32, pub varbinds: Varbinds<'a>, pub v1_trap_info: Option>, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] pub v3_msg_id: i32, } @@ -548,7 +578,7 @@ impl<'a> Pdu<'a> { specific_trap: specific_code, timestamp, }), - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] v3_msg_id: 0, }) } @@ -556,7 +586,7 @@ impl<'a> Pdu<'a> { pub fn from_bytes(bytes: &'a [u8]) -> Result> { Self::from_bytes_inner( bytes, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] None, ) } @@ -571,9 +601,20 @@ impl<'a> Pdu<'a> { } } + #[cfg(feature = "v3_aws_lc_rs")] + pub fn from_bytes_with_security( + bytes: &'a [u8], + security: Option<&'a mut v3_aws_lc_rs::Security>, + ) -> Result> { + { + Self::from_bytes_inner(bytes, security) + } + } + pub(crate) fn from_bytes_inner( bytes: &'a [u8], #[cfg(feature = "v3")] security: Option<&'a mut v3::Security>, + #[cfg(feature = "v3_aws_lc_rs")] security: Option<&'a mut v3_aws_lc_rs::Security>, ) -> Result> { let seq = AsnReader::from_bytes(bytes).read_raw(asn1::TYPE_SEQUENCE)?; let mut rdr = AsnReader::from_bytes(seq); @@ -593,7 +634,16 @@ impl<'a> Pdu<'a> { } return Err(Error::AuthFailure(v3::AuthErrorKind::SecurityNotProvided)); } - #[cfg(not(feature = "v3"))] + #[cfg(feature = "v3_aws_lc_rs")] + { + if let Some(security) = security { + return Self::parse_v3_aws_lc_rs(bytes, rdr, security); + } + return Err(Error::AuthFailure( + v3_aws_lc_rs::AuthErrorKind::SecurityNotProvided, + )); + } + #[cfg(not(any(feature = "v3", feature = "v3_aws_lc_rs")))] { return Err(Error::UnsupportedVersion); } @@ -637,7 +687,7 @@ impl<'a> Pdu<'a> { error_index: u32::try_from(error_index)?, varbinds, v1_trap_info: None, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] v3_msg_id: 0, }) } diff --git a/src/syncsession.rs b/src/syncsession.rs index e639f9f..1a7410c 100644 --- a/src/syncsession.rs +++ b/src/syncsession.rs @@ -13,6 +13,9 @@ use crate::{ #[cfg(feature = "v3")] use crate::v3; +#[cfg(feature = "v3_aws_lc_rs")] +use crate::v3_aws_lc_rs; + /// Synchronous SNMP client pub struct SyncSession { version: Version, @@ -23,6 +26,8 @@ pub struct SyncSession { recv_buf: [u8; BUFFER_SIZE], #[cfg(feature = "v3")] security: Option, + #[cfg(feature = "v3_aws_lc_rs")] + security: Option, } impl SyncSession { @@ -78,6 +83,22 @@ impl SyncSession { Ok(session) } + #[cfg(feature = "v3_aws_lc_rs")] + pub fn new_v3( + destination: SA, + timeout: Option, + starting_req_id: i32, + security: v3_aws_lc_rs::Security, + ) -> io::Result + where + SA: ToSocketAddrs, + { + let mut session = Self::new(Version::V3, destination, &[], timeout, starting_req_id)?; + session.community = security.username.clone(); + session.security = Some(security); + Ok(session) + } + fn new( version: Version, destination: SA, @@ -110,6 +131,8 @@ impl SyncSession { recv_buf: [0; BUFFER_SIZE], #[cfg(feature = "v3")] security: None, + #[cfg(feature = "v3_aws_lc_rs")] + security: None, }) } @@ -125,6 +148,18 @@ impl SyncSession { Ok(self) } + #[cfg(feature = "v3_aws_lc_rs")] + pub fn with_security(mut self, mut security: v3_aws_lc_rs::Security) -> Result { + security.username = self.community.clone(); + if !security.authentication_password.is_empty() + || !security.authoritative_state.engine_id.is_empty() + { + security.update_key()?; + } + self.security = Some(security); + Ok(self) + } + fn send_and_recv<'a>( socket: &UdpSocket, pdu: &pdu::Buf, @@ -140,7 +175,7 @@ impl SyncSession { } } - #[cfg(not(feature = "v3"))] + #[cfg(not(any(feature = "v3", feature = "v3_aws_lc_rs")))] pub fn init(&mut self) -> Result<()> { Ok(()) } @@ -169,6 +204,32 @@ impl SyncSession { Ok(()) } + #[cfg(feature = "v3_aws_lc_rs")] + pub fn init(&mut self) -> Result<()> { + if let Some(ref mut security) = self.security { + security.reset_engine_id(); + security.reset_engine_counters(); + // send a request to get the engine id + let req_id = self.req_id.0; + v3_aws_lc_rs::build_init(req_id, &mut self.send_pdu); + self.req_id += Wrapping(1); + if let Err(e) = Pdu::from_bytes_inner( + Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf)?, + Some(security), + ) { + if e != Error::AuthUpdated { + return Err(e); + } + } + if security.need_init() { + return Err(Error::AuthFailure( + v3_aws_lc_rs::AuthErrorKind::NotAuthenticated, + )); + } + } + Ok(()) + } + /// Checks if KeyExtension affects this session privacy and then re-inits session with different KeyExtension /// /// # Returns @@ -190,7 +251,28 @@ impl SyncSession { Ok(None) } - #[cfg(not(feature = "v3"))] + /// Checks if KeyExtension affects this session privacy and then re-inits session with different KeyExtension + /// + /// # Returns + /// 'Ok(Some(new_key_extension))' When new_key_extension method was set + /// 'Ok(None)' When security disabled + /// or Auth type is not AuthPriv + /// or when Auth-Priv pair is not the one that needs key extension + /// or when KeyExtension was not set for the session. + /// 'Err(error)' when 'init()' failed with error returned from 'init()' + #[cfg(feature = "v3_aws_lc_rs")] + pub fn try_another_key_extension_method(&mut self) -> Result> { + if let Some(ref mut security) = self.security { + if let Some(new_method) = security.another_key_extension_method() { + security.authoritative_state = v3_aws_lc_rs::AuthoritativeState::default(); + self.init()?; + return Ok(Some(new_method)); + } + } + Ok(None) + } + + #[cfg(not(any(feature = "v3", feature = "v3_aws_lc_rs")))] #[allow(clippy::unused_self)] fn prepare(&mut self) {} @@ -201,6 +283,13 @@ impl SyncSession { } } + #[cfg(feature = "v3_aws_lc_rs")] + fn prepare(&mut self) { + if let Some(ref mut security) = self.security { + security.correct_authoritative_engine_time(); + } + } + pub fn get(&mut self, oid: &Oid) -> Result { self.prepare(); let req_id = self.req_id.0; @@ -210,12 +299,12 @@ impl SyncSession { req_id, oid, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf)?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); @@ -232,12 +321,12 @@ impl SyncSession { req_id, oids, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf)?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); @@ -254,12 +343,12 @@ impl SyncSession { req_id, oid, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf)?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); @@ -283,12 +372,12 @@ impl SyncSession { non_repeaters, max_repetitions, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf)?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); @@ -305,12 +394,12 @@ impl SyncSession { req_id, values, &mut self.send_pdu, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_ref(), )?; let resp = Pdu::from_bytes_inner( Self::send_and_recv(&self.socket, &self.send_pdu, &mut self.recv_buf)?, - #[cfg(feature = "v3")] + #[cfg(any(feature = "v3", feature = "v3_aws_lc_rs"))] self.security.as_mut(), )?; self.req_id += Wrapping(1); diff --git a/src/tests.rs b/src/tests.rs index fbff59b..6ba05b4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -18,6 +18,8 @@ fn build_get_many_pdu() { &mut pdu, #[cfg(feature = "v3")] None, + #[cfg(feature = "v3_aws_lc_rs")] + None, ) .unwrap(); @@ -54,6 +56,8 @@ fn build_getnext_pdu() { &mut pdu, #[cfg(feature = "v3")] None, + #[cfg(feature = "v3_aws_lc_rs")] + None, ) .unwrap(); @@ -82,6 +86,8 @@ fn build_getbulk_pdu() { &mut pdu, #[cfg(feature = "v3")] None, + #[cfg(feature = "v3_aws_lc_rs")] + None, ) .unwrap(); @@ -110,6 +116,8 @@ fn build_reply_pdu() { &mut buf, #[cfg(feature = "v3")] None, + #[cfg(feature = "v3_aws_lc_rs")] + None, ) .unwrap(); let pdu = Pdu::from_bytes(&buf).unwrap(); diff --git a/src/v3_aws_lc_rs.rs b/src/v3_aws_lc_rs.rs new file mode 100644 index 0000000..1ffa908 --- /dev/null +++ b/src/v3_aws_lc_rs.rs @@ -0,0 +1,906 @@ +//! SNMPv3 support using aws-lc-rs (FIPS-140-3 compliant). +//! +//! This module provides SNMPv3 authentication and privacy using the aws-lc-rs +//! cryptographic library, which has FIPS 140-3 certification. +//! +//! ## Differences from the `v3` module (OpenSSL-based) +//! +//! This implementation does NOT support: +//! - MD5 authentication (not FIPS compliant) +//! - DES encryption (not FIPS compliant) +//! +//! For legacy MD5/DES support, use the `v3` feature instead. + +use std::{fmt, time::Instant}; + +use aws_lc_rs::{ + digest::{self, Context}, + hmac, + rand::fill, +}; + +use crate::{ + asn1, + pdu::{self, Buf}, + snmp::{self, V3_MSG_FLAGS_AUTH, V3_MSG_FLAGS_PRIVACY, V3_MSG_FLAGS_REPORTABLE}, + AsnReader, Error, MessageType, Oid, Pdu, Result, Value, Varbinds, Version, BUFFER_SIZE, +}; + +const ENGINE_TIME_WINDOW: i64 = 150; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum AuthErrorKind { + UnsupportedUSM, + EngineBootsMismatch, + EngineBootsNotProvided, + EngineTimeMismatch, + NotAuthenticated, + UsernameMismatch, + EngineIdMismatch, + SignatureMismatch, + MessageIdMismatch, + PrivLengthMismatch, + KeyLengthMismatch, + PayloadLengthMismatch, + ReplyNotEncrypted, + SecurityNotProvided, + SecurityNotReady, + KeyExtensionRequired, +} + +impl fmt::Display for AuthErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AuthErrorKind::UnsupportedUSM => write!(f, "Unsupported USM"), + AuthErrorKind::EngineBootsMismatch => write!(f, "Engine boots counter mismatch"), + AuthErrorKind::EngineTimeMismatch => write!(f, "Engine time counter mismatch"), + AuthErrorKind::NotAuthenticated => write!(f, "Not authenticated"), + AuthErrorKind::EngineBootsNotProvided => write!(f, "Engine boots counter not provided"), + AuthErrorKind::EngineIdMismatch => write!(f, "Engine ID mismatch"), + AuthErrorKind::UsernameMismatch => write!(f, "Username mismatch"), + AuthErrorKind::SignatureMismatch => write!(f, "HMAC signature mismatch"), + AuthErrorKind::MessageIdMismatch => write!(f, "Message ID mismatch"), + AuthErrorKind::PrivLengthMismatch => write!(f, "Privacy parameters length mismatch"), + AuthErrorKind::KeyLengthMismatch => write!(f, "Key length mismatch"), + AuthErrorKind::PayloadLengthMismatch => write!(f, "Payload length mismatch"), + AuthErrorKind::ReplyNotEncrypted => write!(f, "Not an encrypted reply"), + AuthErrorKind::SecurityNotProvided => write!(f, "Security parameters not provided"), + AuthErrorKind::SecurityNotReady => write!(f, "Security parameters not ready"), + AuthErrorKind::KeyExtensionRequired => { + write!(f, "Auth/Priv pair needs a key extension method") + } + } + } +} + +#[derive(Debug, Clone)] +pub(crate) struct AuthoritativeState { + auth_key: Vec, + priv_key: Vec, + pub(crate) engine_id: Vec, + engine_boots: i64, + engine_time: i64, + engine_time_current: i64, + start_time: Instant, +} + +impl Default for AuthoritativeState { + fn default() -> Self { + Self { + auth_key: Vec::new(), + priv_key: Vec::new(), + engine_id: Vec::new(), + engine_boots: 0, + engine_time: 0, + engine_time_current: 0, + start_time: Instant::now(), + } + } +} + +impl AuthoritativeState { + fn update_authoritative(&mut self, engine_boots: i64, engine_time: i64) { + self.engine_boots = engine_boots; + self.engine_time = engine_time; + self.start_time = Instant::now(); + } + + fn update_authoritative_engine_time(&mut self, engine_time: i64) { + self.engine_time = engine_time; + self.start_time = Instant::now(); + } + + fn correct_engine_time(&mut self) { + if self.engine_boots == 0 { + self.engine_time_current = 0; + return; + } + let max = i32::MAX.into(); + self.engine_time_current = + i64::try_from(self.start_time.elapsed().as_secs()).unwrap() + self.engine_time; + if self.engine_time_current >= max { + self.engine_time_current -= max; + self.engine_boots += 1; + } + } + + fn generate_key(&self, password: &[u8], auth_protocol: AuthProtocol) -> Result> { + let algorithm = auth_protocol.digest_algorithm(); + let mut ctx = Context::new(algorithm); + let mut password_index = 0; + let mut password_buf = vec![0u8; 64]; + for _ in 0..16384 { + for x in &mut password_buf { + *x = password[password_index]; + password_index += 1; + if password_index == password.len() { + password_index = 0; + } + } + ctx.update(&password_buf); + } + let key = ctx.finish(); + password_buf.clear(); + password_buf.extend_from_slice(key.as_ref()); + password_buf.extend_from_slice(&self.engine_id); + password_buf.extend_from_slice(key.as_ref()); + let mut ctx = Context::new(algorithm); + ctx.update(&password_buf); + Ok(ctx.finish().as_ref().to_vec()) + } + + fn update_auth_key( + &mut self, + authentication_password: &[u8], + auth_protocol: AuthProtocol, + ) -> Result<()> { + if self.engine_id.is_empty() { + self.auth_key.clear(); + return Err(Error::AuthFailure(AuthErrorKind::NotAuthenticated)); + } + self.auth_key = self.generate_key(authentication_password, auth_protocol)?; + Ok(()) + } + + fn update_priv_key( + &mut self, + privacy_password: &[u8], + auth_protocol: AuthProtocol, + cipher: &Cipher, + extension_method: &Option, + ) -> Result<()> { + if self.engine_id.is_empty() { + self.priv_key.clear(); + return Err(Error::AuthFailure(AuthErrorKind::NotAuthenticated)); + } + self.priv_key = self.generate_key(privacy_password, auth_protocol)?; + if !cipher.priv_key_needs_extension(&auth_protocol) { + return Ok(()); + } + match extension_method.as_ref() { + Some(KeyExtension::Blumenthal) => { + self.extend_priv_key_with_blumenthal_method(cipher.priv_key_len(), auth_protocol)? + } + Some(KeyExtension::Reeder) => { + self.extend_priv_key_with_reeder_method(cipher.priv_key_len(), auth_protocol)? + } + None => return Err(Error::AuthFailure(AuthErrorKind::KeyExtensionRequired)), + } + Ok(()) + } + + /// Extend `priv_key` to the required length using the Blumenthal algorithm. + fn extend_priv_key_with_blumenthal_method( + &mut self, + need_key_len: usize, + auth_protocol: AuthProtocol, + ) -> Result<()> { + if need_key_len <= self.priv_key.len() { + return Ok(()); + } + + let mut remaining = need_key_len - self.priv_key.len(); + let algorithm = auth_protocol.digest_algorithm(); + + while remaining > 0 { + let mut ctx = Context::new(algorithm); + ctx.update(&self.priv_key); + let new_hash = ctx.finish(); + + let copy_len = remaining.min(new_hash.as_ref().len()); + self.priv_key + .extend_from_slice(&new_hash.as_ref()[..copy_len]); + remaining -= copy_len; + } + + Ok(()) + } + + /// Extend Kul to the required length using the Reeder method. + fn extend_priv_key_with_reeder_method( + &mut self, + need_key_len: usize, + auth_protocol: AuthProtocol, + ) -> Result<()> { + if need_key_len < self.priv_key.len() { + return Ok(()); + } + let mut remaining = need_key_len - self.priv_key.len(); + while remaining > 0 { + let new_kul = self.generate_key(&self.priv_key, auth_protocol)?; + let copy_len = remaining.min(new_kul.len()); + self.priv_key.extend_from_slice(&new_kul[..copy_len]); + remaining -= copy_len; + } + Ok(()) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum KeyExtension { + Blumenthal, + Reeder, +} + +impl KeyExtension { + pub fn other(&self) -> Self { + match self { + KeyExtension::Blumenthal => KeyExtension::Reeder, + KeyExtension::Reeder => KeyExtension::Blumenthal, + } + } +} + +#[derive(Debug, Clone)] +pub struct Security { + pub(crate) username: Vec, + pub(crate) authentication_password: Vec, + pub(crate) auth: Auth, + pub(crate) auth_protocol: AuthProtocol, + pub(crate) key_extension_method: Option, + pub(crate) authoritative_state: AuthoritativeState, + pub(crate) plain_buf: Vec, +} + +impl Security { + pub fn new(username: &[u8], authentication_password: &[u8]) -> Self { + Self { + username: username.to_vec(), + authentication_password: authentication_password.to_vec(), + auth: Auth::AuthNoPriv, + auth_protocol: AuthProtocol::Sha1, + key_extension_method: None, + authoritative_state: AuthoritativeState::default(), + plain_buf: Vec::new(), + } + } + + pub fn with_auth(mut self, auth: Auth) -> Self { + self.auth = auth; + self + } + + pub fn with_auth_protocol(mut self, auth_protocol: AuthProtocol) -> Self { + self.auth_protocol = auth_protocol; + self + } + + pub fn with_key_extension_method(mut self, key_extension_method: KeyExtension) -> Self { + self.key_extension_method = Some(key_extension_method); + self + } + + pub(crate) fn another_key_extension_method(&mut self) -> Option { + if let Auth::AuthPriv { ref cipher, .. } = self.auth { + if cipher.priv_key_needs_extension(&self.auth_protocol) { + if let Some(used_method) = self.key_extension_method { + self.key_extension_method = Some(used_method.other()); + return self.key_extension_method; + } + } + } + None + } + + /// Note: the engine_id MUST be provided as a hex array, not as a byte-string. + pub fn with_engine_id(mut self, engine_id: &[u8]) -> Result { + self.authoritative_state.engine_id = engine_id.to_vec(); + self.update_key()?; + Ok(self) + } + + pub fn with_engine_boots_and_time(mut self, engine_boots: i64, engine_time: i64) -> Self { + self.authoritative_state.engine_boots = engine_boots; + self.authoritative_state + .update_authoritative_engine_time(engine_time); + self + } + + pub fn reset_engine_id(&mut self) { + self.authoritative_state.engine_id.clear(); + self.authoritative_state.auth_key.clear(); + self.authoritative_state.priv_key.clear(); + } + + pub fn reset_engine_counters(&mut self) { + self.authoritative_state.engine_boots = 0; + self.authoritative_state.update_authoritative_engine_time(0); + } + + fn calculate_hmac(&self, data: &[u8]) -> Result> { + if self.engine_id().is_empty() { + return Err(Error::AuthFailure(AuthErrorKind::SecurityNotReady)); + } + let algorithm = self.auth_protocol.hmac_algorithm(); + let key = hmac::Key::new(algorithm, &self.authoritative_state.auth_key); + let tag = hmac::sign(&key, data); + Ok(tag.as_ref().to_vec()) + } + + pub(crate) fn update_key(&mut self) -> Result<()> { + if !self.need_auth() { + return Ok(()); + } + + self.authoritative_state + .update_auth_key(&self.authentication_password, self.auth_protocol)?; + if let Auth::AuthPriv { + cipher, + privacy_password, + } = &self.auth + { + self.authoritative_state.update_priv_key( + privacy_password, + self.auth_protocol, + cipher, + &self.key_extension_method, + )?; + } + Ok(()) + } + + pub fn engine_id(&self) -> &[u8] { + &self.authoritative_state.engine_id + } + + pub fn engine_boots(&self) -> i64 { + self.authoritative_state.engine_boots + } + + pub fn engine_time(&self) -> i64 { + self.authoritative_state.engine_time + } + + pub fn username(&self) -> &[u8] { + &self.username + } + + pub(crate) fn correct_authoritative_engine_time(&mut self) { + self.authoritative_state.correct_engine_time(); + } + + pub(crate) fn need_auth(&self) -> bool { + self.auth != Auth::NoAuthNoPriv + } + + pub(crate) fn need_encrypt(&self) -> bool { + !self.authoritative_state.priv_key.is_empty() + } + + pub(crate) fn need_init(&self) -> bool { + self.engine_id().is_empty() + } + + fn encrypt_aes(&self, data: &[u8], key_len: usize) -> Result<(Vec, Vec)> { + use aws_lc_rs::cipher::{ + EncryptingKey, EncryptionContext, UnboundCipherKey, AES_128, AES_192, AES_256, + }; + use aws_lc_rs::iv::FixedLength; + + // IV: 4 bytes engine_boots + 4 bytes engine_time + 8 bytes random salt + let mut iv = [0u8; 16]; + iv[..4].copy_from_slice(&u32::try_from(self.engine_boots())?.to_be_bytes()); + iv[4..8].copy_from_slice(&u32::try_from(self.engine_time())?.to_be_bytes()); + fill(&mut iv[8..]).map_err(|e| Error::Crypto(e.to_string()))?; + + if self.authoritative_state.priv_key.len() < key_len { + return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)); + } + + let key_bytes = &self.authoritative_state.priv_key[..key_len]; + + let unbound_key = match key_len { + 16 => UnboundCipherKey::new(&AES_128, key_bytes), + 24 => UnboundCipherKey::new(&AES_192, key_bytes), + 32 => UnboundCipherKey::new(&AES_256, key_bytes), + _ => return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)), + } + .map_err(|e| Error::Crypto(e.to_string()))?; + + let context = EncryptionContext::Iv128(FixedLength::from(iv)); + let encrypting_key = + EncryptingKey::cfb128(unbound_key).map_err(|e| Error::Crypto(e.to_string()))?; + + // CFB128 produces same length output as input + let mut encrypted = data.to_vec(); + encrypting_key + .less_safe_encrypt(&mut encrypted, context) + .map_err(|e| Error::Crypto(e.to_string()))?; + + // Return salt (last 8 bytes of IV) as priv_params + Ok((encrypted, iv[8..].to_vec())) + } + + /// encrypts the data + pub(crate) fn encrypt(&self, data: &[u8]) -> Result<(Vec, Vec)> { + let Auth::AuthPriv { + cipher: cipher_kind, + .. + } = &self.auth + else { + return Err(Error::AuthFailure(AuthErrorKind::SecurityNotProvided)); + }; + + if self.engine_id().is_empty() { + return Err(Error::AuthFailure(AuthErrorKind::SecurityNotReady)); + } + + match cipher_kind { + Cipher::Aes128 => self.encrypt_aes(data, 16), + Cipher::Aes192 => self.encrypt_aes(data, 24), + Cipher::Aes256 => self.encrypt_aes(data, 32), + } + } + + fn decrypt_aes(&mut self, encrypted: &[u8], priv_params: &[u8], key_len: usize) -> Result<()> { + use aws_lc_rs::cipher::{ + DecryptingKey, DecryptionContext, UnboundCipherKey, AES_128, AES_192, AES_256, + }; + use aws_lc_rs::iv::FixedLength; + + // Reconstruct IV: 4 bytes engine_boots + 4 bytes engine_time + 8 bytes priv_params (salt) + if priv_params.len() != 8 { + return Err(Error::AuthFailure(AuthErrorKind::PrivLengthMismatch)); + } + + let mut iv = [0u8; 16]; + iv[..4].copy_from_slice(&u32::try_from(self.engine_boots())?.to_be_bytes()); + iv[4..8].copy_from_slice(&u32::try_from(self.engine_time())?.to_be_bytes()); + iv[8..].copy_from_slice(priv_params); + + if self.authoritative_state.priv_key.len() < key_len { + return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)); + } + + let key_bytes = &self.authoritative_state.priv_key[..key_len]; + + let unbound_key = match key_len { + 16 => UnboundCipherKey::new(&AES_128, key_bytes), + 24 => UnboundCipherKey::new(&AES_192, key_bytes), + 32 => UnboundCipherKey::new(&AES_256, key_bytes), + _ => return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)), + } + .map_err(|e| Error::Crypto(e.to_string()))?; + + let context = DecryptionContext::Iv128(FixedLength::from(iv)); + let decrypting_key = + DecryptingKey::cfb128(unbound_key).map_err(|e| Error::Crypto(e.to_string()))?; + + // CFB128 produces same length output as input + self.plain_buf = encrypted.to_vec(); + decrypting_key + .decrypt(&mut self.plain_buf, context) + .map_err(|e| Error::Crypto(e.to_string()))?; + + Ok(()) + } + + /// decrypts the data, the result is stored in `self.plain_buf` + fn decrypt(&mut self, encrypted: &[u8], priv_params: &[u8]) -> Result<()> { + let Auth::AuthPriv { + cipher: cipher_kind, + .. + } = &self.auth + else { + return Err(Error::AuthFailure(AuthErrorKind::SecurityNotProvided)); + }; + + match cipher_kind { + 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), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Auth { + NoAuthNoPriv, + /// Authentication + AuthNoPriv, + /// Authentication and encryption + AuthPriv { + cipher: Cipher, + privacy_password: Vec, + }, +} + +/// Authentication protocol. +/// +/// Note: MD5 is NOT available in this implementation (not FIPS-140 compliant). +/// Use the `v3` feature (OpenSSL-based) if MD5 support is required. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum AuthProtocol { + Sha1, + Sha224, + Sha256, + Sha384, + Sha512, +} + +impl AuthProtocol { + fn digest_algorithm(self) -> &'static digest::Algorithm { + match self { + AuthProtocol::Sha1 => &digest::SHA1_FOR_LEGACY_USE_ONLY, + AuthProtocol::Sha224 => &digest::SHA224, + AuthProtocol::Sha256 => &digest::SHA256, + AuthProtocol::Sha384 => &digest::SHA384, + AuthProtocol::Sha512 => &digest::SHA512, + } + } + + fn hmac_algorithm(self) -> hmac::Algorithm { + match self { + AuthProtocol::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, + AuthProtocol::Sha224 => hmac::HMAC_SHA224, + AuthProtocol::Sha256 => hmac::HMAC_SHA256, + AuthProtocol::Sha384 => hmac::HMAC_SHA384, + AuthProtocol::Sha512 => hmac::HMAC_SHA512, + } + } + + fn truncation_length(self) -> usize { + match self { + AuthProtocol::Sha1 => 12, + AuthProtocol::Sha224 => 16, + AuthProtocol::Sha256 => 24, + AuthProtocol::Sha384 => 32, + AuthProtocol::Sha512 => 48, + } + } +} + +/// Privacy cipher. +/// +/// Note: DES is NOT available in this implementation (not FIPS-140 compliant). +/// Use the `v3` feature (OpenSSL-based) if DES support is required. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum Cipher { + Aes128, + Aes192, + Aes256, +} + +impl Cipher { + pub fn priv_key_len(&self) -> usize { + match self { + Cipher::Aes128 => 16, + Cipher::Aes192 => 24, + Cipher::Aes256 => 32, + } + } + + /// Tells if for given auth_protocol and cipher pair, the priv_key is too short and need to be extended. + pub fn priv_key_needs_extension(&self, auth_protocol: &AuthProtocol) -> bool { + matches!( + (auth_protocol, self), + (AuthProtocol::Sha1, Cipher::Aes192 | Cipher::Aes256) + | (AuthProtocol::Sha224, Cipher::Aes256) + ) + } +} + +impl<'a> Pdu<'a> { + #[allow(clippy::too_many_lines)] + pub(crate) fn parse_v3_aws_lc_rs( + bytes: &'a [u8], + mut rdr: AsnReader<'a>, + security: &'a mut Security, + ) -> Result> { + let truncation_len = security.auth_protocol.truncation_length(); + let global_data_seq = rdr.read_raw(asn1::TYPE_SEQUENCE)?; + let mut global_data_rdr = AsnReader::from_bytes(global_data_seq); + let msg_id = global_data_rdr.read_asn_integer()?; + let max_size = global_data_rdr.read_asn_integer()?; + + if max_size > BUFFER_SIZE as i64 { + return Err(Error::BufferOverflow); + } + + let flags = global_data_rdr + .read_asn_octetstring()? + .first() + .copied() + .unwrap_or_default(); + + let security_model = global_data_rdr.read_asn_integer()?; + if security_model != 3 { + return Err(Error::AuthFailure(AuthErrorKind::UnsupportedUSM)); + } + + let security_params = rdr.read_asn_octetstring()?; + let security_seq = AsnReader::from_bytes(security_params).read_raw(asn1::TYPE_SEQUENCE)?; + let mut security_rdr = AsnReader::from_bytes(security_seq); + let engine_id = security_rdr.read_asn_octetstring()?; + let engine_boots = security_rdr.read_asn_integer()?; + let engine_time = security_rdr.read_asn_integer()?; + + let username = security_rdr.read_asn_octetstring()?; + let auth_params = security_rdr.read_asn_octetstring().map(<[u8]>::to_vec)?; + let auth_params_pos = + bytes.len() - rdr.bytes_left() - auth_params.len() - security_rdr.bytes_left(); + let priv_params = security_rdr.read_asn_octetstring()?; + + let mut is_discovery = false; + let mut prev_engine_time = security.engine_time(); + + if flags & V3_MSG_FLAGS_AUTH == 0 { + if security.authoritative_state.engine_id.is_empty() { + security.authoritative_state.engine_id = engine_id.to_vec(); + security.update_key()?; + is_discovery = true; + } else if engine_id != security.authoritative_state.engine_id && !engine_id.is_empty() { + return Err(Error::AuthFailure(AuthErrorKind::EngineIdMismatch)); + } + + if security.authoritative_state.engine_boots < engine_boots { + is_discovery = true; + prev_engine_time = engine_time; + security + .authoritative_state + .update_authoritative(engine_boots, engine_time); + } + + if is_discovery { + return Err(Error::AuthUpdated); + } + + if security.need_auth() { + return Err(Error::AuthFailure(AuthErrorKind::NotAuthenticated)); + } + } else { + if security.authoritative_state.engine_boots == 0 && engine_boots == 0 { + return Err(Error::AuthFailure(AuthErrorKind::EngineBootsNotProvided)); + } + + if security.authoritative_state.engine_boots < engine_boots { + is_discovery = true; + prev_engine_time = engine_time; + security + .authoritative_state + .update_authoritative(engine_boots, engine_time); + } else { + security + .authoritative_state + .update_authoritative_engine_time(engine_time); + } + + if username != security.username { + return Err(Error::AuthFailure(AuthErrorKind::UsernameMismatch)); + } + + if engine_id.is_empty() { + return Err(Error::AuthFailure(AuthErrorKind::NotAuthenticated)); + } + + if security.authoritative_state.engine_id.is_empty() { + security.authoritative_state.engine_id = engine_id.to_vec(); + security.update_key()?; + } else if engine_id != security.authoritative_state.engine_id { + return Err(Error::AuthFailure(AuthErrorKind::EngineIdMismatch)); + } + + if auth_params.len() != truncation_len + || auth_params_pos + auth_params.len() > bytes.len() + { + return Err(Error::ValueOutOfRange); + } + + unsafe { + let auth_params_ptr = bytes.as_ptr().add(auth_params_pos) as *mut u8; + std::hint::black_box(|| { + std::ptr::write_bytes(auth_params_ptr, 0, auth_params.len()); + })(); + } + + if security.need_auth() { + let hmac = security.calculate_hmac(bytes)?; + + if hmac.len() < truncation_len || hmac[..truncation_len] != auth_params { + return Err(Error::AuthFailure(AuthErrorKind::SignatureMismatch)); + } + } + } + + let scoped_pdu_seq = if flags & V3_MSG_FLAGS_PRIVACY == 0 { + if security.need_encrypt() && !is_discovery { + return Err(Error::AuthFailure(AuthErrorKind::ReplyNotEncrypted)); + } + + rdr.read_raw(asn1::TYPE_SEQUENCE)? + } else { + let encrypted_pdu = rdr.read_asn_octetstring()?; + security.decrypt(encrypted_pdu, priv_params)?; + let mut rdr = AsnReader::from_bytes(&security.plain_buf); + rdr.read_raw(asn1::TYPE_SEQUENCE)? + }; + + let mut scoped_pdu_rdr = AsnReader::from_bytes(scoped_pdu_seq); + + let _context_engine_id = scoped_pdu_rdr.read_asn_octetstring()?; + let _context_name = scoped_pdu_rdr.read_asn_octetstring()?; + + let ident = scoped_pdu_rdr.peek_byte()?; + let message_type = MessageType::from_ident(ident)?; + + if message_type == MessageType::Trap { + is_discovery = false; + } else { + if security.engine_boots() > engine_boots { + return Err(Error::AuthFailure(AuthErrorKind::EngineBootsMismatch)); + } + if security.engine_boots() == engine_boots + && (engine_time - prev_engine_time).abs() > ENGINE_TIME_WINDOW + { + return Err(Error::AuthFailure(AuthErrorKind::EngineTimeMismatch)); + } + } + + let mut response_pdu = AsnReader::from_bytes(scoped_pdu_rdr.read_raw(ident)?); + + let req_id: i32 = i32::try_from(response_pdu.read_asn_integer()?)?; + + let error_status: u32 = + u32::try_from(response_pdu.read_asn_integer()?).map_err(|_| Error::ValueOutOfRange)?; + + let error_index: u32 = u32::try_from(response_pdu.read_asn_integer()?)?; + + let varbind_bytes = response_pdu.read_raw(asn1::TYPE_SEQUENCE)?; + let varbinds = Varbinds::from_bytes(varbind_bytes); + + if is_discovery { + return Err(Error::AuthUpdated); + } + + Ok(Pdu { + version: Version::V3 as i64, + community: username, + message_type, + req_id, + error_status, + error_index, + varbinds, + v1_trap_info: None, + v3_msg_id: i32::try_from(msg_id).map_err(|_| Error::ValueOutOfRange)?, + }) + } +} + +pub(crate) fn build_init(req_id: i32, buf: &mut Buf) { + buf.reset(); + let mut sec_buf = Buf::default(); + sec_buf.push_sequence(|sec| { + sec.push_octet_string(&[]); // priv params + sec.push_octet_string(&[]); // auth params + sec.push_octet_string(&[]); // user name + sec.push_integer(0); // time + sec.push_integer(0); // boots + sec.push_octet_string(&[]); // engine ID + }); + buf.push_sequence(|message| { + message.push_sequence(|pdu| { + pdu.push_constructed(snmp::MSG_GET, |req| { + req.push_integer(0); // error index + req.push_integer(0); // error status + req.push_integer(req_id.into()); + }); + pdu.push_octet_string(&[]); + pdu.push_octet_string(&[]); + }); + message.push_octet_string(&sec_buf); + message.push_sequence(|global| { + global.push_integer(3); // security_model + global.push_octet_string(&[V3_MSG_FLAGS_REPORTABLE]); // flags + global.push_integer(BUFFER_SIZE.try_into().unwrap()); // max_size + global.push_integer(req_id.into()); // msg_id + }); + message.push_integer(Version::V3 as i64); + }); +} + +pub(crate) fn build( + ident: u8, + req_id: i32, + values: &[(&Oid, Value)], + non_repeaters: u32, + max_repetitions: u32, + buf: &mut Buf, + security: Option<&Security>, +) -> Result<()> { + let security = security.ok_or(Error::AuthFailure(AuthErrorKind::SecurityNotProvided))?; + let truncation_len = security.auth_protocol.truncation_length(); + buf.reset(); + let mut sec_buf_seq = Buf::default(); + sec_buf_seq.reset(); + let mut auth_pos = 0; + let mut sec_buf_len = 0; + let mut priv_params: Vec = Vec::new(); + let mut inner_len = 0; + let mut flags = V3_MSG_FLAGS_REPORTABLE; + + if security.need_auth() { + flags |= V3_MSG_FLAGS_AUTH; + } + + let encrypted = if security.need_encrypt() { + flags |= V3_MSG_FLAGS_PRIVACY; + let mut pdu_buf = Buf::default(); + pdu_buf.push_sequence(|buf| { + pdu::build_inner(req_id, ident, values, max_repetitions, non_repeaters, buf); + buf.push_octet_string(&[]); + buf.push_octet_string(security.engine_id()); + }); + let (encrypted, salt) = security.encrypt(&pdu_buf)?; + priv_params.extend_from_slice(&salt); + Some(encrypted) + } else { + None + }; + + buf.push_sequence(|buf| { + if let Some(ref encrypted) = encrypted { + buf.push_octet_string(encrypted); + } else { + buf.push_sequence(|buf| { + pdu::build_inner(req_id, ident, values, max_repetitions, non_repeaters, buf); + buf.push_octet_string(&[]); + buf.push_octet_string(security.engine_id()); + }); + } + let l0 = buf.len(); + sec_buf_seq.push_sequence(|buf| { + buf.push_octet_string(&priv_params); // priv params + let l0 = buf.len() - priv_params.len(); + buf.push_octet_string(&vec![0u8; truncation_len]); // auth params + let l1 = buf.len() - l0; + buf.push_octet_string(security.username()); // user name + buf.push_integer(security.engine_time()); // time + buf.push_integer(security.engine_boots()); // boots + buf.push_octet_string(security.engine_id()); // engine ID + auth_pos = buf.len() - l1; + sec_buf_len = buf.len(); + }); + buf.push_octet_string(&sec_buf_seq); + buf.push_sequence(|buf| { + buf.push_integer(3); // security_model + buf.push_octet_string(&[flags]); // flags + buf.push_integer(BUFFER_SIZE.try_into().unwrap()); // max_size + buf.push_integer(req_id.into()); // msg_id + }); + buf.push_integer(3); // version + auth_pos = buf.len() - l0 - (sec_buf_len - auth_pos); + inner_len = buf.len(); + }); + + auth_pos += buf.len() - inner_len; + if (auth_pos + truncation_len) > buf.len() { + return Err(Error::ValueOutOfRange); + } + + if security.need_auth() { + let hmac = security.calculate_hmac(buf)?; + buf[auth_pos..auth_pos + truncation_len].copy_from_slice(&hmac[..truncation_len]); + } + + Ok(()) +}