Skip to content
Open
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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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"]
67 changes: 64 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
102 changes: 90 additions & 12 deletions src/asyncsession.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -26,6 +29,8 @@ pub struct AsyncSession {
recv_buf: Box<[u8]>,
#[cfg(feature = "v3")]
security: Option<v3::Security>,
#[cfg(feature = "v3_aws_lc_rs")]
security: Option<v3_aws_lc_rs::Security>,
}

impl AsyncSession {
Expand Down Expand Up @@ -66,6 +71,21 @@ impl AsyncSession {
Ok(session)
}

#[cfg(feature = "v3_aws_lc_rs")]
pub async fn new_v3<SA>(
destination: SA,
starting_req_id: i32,
security: v3_aws_lc_rs::Security,
) -> io::Result<Self>
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<SA>(
version: Version,
destination: SA,
Expand Down Expand Up @@ -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(())
Expand Down Expand Up @@ -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
Expand All @@ -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<Option<v3_aws_lc_rs::KeyExtension>> {
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) {}

Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
25 changes: 19 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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")
}
Expand Down
Loading