Skip to main content

async_snmp/message/
v3.rs

1//! `SNMPv3` message format (RFC 3412).
2//!
3//! V3 messages have a more complex structure than v1/v2c:
4//! ```text
5//! SEQUENCE {
6//!     INTEGER version (3)
7//!     SEQUENCE msgGlobalData {
8//!         INTEGER msgID
9//!         INTEGER msgMaxSize
10//!         OCTET STRING msgFlags (1 byte)
11//!         INTEGER msgSecurityModel
12//!     }
13//!     OCTET STRING msgSecurityParameters (opaque, USM-encoded)
14//!     msgData (ScopedPDU or encrypted OCTET STRING)
15//! }
16//! ```
17//!
18//! The msgData field is either:
19//! - A plaintext `ScopedPDU` (SEQUENCE) for noAuthNoPriv/authNoPriv
20//! - An encrypted OCTET STRING for authPriv (decrypts to `ScopedPDU`)
21
22use bytes::Bytes;
23
24use crate::ber::{Decoder, EncodeBuf};
25use crate::error::internal::DecodeErrorKind;
26use crate::error::{Error, Result, UNKNOWN_TARGET};
27use crate::pdu::Pdu;
28
29/// Minimum `msgMaxSize` per RFC 3412 `HeaderData` (INTEGER 484..2147483647).
30const MSG_MAX_SIZE_MINIMUM: i32 = 484;
31
32/// `SNMPv3` security model identifiers.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[repr(i32)]
35pub enum SecurityModel {
36    /// User-based Security Model (RFC 3414)
37    Usm = 3,
38}
39
40impl SecurityModel {
41    /// Create from raw value.
42    #[must_use]
43    pub fn from_i32(value: i32) -> Option<Self> {
44        match value {
45            3 => Some(Self::Usm),
46            _ => None,
47        }
48    }
49
50    /// Get the raw value.
51    #[must_use]
52    pub fn as_i32(self) -> i32 {
53        self as i32
54    }
55}
56
57/// `SNMPv3` security level.
58///
59/// The variants are ordered from least secure to most secure,
60/// supporting VACM-style level comparisons (e.g., `actual >= required`).
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
62pub enum SecurityLevel {
63    /// No authentication, no privacy
64    NoAuthNoPriv,
65    /// Authentication only
66    AuthNoPriv,
67    /// Authentication and privacy (encryption)
68    AuthPriv,
69}
70
71impl SecurityLevel {
72    /// Decode from msgFlags byte.
73    #[must_use]
74    pub fn from_flags(flags: u8) -> Option<Self> {
75        let auth = flags & 0x01 != 0;
76        let priv_ = flags & 0x02 != 0;
77
78        match (auth, priv_) {
79            (false, false) => Some(Self::NoAuthNoPriv),
80            (true, false) => Some(Self::AuthNoPriv),
81            (true, true) => Some(Self::AuthPriv),
82            (false, true) => None, // Invalid: priv without auth
83        }
84    }
85
86    /// Encode to msgFlags byte (without reportable flag).
87    #[must_use]
88    pub fn to_flags(self) -> u8 {
89        match self {
90            Self::NoAuthNoPriv => 0x00,
91            Self::AuthNoPriv => 0x01,
92            Self::AuthPriv => 0x03,
93        }
94    }
95
96    /// Check if authentication is required.
97    #[must_use]
98    pub fn requires_auth(self) -> bool {
99        matches!(self, Self::AuthNoPriv | Self::AuthPriv)
100    }
101
102    /// Check if privacy (encryption) is required.
103    #[must_use]
104    pub fn requires_priv(self) -> bool {
105        matches!(self, Self::AuthPriv)
106    }
107}
108
109impl TryFrom<u8> for SecurityLevel {
110    type Error = u8;
111
112    fn try_from(flags: u8) -> std::result::Result<Self, u8> {
113        Self::from_flags(flags).ok_or(flags)
114    }
115}
116
117impl From<SecurityLevel> for u8 {
118    fn from(level: SecurityLevel) -> u8 {
119        level.to_flags()
120    }
121}
122
123/// Message flags (RFC 3412 Section 6.4).
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct MsgFlags {
126    /// Security level
127    pub security_level: SecurityLevel,
128    /// Whether a report PDU may be sent on error
129    pub reportable: bool,
130}
131
132impl MsgFlags {
133    /// Create new message flags.
134    #[must_use]
135    pub fn new(security_level: SecurityLevel, reportable: bool) -> Self {
136        Self {
137            security_level,
138            reportable,
139        }
140    }
141
142    /// Decode from byte.
143    pub fn from_byte(byte: u8) -> Result<Self> {
144        let security_level = SecurityLevel::from_flags(byte).ok_or_else(|| {
145            tracing::debug!(target: "async_snmp::v3", { byte, kind = %DecodeErrorKind::InvalidMsgFlags }, "decode error");
146            Error::MalformedResponse {
147                target: UNKNOWN_TARGET,
148            }
149            .boxed()
150        })?;
151        let reportable = byte & 0x04 != 0;
152        Ok(Self {
153            security_level,
154            reportable,
155        })
156    }
157
158    /// Encode to byte.
159    #[must_use]
160    pub fn to_byte(self) -> u8 {
161        let mut flags = self.security_level.to_flags();
162        if self.reportable {
163            flags |= 0x04;
164        }
165        flags
166    }
167}
168
169/// Message global data header (msgGlobalData).
170#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct MsgGlobalData {
172    /// Message identifier for request/response correlation
173    pub msg_id: i32,
174    /// Maximum message size the sender can accept
175    pub msg_max_size: i32,
176    /// Message flags (security level + reportable)
177    pub msg_flags: MsgFlags,
178    /// Security model (always USM=3 for our implementation)
179    pub msg_security_model: SecurityModel,
180}
181
182impl MsgGlobalData {
183    /// Create new global data.
184    #[must_use]
185    pub fn new(msg_id: i32, msg_max_size: i32, msg_flags: MsgFlags) -> Self {
186        Self {
187            msg_id,
188            msg_max_size,
189            msg_flags,
190            msg_security_model: SecurityModel::Usm,
191        }
192    }
193
194    /// Encode to buffer.
195    pub fn encode(&self, buf: &mut EncodeBuf) {
196        buf.push_sequence(|buf| {
197            buf.push_integer(self.msg_security_model.as_i32());
198            // msgFlags is a 1-byte OCTET STRING
199            buf.push_octet_string(&[self.msg_flags.to_byte()]);
200            buf.push_integer(self.msg_max_size);
201            buf.push_integer(self.msg_id);
202        });
203    }
204
205    /// Decode from decoder.
206    ///
207    /// Validates that:
208    /// - `msgID` is in range 0..2147483647 (RFC 3412 `HeaderData`)
209    /// - `msgMaxSize` is in range 484..2147483647 (RFC 3412 `HeaderData`)
210    /// - `msgSecurityModel` is a known value (currently only USM=3)
211    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
212        let mut seq = decoder.read_sequence()?;
213
214        let msg_id = seq.read_integer()?;
215        let msg_max_size = seq.read_integer()?;
216
217        // RFC 3412 HeaderData: msgID INTEGER (0..2147483647)
218        if msg_id < 0 {
219            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), value = msg_id, kind = %DecodeErrorKind::InvalidMsgId { value: msg_id } }, "decode error");
220            return Err(Error::MalformedResponse {
221                target: UNKNOWN_TARGET,
222            }
223            .boxed());
224        }
225
226        // RFC 3412 HeaderData: msgMaxSize INTEGER (484..2147483647)
227        // Negative values indicate the sender encoded a value > 2^31-1
228        if msg_max_size < 0 {
229            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), value = msg_max_size, kind = %DecodeErrorKind::MsgMaxSizeTooLarge { value: msg_max_size } }, "decode error");
230            return Err(Error::MalformedResponse {
231                target: UNKNOWN_TARGET,
232            }
233            .boxed());
234        }
235
236        if msg_max_size < MSG_MAX_SIZE_MINIMUM {
237            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), value = msg_max_size, minimum = MSG_MAX_SIZE_MINIMUM, kind = %DecodeErrorKind::MsgMaxSizeTooSmall { value: msg_max_size, minimum: MSG_MAX_SIZE_MINIMUM } }, "decode error");
238            return Err(Error::MalformedResponse {
239                target: UNKNOWN_TARGET,
240            }
241            .boxed());
242        }
243
244        let flags_bytes = seq.read_octet_string()?;
245        if flags_bytes.len() != 1 {
246            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), expected = 1, actual = flags_bytes.len() }, "invalid msgFlags length");
247            return Err(Error::MalformedResponse {
248                target: UNKNOWN_TARGET,
249            }
250            .boxed());
251        }
252        let msg_flags = MsgFlags::from_byte(flags_bytes[0])?;
253
254        let msg_security_model_raw = seq.read_integer()?;
255        // Reject unknown security models per RFC 3412 Section 7.2
256        let msg_security_model =
257            SecurityModel::from_i32(msg_security_model_raw).ok_or_else(|| {
258                tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), model = msg_security_model_raw, kind = %DecodeErrorKind::UnknownSecurityModel(msg_security_model_raw) }, "decode error");
259                Error::MalformedResponse {
260                    target: UNKNOWN_TARGET,
261                }
262                .boxed()
263            })?;
264
265        Ok(Self {
266            msg_id,
267            msg_max_size,
268            msg_flags,
269            msg_security_model,
270        })
271    }
272}
273
274/// Scoped PDU (contextEngineID + contextName + PDU).
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct ScopedPdu {
277    /// Context engine ID (typically same as authoritative engine ID)
278    pub context_engine_id: Bytes,
279    /// Context name (typically empty string)
280    pub context_name: Bytes,
281    /// The actual PDU
282    pub pdu: Pdu,
283}
284
285impl ScopedPdu {
286    /// Create a new scoped PDU.
287    pub fn new(
288        context_engine_id: impl Into<Bytes>,
289        context_name: impl Into<Bytes>,
290        pdu: Pdu,
291    ) -> Self {
292        Self {
293            context_engine_id: context_engine_id.into(),
294            context_name: context_name.into(),
295            pdu,
296        }
297    }
298
299    /// Create with empty context (most common case).
300    #[must_use]
301    pub fn with_empty_context(pdu: Pdu) -> Self {
302        Self {
303            context_engine_id: Bytes::new(),
304            context_name: Bytes::new(),
305            pdu,
306        }
307    }
308
309    /// Encode to buffer.
310    pub fn encode(&self, buf: &mut EncodeBuf) {
311        buf.push_sequence(|buf| {
312            self.pdu.encode(buf);
313            buf.push_octet_string(&self.context_name);
314            buf.push_octet_string(&self.context_engine_id);
315        });
316    }
317
318    /// Encode to bytes.
319    pub fn encode_to_bytes(&self) -> Bytes {
320        let mut buf = EncodeBuf::new();
321        self.encode(&mut buf);
322        buf.finish()
323    }
324
325    /// Decode from decoder.
326    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
327        let mut seq = decoder.read_sequence()?;
328
329        let context_engine_id = seq.read_octet_string()?;
330        let context_name = seq.read_octet_string()?;
331        let pdu = Pdu::decode(&mut seq)?;
332
333        Ok(Self {
334            context_engine_id,
335            context_name,
336            pdu,
337        })
338    }
339}
340
341/// `SNMPv3` message.
342#[derive(Debug, Clone, PartialEq, Eq)]
343pub struct V3Message {
344    /// Global data (header)
345    pub global_data: MsgGlobalData,
346    /// Security parameters (opaque, USM-encoded)
347    pub security_params: Bytes,
348    /// Message data - either plaintext `ScopedPdu` or encrypted bytes
349    pub data: V3MessageData,
350}
351
352/// Message data payload.
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub enum V3MessageData {
355    /// Plaintext scoped PDU (noAuthNoPriv or authNoPriv)
356    Plaintext(ScopedPdu),
357    /// Encrypted scoped PDU (authPriv) - raw ciphertext
358    Encrypted(Bytes),
359}
360
361impl V3Message {
362    /// Create a new V3 message with plaintext data.
363    pub fn new(global_data: MsgGlobalData, security_params: Bytes, scoped_pdu: ScopedPdu) -> Self {
364        Self {
365            global_data,
366            security_params,
367            data: V3MessageData::Plaintext(scoped_pdu),
368        }
369    }
370
371    /// Create a new V3 message with encrypted data.
372    pub fn new_encrypted(
373        global_data: MsgGlobalData,
374        security_params: Bytes,
375        encrypted: Bytes,
376    ) -> Self {
377        Self {
378            global_data,
379            security_params,
380            data: V3MessageData::Encrypted(encrypted),
381        }
382    }
383
384    /// Get the scoped PDU if available (plaintext only).
385    pub fn scoped_pdu(&self) -> Option<&ScopedPdu> {
386        match &self.data {
387            V3MessageData::Plaintext(pdu) => Some(pdu),
388            V3MessageData::Encrypted(_) => None,
389        }
390    }
391
392    /// Consume and return the scoped PDU if available.
393    pub fn into_scoped_pdu(self) -> Option<ScopedPdu> {
394        match self.data {
395            V3MessageData::Plaintext(pdu) => Some(pdu),
396            V3MessageData::Encrypted(_) => None,
397        }
398    }
399
400    /// Get the PDU if available (convenience method).
401    pub fn pdu(&self) -> Option<&Pdu> {
402        self.scoped_pdu().map(|s| &s.pdu)
403    }
404
405    /// Consume and return the PDU.
406    pub fn into_pdu(self) -> Option<Pdu> {
407        self.into_scoped_pdu().map(|s| s.pdu)
408    }
409
410    /// Get the message ID.
411    pub fn msg_id(&self) -> i32 {
412        self.global_data.msg_id
413    }
414
415    /// Get the security level.
416    pub fn security_level(&self) -> SecurityLevel {
417        self.global_data.msg_flags.security_level
418    }
419
420    /// Encode to BER.
421    ///
422    /// Note: For authenticated messages, the caller must:
423    /// 1. Encode with placeholder auth params (12 zero bytes for HMAC-96)
424    /// 2. Compute HMAC over the entire encoded message
425    /// 3. Replace the placeholder with the actual HMAC
426    pub fn encode(&self) -> Bytes {
427        let mut buf = EncodeBuf::new();
428
429        buf.push_sequence(|buf| {
430            // msgData
431            match &self.data {
432                V3MessageData::Plaintext(scoped_pdu) => {
433                    scoped_pdu.encode(buf);
434                }
435                V3MessageData::Encrypted(ciphertext) => {
436                    buf.push_octet_string(ciphertext);
437                }
438            }
439
440            // msgSecurityParameters (as OCTET STRING)
441            buf.push_octet_string(&self.security_params);
442
443            // msgGlobalData
444            self.global_data.encode(buf);
445
446            // version
447            buf.push_integer(3);
448        });
449
450        buf.finish()
451    }
452
453    /// Decode from BER.
454    ///
455    /// For encrypted messages, returns `V3MessageData::Encrypted` with the raw ciphertext.
456    /// The caller must decrypt using USM before accessing the scoped PDU.
457    pub fn decode(data: Bytes) -> Result<Self> {
458        let mut decoder = Decoder::new(data);
459        let mut seq = decoder.read_sequence()?;
460
461        // Version
462        let version = seq.read_integer()?;
463        if version != 3 {
464            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), version, kind = %DecodeErrorKind::UnknownVersion(version) }, "decode error");
465            return Err(Error::MalformedResponse {
466                target: UNKNOWN_TARGET,
467            }
468            .boxed());
469        }
470
471        Self::decode_from_sequence(&mut seq)
472    }
473
474    /// Decode from a sequence decoder where version has already been read.
475    pub(crate) fn decode_from_sequence(seq: &mut Decoder) -> Result<Self> {
476        // msgGlobalData
477        let global_data = MsgGlobalData::decode(seq)?;
478
479        // msgSecurityParameters (OCTET STRING containing USM params)
480        let security_params = seq.read_octet_string()?;
481
482        // msgData - either plaintext SEQUENCE or encrypted OCTET STRING
483        let data = if global_data.msg_flags.security_level.requires_priv() {
484            // Encrypted: expect OCTET STRING
485            let encrypted = seq.read_octet_string()?;
486            V3MessageData::Encrypted(encrypted)
487        } else {
488            // Plaintext: expect SEQUENCE (ScopedPDU)
489            let scoped_pdu = ScopedPdu::decode(seq)?;
490            V3MessageData::Plaintext(scoped_pdu)
491        };
492
493        Ok(Self {
494            global_data,
495            security_params,
496            data,
497        })
498    }
499
500    /// Create a discovery request message.
501    ///
502    /// This is sent to discover the engine ID and time of a remote SNMP engine.
503    /// Uses empty security parameters and no authentication.
504    #[must_use]
505    pub fn discovery_request(msg_id: i32) -> Self {
506        let global_data = MsgGlobalData::new(
507            msg_id,
508            65507, // max UDP size
509            MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
510        );
511
512        // Empty USM security parameters for discovery
513        let security_params = crate::v3::UsmSecurityParams::empty().encode();
514
515        // Empty scoped PDU with Report request
516        let pdu = Pdu::get_request(0, &[]);
517        let scoped_pdu = ScopedPdu::with_empty_context(pdu);
518
519        Self::new(global_data, security_params, scoped_pdu)
520    }
521}
522
523/// RFC 3412 MPD failures that must be counted before the message is
524/// discarded (Sections 7.2.4 and 7.2.7).
525#[derive(Debug, Clone, Copy, PartialEq, Eq)]
526pub(crate) enum MpdFailure {
527    /// Invalid msgFlags (priv without auth) - snmpInvalidMsgs.
528    InvalidMsgFlags,
529    /// Unrecognized msgSecurityModel - snmpUnknownSecurityModels.
530    UnknownSecurityModel,
531}
532
533/// Classify a failed [`V3Message::decode`] as an MPD-countable failure.
534///
535/// Re-parses only the header path so it stays in lockstep with
536/// [`MsgGlobalData::decode`]: the first countable defect wins, and `None`
537/// means the failure was some other malformation.
538pub(crate) fn classify_mpd_failure(data: Bytes) -> Option<MpdFailure> {
539    let mut decoder = Decoder::new(data);
540    let mut seq = decoder.read_sequence().ok()?;
541    if seq.read_integer().ok()? != 3 {
542        return None;
543    }
544    let mut global = seq.read_sequence().ok()?;
545    // Mirror `MsgGlobalData::decode`'s fail-fast order so a failure is only
546    // attributed to the field that actually caused decode to reject. A
547    // countable defect at a later field is unreachable once decode would have
548    // stopped at an earlier one (out-of-range msgID/msgMaxSize, wrong-length
549    // msgFlags), and those earlier rejections are ASN.1/header errors rather
550    // than snmpInvalidMsgs/snmpUnknownSecurityModels, so they return None.
551    let msg_id = global.read_integer().ok()?;
552    if msg_id < 0 {
553        return None;
554    }
555    let msg_max_size = global.read_integer().ok()?;
556    if msg_max_size < MSG_MAX_SIZE_MINIMUM {
557        return None;
558    }
559    let flags_bytes = global.read_octet_string().ok()?;
560    if flags_bytes.len() != 1 {
561        return None;
562    }
563    if MsgFlags::from_byte(flags_bytes[0]).is_err() {
564        return Some(MpdFailure::InvalidMsgFlags);
565    }
566    let model = global.read_integer().ok()?;
567    if SecurityModel::from_i32(model).is_none() {
568        return Some(MpdFailure::UnknownSecurityModel);
569    }
570    None
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::oid;
577
578    #[test]
579    fn test_security_level_flags() {
580        assert_eq!(SecurityLevel::NoAuthNoPriv.to_flags(), 0x00);
581        assert_eq!(SecurityLevel::AuthNoPriv.to_flags(), 0x01);
582        assert_eq!(SecurityLevel::AuthPriv.to_flags(), 0x03);
583
584        assert_eq!(
585            SecurityLevel::from_flags(0x00),
586            Some(SecurityLevel::NoAuthNoPriv)
587        );
588        assert_eq!(
589            SecurityLevel::from_flags(0x01),
590            Some(SecurityLevel::AuthNoPriv)
591        );
592        assert_eq!(
593            SecurityLevel::from_flags(0x03),
594            Some(SecurityLevel::AuthPriv)
595        );
596        assert_eq!(SecurityLevel::from_flags(0x02), None); // Invalid
597    }
598
599    #[test]
600    fn security_level_try_from_u8() {
601        assert_eq!(
602            SecurityLevel::try_from(0x00),
603            Ok(SecurityLevel::NoAuthNoPriv)
604        );
605        assert_eq!(SecurityLevel::try_from(0x01), Ok(SecurityLevel::AuthNoPriv));
606        assert_eq!(SecurityLevel::try_from(0x03), Ok(SecurityLevel::AuthPriv));
607        assert_eq!(SecurityLevel::try_from(0x02), Err(0x02));
608    }
609
610    #[test]
611    fn security_level_into_u8() {
612        assert_eq!(u8::from(SecurityLevel::NoAuthNoPriv), 0x00);
613        assert_eq!(u8::from(SecurityLevel::AuthNoPriv), 0x01);
614        assert_eq!(u8::from(SecurityLevel::AuthPriv), 0x03);
615    }
616
617    #[test]
618    fn test_msg_flags_roundtrip() {
619        let flags = MsgFlags::new(SecurityLevel::AuthPriv, true);
620        let byte = flags.to_byte();
621        assert_eq!(byte, 0x07); // auth=1, priv=1, reportable=1
622
623        let decoded = MsgFlags::from_byte(byte).unwrap();
624        assert_eq!(decoded.security_level, SecurityLevel::AuthPriv);
625        assert!(decoded.reportable);
626    }
627
628    /// `classify_mpd_failure` must attribute a failure only to the field that
629    /// actually caused `MsgGlobalData::decode` to reject, matching its
630    /// fail-fast order: a message rejected at an earlier field (here a
631    /// negative msgID) must not be blamed on a later unknown security model.
632    #[test]
633    fn classify_mpd_failure_mirrors_decode_fail_fast() {
634        use crate::pdu::Pdu;
635        use crate::v3::UsmSecurityParams;
636
637        // Valid noAuthNoPriv v3 message; single-byte msgID and model keep the
638        // byte patches below length-preserving.
639        let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
640        let usm =
641            UsmSecurityParams::new(Bytes::from_static(b"eid"), 0, 0, Bytes::from_static(b"u"));
642        let scoped = ScopedPdu::new(
643            Bytes::from_static(b"eid"),
644            Bytes::new(),
645            Pdu::get_request(42, &[]),
646        );
647        let base = V3Message::new(global, usm.encode(), scoped).encode();
648
649        let patch = |data: &Bytes, pattern: &[u8], off: usize, val: u8| -> Bytes {
650            let mut b = data.to_vec();
651            let pos = b
652                .windows(pattern.len())
653                .position(|w| w == pattern)
654                .expect("pattern not found");
655            b[pos + off] = val;
656            Bytes::from(b)
657        };
658
659        // Only the security model is unknown -> UnknownSecurityModel.
660        let model_pattern = [0x04, 0x01, 0x04, 0x02, 0x01, 0x03];
661        let unknown_model = patch(&base, &model_pattern, 5, 99);
662        assert_eq!(
663            classify_mpd_failure(unknown_model),
664            Some(MpdFailure::UnknownSecurityModel)
665        );
666
667        // Decode rejects at the negative msgID before reaching the model, so
668        // the unknown model must not be attributed.
669        let neg_id = patch(&base, &[0x02, 0x01, 0x01, 0x02, 0x03], 2, 0x81);
670        let neg_id_unknown_model = patch(&neg_id, &model_pattern, 5, 99);
671        assert_eq!(classify_mpd_failure(neg_id_unknown_model), None);
672    }
673
674    #[test]
675    fn test_msg_global_data_roundtrip() {
676        let global =
677            MsgGlobalData::new(12345, 1472, MsgFlags::new(SecurityLevel::AuthNoPriv, true));
678
679        let mut buf = EncodeBuf::new();
680        global.encode(&mut buf);
681        let encoded = buf.finish();
682
683        let mut decoder = Decoder::new(encoded);
684        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
685
686        assert_eq!(decoded.msg_id, 12345);
687        assert_eq!(decoded.msg_max_size, 1472);
688        assert_eq!(decoded.msg_flags.security_level, SecurityLevel::AuthNoPriv);
689        assert!(decoded.msg_flags.reportable);
690        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
691    }
692
693    #[test]
694    fn test_scoped_pdu_roundtrip() {
695        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
696        let scoped = ScopedPdu::new(b"engine".as_slice(), b"ctx".as_slice(), pdu);
697
698        let mut buf = EncodeBuf::new();
699        scoped.encode(&mut buf);
700        let encoded = buf.finish();
701
702        let mut decoder = Decoder::new(encoded);
703        let decoded = ScopedPdu::decode(&mut decoder).unwrap();
704
705        assert_eq!(decoded.context_engine_id.as_ref(), b"engine");
706        assert_eq!(decoded.context_name.as_ref(), b"ctx");
707        assert_eq!(decoded.pdu.request_id, 42);
708    }
709
710    #[test]
711    fn test_v3_message_plaintext_roundtrip() {
712        let global =
713            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
714        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
715        let scoped = ScopedPdu::with_empty_context(pdu);
716        let msg = V3Message::new(global, Bytes::from_static(b"usm-params"), scoped);
717
718        let encoded = msg.encode();
719        let decoded = V3Message::decode(encoded).unwrap();
720
721        assert_eq!(decoded.global_data.msg_id, 100);
722        assert_eq!(decoded.security_level(), SecurityLevel::NoAuthNoPriv);
723        assert_eq!(decoded.security_params.as_ref(), b"usm-params");
724
725        let scoped_pdu = decoded.scoped_pdu().unwrap();
726        assert_eq!(scoped_pdu.pdu.request_id, 42);
727    }
728
729    #[test]
730    fn test_v3_message_encrypted_roundtrip() {
731        let global = MsgGlobalData::new(200, 1472, MsgFlags::new(SecurityLevel::AuthPriv, false));
732        let msg = V3Message::new_encrypted(
733            global,
734            Bytes::from_static(b"usm-params"),
735            Bytes::from_static(b"encrypted-data"),
736        );
737
738        let encoded = msg.encode();
739        let decoded = V3Message::decode(encoded).unwrap();
740
741        assert_eq!(decoded.global_data.msg_id, 200);
742        assert_eq!(decoded.security_level(), SecurityLevel::AuthPriv);
743
744        match &decoded.data {
745            V3MessageData::Encrypted(data) => {
746                assert_eq!(data.as_ref(), b"encrypted-data");
747            }
748            V3MessageData::Plaintext(_) => panic!("expected encrypted data"),
749        }
750    }
751
752    #[test]
753    fn test_msg_global_data_rejects_msg_max_size_below_minimum() {
754        // Encode with invalid msgMaxSize (below 484)
755        let global = MsgGlobalData {
756            msg_id: 100,
757            msg_max_size: 400, // Below RFC 3412 minimum of 484
758            msg_flags: MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
759            msg_security_model: SecurityModel::Usm,
760        };
761
762        let mut buf = EncodeBuf::new();
763        global.encode(&mut buf);
764        let encoded = buf.finish();
765
766        let mut decoder = Decoder::new(encoded);
767        let result = MsgGlobalData::decode(&mut decoder);
768
769        assert!(result.is_err());
770        assert!(matches!(
771            *result.unwrap_err(),
772            Error::MalformedResponse { .. }
773        ));
774    }
775
776    #[test]
777    fn test_msg_global_data_accepts_msg_max_size_at_minimum() {
778        // 484 is exactly the RFC 3412 minimum
779        let global = MsgGlobalData::new(100, 484, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
780
781        let mut buf = EncodeBuf::new();
782        global.encode(&mut buf);
783        let encoded = buf.finish();
784
785        let mut decoder = Decoder::new(encoded);
786        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
787
788        assert_eq!(decoded.msg_max_size, 484);
789    }
790
791    #[test]
792    fn test_msg_global_data_rejects_unknown_security_model() {
793        // Manually build encoded data with unknown security model
794        // SEQUENCE { msg_id, msg_max_size, msgFlags, msgSecurityModel=99 }
795        let mut buf = EncodeBuf::new();
796        buf.push_sequence(|buf| {
797            buf.push_integer(99); // unknown security model
798            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
799            buf.push_integer(1472); // msg_max_size
800            buf.push_integer(100); // msg_id
801        });
802        let encoded = buf.finish();
803
804        let mut decoder = Decoder::new(encoded);
805        let result = MsgGlobalData::decode(&mut decoder);
806
807        assert!(result.is_err());
808        assert!(matches!(
809            *result.unwrap_err(),
810            Error::MalformedResponse { .. }
811        ));
812    }
813
814    #[test]
815    fn test_msg_global_data_rejects_zero_length_msg_flags() {
816        // RFC 3412 Section 6.4: msgFlags OCTET STRING (SIZE(1))
817        // SEQUENCE { msg_id, msg_max_size, msgFlags=<empty>, msgSecurityModel=3(Usm) }
818        let mut buf = EncodeBuf::new();
819        buf.push_sequence(|buf| {
820            buf.push_integer(3); // Usm
821            buf.push_octet_string(&[]); // zero-length msgFlags
822            buf.push_integer(1472); // msg_max_size
823            buf.push_integer(100); // msg_id
824        });
825        let encoded = buf.finish();
826
827        let mut decoder = Decoder::new(encoded);
828        let result = MsgGlobalData::decode(&mut decoder);
829
830        assert!(result.is_err());
831        assert!(matches!(
832            *result.unwrap_err(),
833            Error::MalformedResponse { .. }
834        ));
835    }
836
837    #[test]
838    fn test_msg_global_data_rejects_two_byte_msg_flags() {
839        // RFC 3412 Section 6.4: msgFlags OCTET STRING (SIZE(1))
840        // SEQUENCE { msg_id, msg_max_size, msgFlags=<two bytes>, msgSecurityModel=3(Usm) }
841        let mut buf = EncodeBuf::new();
842        buf.push_sequence(|buf| {
843            buf.push_integer(3); // Usm
844            buf.push_octet_string(&[0x04, 0x00]); // two-byte msgFlags
845            buf.push_integer(1472); // msg_max_size
846            buf.push_integer(100); // msg_id
847        });
848        let encoded = buf.finish();
849
850        let mut decoder = Decoder::new(encoded);
851        let result = MsgGlobalData::decode(&mut decoder);
852
853        assert!(result.is_err());
854        assert!(matches!(
855            *result.unwrap_err(),
856            Error::MalformedResponse { .. }
857        ));
858    }
859
860    #[test]
861    fn test_msg_global_data_accepts_one_byte_msg_flags() {
862        // Control: a valid single-byte msgFlags (reportable, noAuthNoPriv) must be accepted
863        // SEQUENCE { msg_id, msg_max_size, msgFlags=[0x04], msgSecurityModel=3(Usm) }
864        let mut buf = EncodeBuf::new();
865        buf.push_sequence(|buf| {
866            buf.push_integer(3); // Usm
867            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
868            buf.push_integer(1472); // msg_max_size
869            buf.push_integer(100); // msg_id
870        });
871        let encoded = buf.finish();
872
873        let mut decoder = Decoder::new(encoded);
874        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
875
876        assert_eq!(decoded.msg_flags, MsgFlags::from_byte(0x04).unwrap());
877        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
878    }
879
880    #[test]
881    fn test_msg_global_data_accepts_usm_security_model() {
882        // USM (3) should be accepted
883        let global =
884            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
885
886        let mut buf = EncodeBuf::new();
887        global.encode(&mut buf);
888        let encoded = buf.finish();
889
890        let mut decoder = Decoder::new(encoded);
891        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
892
893        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
894    }
895
896    // RFC 3412 bounds tests for msgID and msgMaxSize
897    //
898    // RFC 3412 HeaderData definition specifies:
899    //   msgID INTEGER (0..2147483647)
900    //   msgMaxSize INTEGER (484..2147483647)
901    //
902    // Values outside these ranges should be rejected.
903
904    #[test]
905    fn test_msg_global_data_rejects_negative_msg_id() {
906        // RFC 3412: msgID must be in range [0..2147483647]
907        // Negative values should be rejected
908        let mut buf = EncodeBuf::new();
909        buf.push_sequence(|buf| {
910            buf.push_integer(3); // USM security model
911            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
912            buf.push_integer(1472); // valid msg_max_size
913            buf.push_integer(-1); // negative msg_id
914        });
915        let encoded = buf.finish();
916
917        let mut decoder = Decoder::new(encoded);
918        let result = MsgGlobalData::decode(&mut decoder);
919
920        assert!(result.is_err());
921        assert!(matches!(
922            *result.unwrap_err(),
923            Error::MalformedResponse { .. }
924        ));
925    }
926
927    #[test]
928    fn test_msg_global_data_rejects_negative_msg_max_size() {
929        // RFC 3412: msgMaxSize must be in range [484..2147483647]
930        // Negative values (from signed integer interpretation) should be rejected
931        let mut buf = EncodeBuf::new();
932        buf.push_sequence(|buf| {
933            buf.push_integer(3); // USM security model
934            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
935            buf.push_integer(-1); // negative msg_max_size (would be > 2^31-1 unsigned)
936            buf.push_integer(100); // valid msg_id
937        });
938        let encoded = buf.finish();
939
940        let mut decoder = Decoder::new(encoded);
941        let result = MsgGlobalData::decode(&mut decoder);
942
943        assert!(result.is_err());
944        assert!(matches!(
945            *result.unwrap_err(),
946            Error::MalformedResponse { .. }
947        ));
948    }
949
950    #[test]
951    fn test_msg_global_data_accepts_msg_id_at_zero() {
952        // RFC 3412: msgID 0 is at the lower bound, should be accepted
953        let mut buf = EncodeBuf::new();
954        buf.push_sequence(|buf| {
955            buf.push_integer(3); // USM
956            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
957            buf.push_integer(1472); // valid msg_max_size
958            buf.push_integer(0); // msg_id at lower bound
959        });
960        let encoded = buf.finish();
961
962        let mut decoder = Decoder::new(encoded);
963        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
964
965        assert_eq!(decoded.msg_id, 0);
966    }
967
968    #[test]
969    fn test_msg_global_data_accepts_msg_id_at_maximum() {
970        // RFC 3412: msgID 2147483647 is at the upper bound, should be accepted
971        let mut buf = EncodeBuf::new();
972        buf.push_sequence(|buf| {
973            buf.push_integer(3); // USM
974            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
975            buf.push_integer(1472); // valid msg_max_size
976            buf.push_integer(i32::MAX); // msg_id at upper bound (2147483647)
977        });
978        let encoded = buf.finish();
979
980        let mut decoder = Decoder::new(encoded);
981        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
982
983        assert_eq!(decoded.msg_id, i32::MAX);
984    }
985
986    #[test]
987    fn test_msg_global_data_accepts_msg_max_size_at_maximum() {
988        // RFC 3412: msgMaxSize 2147483647 is at the upper bound, should be accepted
989        let mut buf = EncodeBuf::new();
990        buf.push_sequence(|buf| {
991            buf.push_integer(3); // USM
992            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
993            buf.push_integer(i32::MAX); // msg_max_size at upper bound (2147483647)
994            buf.push_integer(100); // valid msg_id
995        });
996        let encoded = buf.finish();
997
998        let mut decoder = Decoder::new(encoded);
999        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
1000
1001        assert_eq!(decoded.msg_max_size, i32::MAX);
1002    }
1003}