use bytes::Bytes;
use std::net::SocketAddr;
use crate::ber::{Decoder, EncodeBuf};
use crate::compatibility::DecodeConfig;
use crate::error::internal::DecodeErrorKind;
use crate::error::{Error, Result};
use crate::message::{DecodeOutcome, SecurityLevel};
use crate::v3::validate_engine_id;
const MAX_USER_NAME_LEN: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsmSecurityParams {
pub(crate) engine_id: Bytes,
pub(crate) engine_boots: u32,
pub(crate) engine_time: u32,
pub(crate) username: Bytes,
pub(crate) auth_params: Bytes,
pub(crate) priv_params: Bytes,
discovery: bool,
}
impl UsmSecurityParams {
pub fn new(
engine_id: impl Into<Bytes>,
engine_boots: u32,
engine_time: u32,
username: impl Into<Bytes>,
) -> Result<Self> {
let value = Self {
engine_id: engine_id.into(),
engine_boots,
engine_time,
username: username.into(),
auth_params: Bytes::new(),
priv_params: Bytes::new(),
discovery: false,
};
value.validate_common()?;
Ok(value)
}
#[must_use]
pub fn discovery() -> Self {
Self {
engine_id: Bytes::new(),
engine_boots: 0,
engine_time: 0,
username: Bytes::new(),
auth_params: Bytes::new(),
priv_params: Bytes::new(),
discovery: true,
}
}
#[must_use]
pub fn engine_id(&self) -> &Bytes {
&self.engine_id
}
#[must_use]
pub fn engine_boots(&self) -> u32 {
self.engine_boots
}
#[must_use]
pub fn engine_time(&self) -> u32 {
self.engine_time
}
#[must_use]
pub fn username(&self) -> &Bytes {
&self.username
}
#[must_use]
pub fn auth_params(&self) -> &Bytes {
&self.auth_params
}
#[must_use]
pub fn priv_params(&self) -> &Bytes {
&self.priv_params
}
pub fn with_auth_params(mut self, auth_params: impl Into<Bytes>) -> Result<Self> {
self.auth_params = auth_params.into();
if self.auth_params.is_empty() {
return Err(
Error::Config("USM authentication parameters must be non-empty".into()).boxed(),
);
}
self.validate_common()?;
Ok(self)
}
pub fn with_priv_params(mut self, priv_params: impl Into<Bytes>) -> Result<Self> {
self.priv_params = priv_params.into();
if self.priv_params.is_empty() {
return Err(Error::Config("USM privacy parameters must be non-empty".into()).boxed());
}
self.validate_common()?;
Ok(self)
}
pub fn with_auth_placeholder(self, mac_len: usize) -> Result<Self> {
self.with_auth_params(Bytes::from(vec![0u8; mac_len]))
}
fn validate_common(&self) -> Result<()> {
if self.engine_boots > i32::MAX as u32 || self.engine_time > i32::MAX as u32 {
return Err(Error::Config("USM engine boots/time exceed i32::MAX".into()).boxed());
}
if self.username.len() > MAX_USER_NAME_LEN {
return Err(Error::Config(
format!("USM username exceeds {MAX_USER_NAME_LEN} octets").into(),
)
.boxed());
}
if self.engine_id.is_empty() {
if !self.discovery
|| self.engine_boots != 0
|| self.engine_time != 0
|| !self.username.is_empty()
|| !self.auth_params.is_empty()
|| !self.priv_params.is_empty()
{
return Err(Error::Config(
"empty engine ID is reserved for explicit discovery parameters".into(),
)
.boxed());
}
} else {
validate_engine_id(&self.engine_id)?;
}
self.validate_field_relationships()
}
fn validate_field_relationships(&self) -> Result<()> {
if self.auth_params.is_empty() && !self.priv_params.is_empty() {
return Err(Error::Config(
"USM privacy parameters require authentication parameters".into(),
)
.boxed());
}
Ok(())
}
pub fn validate_for_security_level(&self, level: SecurityLevel) -> Result<()> {
self.validate_common()?;
let valid = match level {
SecurityLevel::NoAuthNoPriv => {
self.auth_params.is_empty() && self.priv_params.is_empty()
}
SecurityLevel::AuthNoPriv => {
!self.auth_params.is_empty() && self.priv_params.is_empty()
}
SecurityLevel::AuthPriv => !self.auth_params.is_empty() && !self.priv_params.is_empty(),
};
if !valid {
return Err(Error::Config(
"USM authentication/privacy fields contradict the security level".into(),
)
.boxed());
}
Ok(())
}
pub fn encode(&self) -> Result<Bytes> {
self.validate_common()?;
let mut buf = EncodeBuf::new();
self.encode_to_buf(&mut buf)?;
Ok(buf.finish())
}
pub fn encode_to_buf(&self, buf: &mut EncodeBuf) -> Result<()> {
self.validate_common()?;
buf.push_sequence(|buf| {
buf.push_octet_string(&self.priv_params)?;
buf.push_octet_string(&self.auth_params)?;
buf.push_octet_string(&self.username)?;
buf.push_unsigned32(crate::ber::tag::universal::INTEGER, self.engine_time);
buf.push_unsigned32(crate::ber::tag::universal::INTEGER, self.engine_boots);
buf.push_octet_string(&self.engine_id)?;
Ok(())
})
}
pub fn decode(data: Bytes, config: DecodeConfig) -> Result<DecodeOutcome<Self>> {
let anomalies = std::cell::RefCell::new(Vec::new());
let mut decoder = Decoder::new(data)
.with_decode_config(config)
.with_anomaly_sink(&anomalies);
let params = Self::decode_from(&mut decoder)?;
if !decoder.is_empty() {
return Err(decoder.malformed(DecodeErrorKind::TrailingData {
remaining: decoder.remaining(),
}));
}
drop(decoder);
Ok(DecodeOutcome {
value: params,
anomalies: anomalies.into_inner(),
})
}
pub(crate) fn decode_with_context(
data: Bytes,
base_offset: usize,
target: SocketAddr,
config: DecodeConfig,
) -> Result<DecodeOutcome<Self>> {
let anomalies = std::cell::RefCell::new(Vec::new());
let mut decoder = Decoder::with_context(data, base_offset, Some(target))
.with_decode_config(config)
.with_anomaly_sink(&anomalies);
let params = Self::decode_from(&mut decoder)?;
if !decoder.is_empty() {
return Err(decoder.malformed(DecodeErrorKind::TrailingData {
remaining: decoder.remaining(),
}));
}
drop(decoder);
Ok(DecodeOutcome {
value: params,
anomalies: anomalies.into_inner(),
})
}
pub(crate) fn decode_from(decoder: &mut Decoder) -> Result<Self> {
let mut seq = decoder.read_sequence()?;
let engine_id = seq.read_octet_string()?;
let raw_boots = seq.read_bounded_integer(0, i32::MAX)?;
let engine_boots = raw_boots as u32;
let raw_time = seq.read_bounded_integer(0, i32::MAX)?;
let engine_time = raw_time as u32;
let username = seq.read_octet_string()?;
if username.len() > MAX_USER_NAME_LEN {
tracing::debug!(target: "async_snmp::usm", { offset = seq.offset(), length = username.len(), kind = %DecodeErrorKind::InvalidUserNameLength { length: username.len() } }, "decode error");
return Err(seq.malformed(DecodeErrorKind::InvalidUserNameLength {
length: username.len(),
}));
}
let auth_params = seq.read_octet_string()?;
let priv_params = seq.read_octet_string()?;
if !seq.is_empty() {
return Err(seq.malformed(DecodeErrorKind::TrailingData {
remaining: seq.remaining(),
}));
}
let discovery = engine_id.is_empty();
Ok(Self {
engine_id,
engine_boots,
engine_time,
username,
auth_params,
priv_params,
discovery,
})
}
#[must_use]
pub fn find_auth_params_offset(encoded_msg: &[u8]) -> Option<(usize, usize)> {
use crate::ber::tag::universal::{OCTET_STRING, SEQUENCE};
let mut dec = Decoder::from_slice(encoded_msg);
dec.expect_tag(SEQUENCE).ok()?; dec.skip_tlv().ok()?; dec.skip_tlv().ok()?; dec.expect_tag(OCTET_STRING).ok()?; dec.expect_tag(SEQUENCE).ok()?; dec.skip_tlv().ok()?; dec.skip_tlv().ok()?; dec.skip_tlv().ok()?; dec.skip_tlv().ok()?;
let auth_len = dec.expect_tag(OCTET_STRING).ok()?;
let auth_start = dec.offset();
dec.read_bytes(auth_len).ok()?;
Some((auth_start, auth_len))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
fn push_integer_content(buf: &mut EncodeBuf, content: &[u8]) {
buf.push_bytes(content);
buf.push_length(content.len()).unwrap();
buf.push_tag(crate::ber::tag::universal::INTEGER);
}
fn params_with_integer_contents(engine_boots: &[u8], engine_time: &[u8]) -> Bytes {
let mut buf = EncodeBuf::new();
buf.push_sequence(|buf| {
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
push_integer_content(buf, engine_time);
push_integer_content(buf, engine_boots);
buf.push_octet_string(&[])
})
.unwrap();
buf.finish()
}
#[test]
fn constructors_enforce_public_usm_invariants_and_encode_rechecks() {
assert!(UsmSecurityParams::new(Bytes::new(), 0, 0, Bytes::new()).is_err());
assert!(UsmSecurityParams::new(b"abcd".as_slice(), 0, 0, Bytes::new()).is_err());
assert!(UsmSecurityParams::new([0_u8; 8].as_slice(), 0, 0, Bytes::new()).is_err());
assert!(UsmSecurityParams::new([0xff_u8; 8].as_slice(), 0, 0, Bytes::new()).is_err());
assert!(
UsmSecurityParams::new(
b"engine".as_slice(),
i32::MAX as u32,
i32::MAX as u32,
[b'u'; 32].as_slice()
)
.is_ok()
);
assert!(
UsmSecurityParams::new(b"engine".as_slice(), i32::MAX as u32 + 1, 0, Bytes::new())
.is_err()
);
assert!(
UsmSecurityParams::new(b"engine".as_slice(), 0, i32::MAX as u32 + 1, Bytes::new())
.is_err()
);
assert!(UsmSecurityParams::new(b"engine".as_slice(), 0, 0, [b'u'; 33].as_slice()).is_err());
let mut params = UsmSecurityParams::new(b"engine".as_slice(), 0, 0, Bytes::new()).unwrap();
params.engine_time = i32::MAX as u32 + 1;
assert!(params.encode().is_err());
let mut non_discovery =
UsmSecurityParams::new(b"engine".as_slice(), 0, 0, Bytes::new()).unwrap();
non_discovery.engine_id = Bytes::new();
assert!(non_discovery.encode().is_err());
let discovery = UsmSecurityParams::discovery();
assert!(discovery.encode().is_ok());
assert!(
discovery
.validate_for_security_level(SecurityLevel::NoAuthNoPriv)
.is_ok()
);
assert!(
UsmSecurityParams::discovery()
.with_auth_params([0_u8; 12].as_slice())
.is_err()
);
assert!(
UsmSecurityParams::discovery()
.with_auth_placeholder(12)
.is_err()
);
}
#[test]
fn security_level_rejects_auth_priv_field_contradictions() {
let base = UsmSecurityParams::new(b"engine".as_slice(), 0, 0, b"user".as_slice()).unwrap();
assert!(
base.validate_for_security_level(SecurityLevel::AuthNoPriv)
.is_err()
);
let auth = base.with_auth_params([0_u8; 12].as_slice()).unwrap();
assert!(
auth.validate_for_security_level(SecurityLevel::NoAuthNoPriv)
.is_err()
);
assert!(
auth.validate_for_security_level(SecurityLevel::AuthPriv)
.is_err()
);
assert!(
UsmSecurityParams::new(b"engine".as_slice(), 0, 0, b"user".as_slice())
.unwrap()
.with_priv_params([0_u8; 8].as_slice())
.is_err()
);
}
#[test]
fn test_usm_params_empty_roundtrip() {
let params = UsmSecurityParams::discovery();
let encoded = params.encode().unwrap();
let decoded = UsmSecurityParams::decode(encoded, DecodeConfig::default())
.unwrap()
.value;
assert!(decoded.engine_id.is_empty());
assert_eq!(decoded.engine_boots, 0);
assert_eq!(decoded.engine_time, 0);
assert!(decoded.username.is_empty());
assert!(decoded.auth_params.is_empty());
assert!(decoded.priv_params.is_empty());
}
#[test]
fn test_usm_params_roundtrip() {
let params =
UsmSecurityParams::new(b"engine-id".as_slice(), 1234, 5678, b"admin".as_slice())
.unwrap()
.with_auth_params(b"auth123456789012".as_slice())
.unwrap() .with_priv_params(b"priv1234".as_slice())
.unwrap();
let encoded = params.encode().unwrap();
let decoded = UsmSecurityParams::decode(encoded, DecodeConfig::default())
.unwrap()
.value;
assert_eq!(decoded.engine_id.as_ref(), b"engine-id");
assert_eq!(decoded.engine_boots, 1234);
assert_eq!(decoded.engine_time, 5678);
assert_eq!(decoded.username.as_ref(), b"admin");
assert_eq!(decoded.auth_params.as_ref(), b"auth123456789012");
assert_eq!(decoded.priv_params.as_ref(), b"priv1234");
}
#[test]
fn test_usm_params_rejects_extra_fields_and_trailing_data() {
let encoded = UsmSecurityParams::discovery().encode().unwrap();
let mut trailing = encoded.to_vec();
trailing.extend_from_slice(&[0x05, 0x00]);
assert!(UsmSecurityParams::decode(Bytes::from(trailing), DecodeConfig::default()).is_err());
let mut extra_field = encoded.to_vec();
assert_eq!(extra_field[0], 0x30);
assert!(extra_field[1] < 0x80);
extra_field[1] += 2;
extra_field.extend_from_slice(&[0x05, 0x00]);
assert!(
UsmSecurityParams::decode(Bytes::from(extra_field), DecodeConfig::default()).is_err()
);
}
#[test]
fn test_usm_params_with_placeholder() {
let params = UsmSecurityParams::new(b"engine".as_slice(), 100, 200, b"user".as_slice())
.unwrap()
.with_auth_placeholder(12)
.unwrap();
assert_eq!(params.auth_params.len(), 12);
assert!(params.auth_params.iter().all(|&b| b == 0));
}
#[test]
fn test_find_auth_params_offset() {
use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, SecurityLevel, V3Message};
use crate::oid;
use crate::pdu::Pdu;
let global = MsgGlobalData::new(
12345,
crate::MessageSize::new(1472).unwrap(),
MsgFlags::new(SecurityLevel::AuthNoPriv, true),
)
.unwrap();
let usm_params =
UsmSecurityParams::new(b"engine123".as_slice(), 100, 200, b"testuser".as_slice())
.unwrap()
.with_auth_placeholder(12)
.unwrap();
let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
let scoped = ScopedPdu::with_empty_context(pdu);
let msg = V3Message::new(global, usm_params.encode().unwrap(), scoped).unwrap();
let encoded = msg.encode().unwrap();
let (offset, len) = UsmSecurityParams::find_auth_params_offset(&encoded).unwrap();
assert_eq!(len, 12);
assert!(encoded[offset..offset + len].iter().all(|&b| b == 0));
}
#[test]
fn test_usm_params_rejects_negative_engine_boots() {
use crate::ber::EncodeBuf;
let mut buf = EncodeBuf::new();
buf.push_sequence(|buf| {
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_integer(100);
buf.push_integer(-1);
buf.push_octet_string(&[])
})
.unwrap();
let encoded = buf.finish();
let result = UsmSecurityParams::decode(encoded, DecodeConfig::default());
assert!(result.is_err());
assert!(matches!(*result.unwrap_err(), Error::Decode(_)));
}
#[test]
fn test_usm_params_rejects_negative_engine_time() {
use crate::ber::EncodeBuf;
let mut buf = EncodeBuf::new();
buf.push_sequence(|buf| {
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_integer(-1);
buf.push_integer(100);
buf.push_octet_string(&[])
})
.unwrap();
let encoded = buf.finish();
let result = UsmSecurityParams::decode(encoded, DecodeConfig::default());
assert!(result.is_err());
assert!(matches!(*result.unwrap_err(), Error::Decode(_)));
}
#[test]
fn test_usm_params_accepts_max_values() {
use crate::ber::EncodeBuf;
let mut buf = EncodeBuf::new();
buf.push_sequence(|buf| {
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_integer(i32::MAX);
buf.push_integer(i32::MAX);
buf.push_octet_string(&[])
})
.unwrap();
let encoded = buf.finish();
let decoded = UsmSecurityParams::decode(encoded, DecodeConfig::default())
.unwrap()
.value;
assert_eq!(decoded.engine_boots, i32::MAX as u32);
assert_eq!(decoded.engine_time, i32::MAX as u32);
}
#[test]
fn usm_params_reject_over_width_engine_time_aliases() {
const ZERO: &[u8] = &[0x00];
const TWO_TO_32: &[u8] = &[0x01, 0x00, 0x00, 0x00, 0x00];
assert!(
UsmSecurityParams::decode(
params_with_integer_contents(TWO_TO_32, ZERO),
DecodeConfig::default(),
)
.is_err()
);
assert!(
UsmSecurityParams::decode(
params_with_integer_contents(ZERO, TWO_TO_32),
DecodeConfig::default(),
)
.is_err()
);
}
#[test]
fn test_usm_params_accepts_zero_values() {
use crate::ber::EncodeBuf;
let mut buf = EncodeBuf::new();
buf.push_sequence(|buf| {
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_integer(0);
buf.push_integer(0);
buf.push_octet_string(&[])
})
.unwrap();
let encoded = buf.finish();
let decoded = UsmSecurityParams::decode(encoded, DecodeConfig::default())
.unwrap()
.value;
assert_eq!(decoded.engine_boots, 0);
assert_eq!(decoded.engine_time, 0);
}
#[test]
fn test_find_auth_params_offset_truncated_returns_none() {
assert_eq!(UsmSecurityParams::find_auth_params_offset(&[]), None);
assert_eq!(UsmSecurityParams::find_auth_params_offset(&[0x30]), None);
let msg: &[u8] = &[
0x30, 0x64, ];
assert_eq!(UsmSecurityParams::find_auth_params_offset(msg), None);
}
#[test]
fn test_find_auth_params_offset_inflated_global_len_returns_none() {
let msg: &[u8] = &[
0x30, 0x06, 0x02, 0x01, 0x03, 0x30, 0x7f, ];
assert_eq!(UsmSecurityParams::find_auth_params_offset(msg), None);
}
#[test]
fn test_find_auth_params_offset_auth_len_overflow_returns_none() {
use crate::message::{MsgFlags, MsgGlobalData, ScopedPdu, SecurityLevel, V3Message};
use crate::oid;
use crate::pdu::Pdu;
let global = MsgGlobalData::new(
1,
crate::MessageSize::new(1472).unwrap(),
MsgFlags::new(SecurityLevel::AuthNoPriv, true),
)
.unwrap();
let usm_params = UsmSecurityParams::new(b"engine".as_slice(), 1, 1, b"u".as_slice())
.unwrap()
.with_auth_placeholder(12)
.unwrap();
let pdu = Pdu::get_request(1, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
let scoped = ScopedPdu::with_empty_context(pdu);
let msg = V3Message::new(global, usm_params.encode().unwrap(), scoped).unwrap();
let encoded_bytes = msg.encode().unwrap();
let mut encoded: Vec<u8> = encoded_bytes.to_vec();
let (auth_start, auth_len) = UsmSecurityParams::find_auth_params_offset(&encoded).unwrap();
assert_eq!(auth_len, 12);
encoded[auth_start - 1] = 0x40;
assert_eq!(UsmSecurityParams::find_auth_params_offset(&encoded), None);
}
#[test]
fn test_usm_params_rejects_username_over_32_octets() {
use crate::ber::EncodeBuf;
let long_username = vec![b'x'; 33];
let mut buf = EncodeBuf::new();
buf.push_sequence(|buf| {
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_octet_string(&long_username)?;
buf.push_integer(0);
buf.push_integer(0);
buf.push_octet_string(&[])
})
.unwrap();
let encoded = buf.finish();
let result = UsmSecurityParams::decode(encoded, DecodeConfig::default());
assert!(result.is_err());
assert!(matches!(*result.unwrap_err(), Error::Decode(_)));
}
#[test]
fn test_usm_params_accepts_username_exactly_32_octets() {
let username = vec![b'u'; 32];
let params = UsmSecurityParams::new(b"engine".as_slice(), 0, 0, username.clone()).unwrap();
let encoded = params.encode().unwrap();
let decoded = UsmSecurityParams::decode(encoded, DecodeConfig::default())
.unwrap()
.value;
assert_eq!(decoded.username.as_ref(), username.as_slice());
}
#[test]
fn test_usm_params_accepts_short_username() {
let params =
UsmSecurityParams::new(b"engine".as_slice(), 0, 0, b"admin".as_slice()).unwrap();
let encoded = params.encode().unwrap();
let decoded = UsmSecurityParams::decode(encoded, DecodeConfig::default())
.unwrap()
.value;
assert_eq!(decoded.username.as_ref(), b"admin");
}
#[test]
fn usm_security_params_equality() {
let a = UsmSecurityParams::new(
Bytes::from_static(b"engine"),
1,
100,
Bytes::from_static(b"user"),
)
.unwrap();
let b = a.clone();
assert_eq!(a, b);
}
}