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        // These ASN.1 constraints must be checked against the complete BER
215        // value before it is narrowed to i32.
216        let msg_id = seq.read_bounded_integer(0, i32::MAX)?;
217        let msg_max_size = seq.read_bounded_integer(MSG_MAX_SIZE_MINIMUM, i32::MAX)?;
218
219        let flags_bytes = seq.read_octet_string()?;
220        if flags_bytes.len() != 1 {
221            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), expected = 1, actual = flags_bytes.len() }, "invalid msgFlags length");
222            return Err(Error::MalformedResponse {
223                target: UNKNOWN_TARGET,
224            }
225            .boxed());
226        }
227        let msg_flags = MsgFlags::from_byte(flags_bytes[0])?;
228
229        let msg_security_model_raw = seq.read_bounded_integer(1, i32::MAX)?;
230        // Reject unknown security models per RFC 3412 Section 7.2
231        let msg_security_model =
232            SecurityModel::from_i32(msg_security_model_raw).ok_or_else(|| {
233                tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), model = msg_security_model_raw, kind = %DecodeErrorKind::UnknownSecurityModel(msg_security_model_raw) }, "decode error");
234                Error::MalformedResponse {
235                    target: UNKNOWN_TARGET,
236                }
237                .boxed()
238            })?;
239
240        if !seq.is_empty() {
241            return Err(Error::MalformedResponse {
242                target: UNKNOWN_TARGET,
243            }
244            .boxed());
245        }
246
247        Ok(Self {
248            msg_id,
249            msg_max_size,
250            msg_flags,
251            msg_security_model,
252        })
253    }
254}
255
256/// Scoped PDU (contextEngineID + contextName + PDU).
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct ScopedPdu {
259    /// Context engine ID (typically same as authoritative engine ID)
260    pub context_engine_id: Bytes,
261    /// Context name (typically empty string)
262    pub context_name: Bytes,
263    /// The actual PDU
264    pub pdu: Pdu,
265}
266
267impl ScopedPdu {
268    /// Create a new scoped PDU.
269    pub fn new(
270        context_engine_id: impl Into<Bytes>,
271        context_name: impl Into<Bytes>,
272        pdu: Pdu,
273    ) -> Self {
274        Self {
275            context_engine_id: context_engine_id.into(),
276            context_name: context_name.into(),
277            pdu,
278        }
279    }
280
281    /// Create with empty context (most common case).
282    #[must_use]
283    pub fn with_empty_context(pdu: Pdu) -> Self {
284        Self {
285            context_engine_id: Bytes::new(),
286            context_name: Bytes::new(),
287            pdu,
288        }
289    }
290
291    /// Encode to buffer.
292    pub fn encode(&self, buf: &mut EncodeBuf) {
293        buf.push_sequence(|buf| {
294            self.pdu.encode(buf);
295            buf.push_octet_string(&self.context_name);
296            buf.push_octet_string(&self.context_engine_id);
297        });
298    }
299
300    /// Encode to bytes.
301    pub fn encode_to_bytes(&self) -> Bytes {
302        let mut buf = EncodeBuf::new();
303        self.encode(&mut buf);
304        buf.finish()
305    }
306
307    /// Decode from decoder.
308    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
309        let mut seq = decoder.read_sequence()?;
310
311        let context_engine_id = seq.read_octet_string()?;
312        let context_name = seq.read_octet_string()?;
313        let pdu = Pdu::decode(&mut seq)?;
314
315        if !seq.is_empty() {
316            return Err(Error::MalformedResponse {
317                target: UNKNOWN_TARGET,
318            }
319            .boxed());
320        }
321
322        Ok(Self {
323            context_engine_id,
324            context_name,
325            pdu,
326        })
327    }
328}
329
330/// `SNMPv3` message.
331#[derive(Debug, Clone, PartialEq, Eq)]
332pub struct V3Message {
333    /// Global data (header)
334    pub global_data: MsgGlobalData,
335    /// Security parameters (opaque, USM-encoded)
336    pub security_params: Bytes,
337    /// Message data - either plaintext `ScopedPdu` or encrypted bytes
338    pub data: V3MessageData,
339}
340
341/// Message data payload.
342#[derive(Debug, Clone, PartialEq, Eq)]
343pub enum V3MessageData {
344    /// Plaintext scoped PDU (noAuthNoPriv or authNoPriv)
345    Plaintext(ScopedPdu),
346    /// Encrypted scoped PDU (authPriv) - raw ciphertext
347    Encrypted(Bytes),
348}
349
350impl V3Message {
351    /// Create a new V3 message with plaintext data.
352    pub fn new(global_data: MsgGlobalData, security_params: Bytes, scoped_pdu: ScopedPdu) -> Self {
353        Self {
354            global_data,
355            security_params,
356            data: V3MessageData::Plaintext(scoped_pdu),
357        }
358    }
359
360    /// Create a new V3 message with encrypted data.
361    pub fn new_encrypted(
362        global_data: MsgGlobalData,
363        security_params: Bytes,
364        encrypted: Bytes,
365    ) -> Self {
366        Self {
367            global_data,
368            security_params,
369            data: V3MessageData::Encrypted(encrypted),
370        }
371    }
372
373    /// Get the scoped PDU if available (plaintext only).
374    pub fn scoped_pdu(&self) -> Option<&ScopedPdu> {
375        match &self.data {
376            V3MessageData::Plaintext(pdu) => Some(pdu),
377            V3MessageData::Encrypted(_) => None,
378        }
379    }
380
381    /// Consume and return the scoped PDU if available.
382    pub fn into_scoped_pdu(self) -> Option<ScopedPdu> {
383        match self.data {
384            V3MessageData::Plaintext(pdu) => Some(pdu),
385            V3MessageData::Encrypted(_) => None,
386        }
387    }
388
389    /// Get the PDU if available (convenience method).
390    pub fn pdu(&self) -> Option<&Pdu> {
391        self.scoped_pdu().map(|s| &s.pdu)
392    }
393
394    /// Consume and return the PDU.
395    pub fn into_pdu(self) -> Option<Pdu> {
396        self.into_scoped_pdu().map(|s| s.pdu)
397    }
398
399    /// Get the message ID.
400    pub fn msg_id(&self) -> i32 {
401        self.global_data.msg_id
402    }
403
404    /// Get the security level.
405    pub fn security_level(&self) -> SecurityLevel {
406        self.global_data.msg_flags.security_level
407    }
408
409    /// Encode to BER.
410    ///
411    /// Note: For authenticated messages, the caller must:
412    /// 1. Encode with placeholder auth params (12 zero bytes for HMAC-96)
413    /// 2. Compute HMAC over the entire encoded message
414    /// 3. Replace the placeholder with the actual HMAC
415    pub fn encode(&self) -> Bytes {
416        let mut buf = EncodeBuf::new();
417
418        buf.push_sequence(|buf| {
419            // msgData
420            match &self.data {
421                V3MessageData::Plaintext(scoped_pdu) => {
422                    scoped_pdu.encode(buf);
423                }
424                V3MessageData::Encrypted(ciphertext) => {
425                    buf.push_octet_string(ciphertext);
426                }
427            }
428
429            // msgSecurityParameters (as OCTET STRING)
430            buf.push_octet_string(&self.security_params);
431
432            // msgGlobalData
433            self.global_data.encode(buf);
434
435            // version
436            buf.push_integer(3);
437        });
438
439        buf.finish()
440    }
441
442    /// Decode from BER.
443    ///
444    /// For encrypted messages, returns `V3MessageData::Encrypted` with the raw
445    /// ciphertext. For plaintext messages this parses the scoped PDU without
446    /// performing USM authentication. Receive paths handling untrusted input
447    /// should use [`RawV3Message::decode`] so authentication and timeliness can
448    /// precede scoped-PDU parsing.
449    pub fn decode(data: Bytes) -> Result<Self> {
450        let mut decoder = Decoder::new(data);
451        let mut seq = decoder.read_sequence()?;
452
453        // Version
454        let version = seq.read_bounded_integer(0, i32::MAX)?;
455        if version != 3 {
456            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), version, kind = %DecodeErrorKind::UnknownVersion(version) }, "decode error");
457            return Err(Error::MalformedResponse {
458                target: UNKNOWN_TARGET,
459            }
460            .boxed());
461        }
462
463        Self::decode_from_sequence(&mut seq)
464    }
465
466    /// Decode from a sequence decoder where version has already been read.
467    pub(crate) fn decode_from_sequence(seq: &mut Decoder) -> Result<Self> {
468        // msgGlobalData
469        let global_data = MsgGlobalData::decode(seq)?;
470
471        // msgSecurityParameters (OCTET STRING containing USM params)
472        let security_params = seq.read_octet_string()?;
473
474        // msgData - either plaintext SEQUENCE or encrypted OCTET STRING
475        let data = if global_data.msg_flags.security_level.requires_priv() {
476            // Encrypted: expect OCTET STRING
477            let encrypted = seq.read_octet_string()?;
478            V3MessageData::Encrypted(encrypted)
479        } else {
480            // Plaintext: expect SEQUENCE (ScopedPDU)
481            let scoped_pdu = ScopedPdu::decode(seq)?;
482            V3MessageData::Plaintext(scoped_pdu)
483        };
484
485        Ok(Self {
486            global_data,
487            security_params,
488            data,
489        })
490    }
491
492    /// Create a discovery request message.
493    ///
494    /// This is sent to discover a remote SNMP engine's identity and message-size
495    /// limit. The response is unauthenticated, so its boots/time tuple must not
496    /// establish trusted time; authenticated communication performs that step.
497    /// Uses empty security parameters and no authentication.
498    #[must_use]
499    pub fn discovery_request(msg_id: i32) -> Self {
500        let global_data = MsgGlobalData::new(
501            msg_id,
502            65507, // max UDP size
503            MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
504        );
505
506        // Empty USM security parameters for discovery
507        let security_params = crate::v3::UsmSecurityParams::empty().encode();
508
509        // Empty scoped PDU with Report request
510        let pdu = Pdu::get_request(0, &[]);
511        let scoped_pdu = ScopedPdu::with_empty_context(pdu);
512
513        Self::new(global_data, security_params, scoped_pdu)
514    }
515}
516
517/// An `SNMPv3` message whose msgData has not been through security
518/// processing.
519///
520/// [`RawV3Message::decode`] parses only the outer envelope: version, global
521/// header, and the opaque security parameters. The scoped PDU stays as raw
522/// bytes (plaintext or ciphertext) so that authentication and decryption can
523/// run before any PDU parsing, in the RFC 3412 Section 7.2 order.
524#[derive(Debug, Clone, PartialEq, Eq)]
525pub struct RawV3Message {
526    /// Global data (header)
527    pub(crate) global_data: MsgGlobalData,
528    /// Security parameters (opaque, USM-encoded)
529    pub(crate) security_params: Bytes,
530    /// Raw msgData, form selected by the received privacy flag
531    pub(crate) msg_data: RawMsgData,
532}
533
534/// Raw msgData payload of a [`RawV3Message`].
535#[derive(Debug, Clone, PartialEq, Eq)]
536pub enum RawMsgData {
537    /// Unparsed plaintext `ScopedPDU` TLV bytes (noAuthNoPriv or authNoPriv)
538    Plaintext(Bytes),
539    /// Encrypted `ScopedPDU` ciphertext (authPriv)
540    Encrypted(Bytes),
541}
542
543impl RawV3Message {
544    /// Decode the outer envelope from BER without touching the scoped PDU.
545    ///
546    /// The received security level is derived from the message's own flags;
547    /// invalid flag combinations (privacy without authentication) are
548    /// rejected here, before any authentication or PDU processing.
549    pub fn decode(data: Bytes) -> Result<Self> {
550        let mut decoder = Decoder::new(data);
551        let mut seq = decoder.read_sequence()?;
552
553        let version = seq.read_bounded_integer(0, i32::MAX)?;
554        if version != 3 {
555            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), version, kind = %DecodeErrorKind::UnknownVersion(version) }, "decode error");
556            return Err(Error::MalformedResponse {
557                target: UNKNOWN_TARGET,
558            }
559            .boxed());
560        }
561
562        let global_data = MsgGlobalData::decode(&mut seq)?;
563        let security_params = seq.read_octet_string()?;
564
565        let msg_data = if global_data.msg_flags.security_level.requires_priv() {
566            RawMsgData::Encrypted(seq.read_octet_string()?)
567        } else {
568            // Capture the complete plaintext ScopedPDU TLV unparsed.
569            let start = seq.offset();
570            seq.skip_tlv()?;
571            RawMsgData::Plaintext(seq.as_bytes().slice(start..seq.offset()))
572        };
573
574        if !seq.is_empty() || !decoder.is_empty() {
575            return Err(Error::MalformedResponse {
576                target: UNKNOWN_TARGET,
577            }
578            .boxed());
579        }
580
581        Ok(Self {
582            global_data,
583            security_params,
584            msg_data,
585        })
586    }
587
588    /// Get the decoded global header.
589    pub fn global_data(&self) -> &MsgGlobalData {
590        &self.global_data
591    }
592
593    /// Get the opaque security parameters.
594    pub fn security_params(&self) -> &Bytes {
595        &self.security_params
596    }
597
598    /// Get the unprocessed message data.
599    pub fn msg_data(&self) -> &RawMsgData {
600        &self.msg_data
601    }
602
603    /// Get the message ID.
604    pub fn msg_id(&self) -> i32 {
605        self.global_data.msg_id
606    }
607
608    /// Get the security level indicated by the received flags.
609    pub fn security_level(&self) -> SecurityLevel {
610        self.global_data.msg_flags.security_level
611    }
612}
613
614/// RFC 3412 MPD failures that must be counted before the message is
615/// discarded (Sections 7.2.4 and 7.2.7).
616#[derive(Debug, Clone, Copy, PartialEq, Eq)]
617pub(crate) enum MpdFailure {
618    /// Invalid msgFlags (priv without auth) - snmpInvalidMsgs.
619    InvalidMsgFlags,
620    /// Unrecognized msgSecurityModel - snmpUnknownSecurityModels.
621    UnknownSecurityModel,
622}
623
624/// Classify a failed [`V3Message::decode`] as an MPD-countable failure.
625///
626/// Re-parses only the header path so it stays in lockstep with
627/// [`MsgGlobalData::decode`]: the first countable defect wins, and `None`
628/// means the failure was some other malformation.
629pub(crate) fn classify_mpd_failure(data: Bytes) -> Option<MpdFailure> {
630    let mut decoder = Decoder::new(data);
631    let mut seq = decoder.read_sequence().ok()?;
632    if seq.read_bounded_integer(0, i32::MAX).ok()? != 3 {
633        return None;
634    }
635    let mut global = seq.read_sequence().ok()?;
636    // Mirror `MsgGlobalData::decode`'s fail-fast order so a failure is only
637    // attributed to the field that actually caused decode to reject. A
638    // countable defect at a later field is unreachable once decode would have
639    // stopped at an earlier one (out-of-range msgID/msgMaxSize, wrong-length
640    // msgFlags), and those earlier rejections are ASN.1/header errors rather
641    // than snmpInvalidMsgs/snmpUnknownSecurityModels, so they return None.
642    global.read_bounded_integer(0, i32::MAX).ok()?;
643    global
644        .read_bounded_integer(MSG_MAX_SIZE_MINIMUM, i32::MAX)
645        .ok()?;
646    let flags_bytes = global.read_octet_string().ok()?;
647    if flags_bytes.len() != 1 {
648        return None;
649    }
650    if MsgFlags::from_byte(flags_bytes[0]).is_err() {
651        return Some(MpdFailure::InvalidMsgFlags);
652    }
653    let model = global.read_bounded_integer(1, i32::MAX).ok()?;
654    if SecurityModel::from_i32(model).is_none() {
655        return Some(MpdFailure::UnknownSecurityModel);
656    }
657    None
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use crate::oid;
664
665    fn push_integer_content(buf: &mut EncodeBuf, content: &[u8]) {
666        buf.push_bytes(content);
667        buf.push_length(content.len());
668        buf.push_tag(crate::ber::tag::universal::INTEGER);
669    }
670
671    fn global_data_with_integer_contents(
672        msg_id: &[u8],
673        msg_max_size: &[u8],
674        security_model: &[u8],
675    ) -> Bytes {
676        let mut buf = EncodeBuf::new();
677        buf.push_sequence(|buf| {
678            push_integer_content(buf, security_model);
679            buf.push_octet_string(&[0x04]);
680            push_integer_content(buf, msg_max_size);
681            push_integer_content(buf, msg_id);
682        });
683        buf.finish()
684    }
685
686    #[test]
687    fn test_security_level_flags() {
688        assert_eq!(SecurityLevel::NoAuthNoPriv.to_flags(), 0x00);
689        assert_eq!(SecurityLevel::AuthNoPriv.to_flags(), 0x01);
690        assert_eq!(SecurityLevel::AuthPriv.to_flags(), 0x03);
691
692        assert_eq!(
693            SecurityLevel::from_flags(0x00),
694            Some(SecurityLevel::NoAuthNoPriv)
695        );
696        assert_eq!(
697            SecurityLevel::from_flags(0x01),
698            Some(SecurityLevel::AuthNoPriv)
699        );
700        assert_eq!(
701            SecurityLevel::from_flags(0x03),
702            Some(SecurityLevel::AuthPriv)
703        );
704        assert_eq!(SecurityLevel::from_flags(0x02), None); // Invalid
705    }
706
707    #[test]
708    fn security_level_try_from_u8() {
709        assert_eq!(
710            SecurityLevel::try_from(0x00),
711            Ok(SecurityLevel::NoAuthNoPriv)
712        );
713        assert_eq!(SecurityLevel::try_from(0x01), Ok(SecurityLevel::AuthNoPriv));
714        assert_eq!(SecurityLevel::try_from(0x03), Ok(SecurityLevel::AuthPriv));
715        assert_eq!(SecurityLevel::try_from(0x02), Err(0x02));
716    }
717
718    #[test]
719    fn security_level_into_u8() {
720        assert_eq!(u8::from(SecurityLevel::NoAuthNoPriv), 0x00);
721        assert_eq!(u8::from(SecurityLevel::AuthNoPriv), 0x01);
722        assert_eq!(u8::from(SecurityLevel::AuthPriv), 0x03);
723    }
724
725    #[test]
726    fn test_msg_flags_roundtrip() {
727        let flags = MsgFlags::new(SecurityLevel::AuthPriv, true);
728        let byte = flags.to_byte();
729        assert_eq!(byte, 0x07); // auth=1, priv=1, reportable=1
730
731        let decoded = MsgFlags::from_byte(byte).unwrap();
732        assert_eq!(decoded.security_level, SecurityLevel::AuthPriv);
733        assert!(decoded.reportable);
734    }
735
736    /// `classify_mpd_failure` must attribute a failure only to the field that
737    /// actually caused `MsgGlobalData::decode` to reject, matching its
738    /// fail-fast order: a message rejected at an earlier field (here a
739    /// negative msgID) must not be blamed on a later unknown security model.
740    #[test]
741    fn classify_mpd_failure_mirrors_decode_fail_fast() {
742        use crate::pdu::Pdu;
743        use crate::v3::UsmSecurityParams;
744
745        // Valid noAuthNoPriv v3 message; single-byte msgID and model keep the
746        // byte patches below length-preserving.
747        let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
748        let usm =
749            UsmSecurityParams::new(Bytes::from_static(b"eid"), 0, 0, Bytes::from_static(b"u"));
750        let scoped = ScopedPdu::new(
751            Bytes::from_static(b"eid"),
752            Bytes::new(),
753            Pdu::get_request(42, &[]),
754        );
755        let base = V3Message::new(global, usm.encode(), scoped).encode();
756
757        let patch = |data: &Bytes, pattern: &[u8], off: usize, val: u8| -> Bytes {
758            let mut b = data.to_vec();
759            let pos = b
760                .windows(pattern.len())
761                .position(|w| w == pattern)
762                .expect("pattern not found");
763            b[pos + off] = val;
764            Bytes::from(b)
765        };
766
767        // Only the security model is unknown -> UnknownSecurityModel.
768        let model_pattern = [0x04, 0x01, 0x04, 0x02, 0x01, 0x03];
769        let unknown_model = patch(&base, &model_pattern, 5, 99);
770        assert_eq!(
771            classify_mpd_failure(unknown_model),
772            Some(MpdFailure::UnknownSecurityModel)
773        );
774
775        // Decode rejects at the negative msgID before reaching the model, so
776        // the unknown model must not be attributed.
777        let neg_id = patch(&base, &[0x02, 0x01, 0x01, 0x02, 0x03], 2, 0x81);
778        let neg_id_unknown_model = patch(&neg_id, &model_pattern, 5, 99);
779        assert_eq!(classify_mpd_failure(neg_id_unknown_model), None);
780    }
781
782    /// A valid envelope around a malformed plaintext scoped PDU must decode
783    /// as a raw message (the scoped PDU is not parsed), while the eager
784    /// decode fails. This is the invariant that lets HMAC verification run
785    /// before plaintext PDU parsing.
786    #[test]
787    fn raw_decode_does_not_parse_plaintext_scoped_pdu() {
788        let mut buf = EncodeBuf::new();
789        buf.push_sequence(|buf| {
790            // msgData: structurally a SEQUENCE TLV, but garbage inside
791            buf.push_sequence(|buf| {
792                buf.push_bytes(&[0xDE, 0xAD, 0xBE, 0xEF]);
793            });
794            buf.push_octet_string(b"usm-params");
795            MsgGlobalData::new(7, 65507, MsgFlags::new(SecurityLevel::AuthNoPriv, false))
796                .encode(buf);
797            buf.push_integer(3);
798        });
799        let encoded = buf.finish();
800
801        assert!(
802            V3Message::decode(encoded.clone()).is_err(),
803            "eager decode must reject the malformed scoped PDU"
804        );
805
806        let raw = RawV3Message::decode(encoded).unwrap();
807        assert_eq!(raw.msg_id(), 7);
808        assert_eq!(raw.security_level(), SecurityLevel::AuthNoPriv);
809        assert_eq!(raw.security_params.as_ref(), b"usm-params");
810        let RawMsgData::Plaintext(scoped) = raw.msg_data else {
811            panic!("expected plaintext msgData");
812        };
813        assert_eq!(scoped.as_ref(), &[0x30, 0x04, 0xDE, 0xAD, 0xBE, 0xEF]);
814    }
815
816    #[test]
817    fn v3_decoders_reject_over_width_version_alias() {
818        let global = MsgGlobalData::new(7, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
819        let scoped = ScopedPdu::with_empty_context(Pdu::get_request(42, &[]));
820        let security_params = crate::v3::UsmSecurityParams::empty().encode();
821
822        let mut buf = EncodeBuf::new();
823        buf.push_sequence(|buf| {
824            scoped.encode(buf);
825            buf.push_octet_string(&security_params);
826            global.encode(buf);
827            // 2^32 + 3 previously narrowed to the accepted v3 value.
828            push_integer_content(buf, &[0x01, 0x00, 0x00, 0x00, 0x03]);
829        });
830        let encoded = buf.finish();
831
832        assert!(V3Message::decode(encoded.clone()).is_err());
833        assert!(RawV3Message::decode(encoded.clone()).is_err());
834        assert!(crate::message::Message::decode(encoded).is_err());
835    }
836
837    /// The captured plaintext bytes are the complete ScopedPDU TLV, so a
838    /// later parse of a well-formed message succeeds from the raw bytes.
839    #[test]
840    fn raw_plaintext_bytes_reparse_as_scoped_pdu() {
841        let global = MsgGlobalData::new(9, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
842        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
843        let scoped = ScopedPdu::new(b"eng".as_slice(), b"ctx".as_slice(), pdu);
844        let msg = V3Message::new(global, Bytes::from_static(b"usm"), scoped);
845
846        let raw = RawV3Message::decode(msg.encode()).unwrap();
847        let RawMsgData::Plaintext(bytes) = raw.msg_data else {
848            panic!("expected plaintext msgData");
849        };
850        let mut decoder = Decoder::new(bytes);
851        let reparsed = ScopedPdu::decode(&mut decoder).unwrap();
852        assert_eq!(reparsed.context_engine_id.as_ref(), b"eng");
853        assert_eq!(reparsed.context_name.as_ref(), b"ctx");
854        assert_eq!(reparsed.pdu.request_id, 42);
855    }
856
857    /// authPriv messages keep their ciphertext untouched.
858    #[test]
859    fn raw_decode_keeps_ciphertext() {
860        let global = MsgGlobalData::new(200, 1472, MsgFlags::new(SecurityLevel::AuthPriv, false));
861        let msg = V3Message::new_encrypted(
862            global,
863            Bytes::from_static(b"usm-params"),
864            Bytes::from_static(b"encrypted-data"),
865        );
866
867        let raw = RawV3Message::decode(msg.encode()).unwrap();
868        assert_eq!(raw.security_level(), SecurityLevel::AuthPriv);
869        let RawMsgData::Encrypted(ciphertext) = raw.msg_data else {
870            panic!("expected encrypted msgData");
871        };
872        assert_eq!(ciphertext.as_ref(), b"encrypted-data");
873    }
874
875    /// Privacy without authentication is rejected during envelope decode,
876    /// before any authentication or PDU work can start.
877    #[test]
878    fn raw_decode_rejects_priv_without_auth_flags() {
879        let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
880        let pdu = Pdu::get_request(1, &[]);
881        let msg = V3Message::new(
882            global,
883            Bytes::from_static(b"usm"),
884            ScopedPdu::with_empty_context(pdu),
885        );
886        let mut bytes = msg.encode().to_vec();
887        // Locate the single-byte msgFlags OCTET STRING (0x04 0x01 0x04) and
888        // patch it to priv-without-auth (0x02).
889        let pos = bytes
890            .windows(3)
891            .position(|w| w == [0x04, 0x01, 0x04])
892            .expect("msgFlags not found");
893        bytes[pos + 2] = 0x02;
894
895        let result = RawV3Message::decode(Bytes::from(bytes));
896        assert!(result.is_err());
897        assert!(matches!(
898            *result.unwrap_err(),
899            Error::MalformedResponse { .. }
900        ));
901    }
902
903    /// Reserved and reportable flag bits do not alter the derived security
904    /// level (RFC 3412 Section 7.2 derives the level from the auth/priv bits
905    /// only).
906    #[test]
907    fn raw_decode_ignores_reserved_bits_for_level() {
908        let global = MsgGlobalData::new(1, 65507, MsgFlags::new(SecurityLevel::AuthNoPriv, false));
909        let pdu = Pdu::get_request(1, &[]);
910        let msg = V3Message::new(
911            global,
912            Bytes::from_static(b"usm"),
913            ScopedPdu::with_empty_context(pdu),
914        );
915        let mut bytes = msg.encode().to_vec();
916        let pos = bytes
917            .windows(3)
918            .position(|w| w == [0x04, 0x01, 0x01])
919            .expect("msgFlags not found");
920        // auth + reportable + a reserved bit
921        bytes[pos + 2] = 0x01 | 0x04 | 0x08;
922
923        let raw = RawV3Message::decode(Bytes::from(bytes)).unwrap();
924        assert_eq!(raw.security_level(), SecurityLevel::AuthNoPriv);
925        assert!(raw.global_data.msg_flags.reportable);
926    }
927
928    #[test]
929    fn raw_decode_rejects_trailing_envelope_fields() {
930        let global =
931            MsgGlobalData::new(17, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, false));
932        let scoped = ScopedPdu::with_empty_context(Pdu::get_request(23, &[]));
933
934        // Encode an extra INTEGER after msgData inside the outer sequence.
935        let mut with_outer_field = EncodeBuf::new();
936        with_outer_field.push_sequence(|buf| {
937            buf.push_integer(99);
938            scoped.encode(buf);
939            buf.push_octet_string(b"usm");
940            global.encode(buf);
941            buf.push_integer(3);
942        });
943        assert!(RawV3Message::decode(with_outer_field.finish()).is_err());
944
945        // Encode an extra INTEGER inside msgGlobalData.
946        let mut with_global_field = EncodeBuf::new();
947        with_global_field.push_sequence(|buf| {
948            scoped.encode(buf);
949            buf.push_octet_string(b"usm");
950            buf.push_sequence(|buf| {
951                buf.push_integer(99);
952                buf.push_integer(SecurityModel::Usm.as_i32());
953                buf.push_octet_string(&[0]);
954                buf.push_integer(1472);
955                buf.push_integer(17);
956            });
957            buf.push_integer(3);
958        });
959        assert!(RawV3Message::decode(with_global_field.finish()).is_err());
960
961        // Append another top-level TLV after an otherwise complete message.
962        let message = V3Message::new(global, Bytes::from_static(b"usm"), scoped);
963        let mut with_root_trailing = message.encode().to_vec();
964        with_root_trailing.extend_from_slice(&[0x05, 0]);
965        assert!(RawV3Message::decode(Bytes::from(with_root_trailing)).is_err());
966    }
967
968    #[test]
969    fn test_msg_global_data_roundtrip() {
970        let global =
971            MsgGlobalData::new(12345, 1472, MsgFlags::new(SecurityLevel::AuthNoPriv, true));
972
973        let mut buf = EncodeBuf::new();
974        global.encode(&mut buf);
975        let encoded = buf.finish();
976
977        let mut decoder = Decoder::new(encoded);
978        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
979
980        assert_eq!(decoded.msg_id, 12345);
981        assert_eq!(decoded.msg_max_size, 1472);
982        assert_eq!(decoded.msg_flags.security_level, SecurityLevel::AuthNoPriv);
983        assert!(decoded.msg_flags.reportable);
984        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
985    }
986
987    #[test]
988    fn test_scoped_pdu_roundtrip() {
989        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
990        let scoped = ScopedPdu::new(b"engine".as_slice(), b"ctx".as_slice(), pdu);
991
992        let mut buf = EncodeBuf::new();
993        scoped.encode(&mut buf);
994        let encoded = buf.finish();
995
996        let mut decoder = Decoder::new(encoded);
997        let decoded = ScopedPdu::decode(&mut decoder).unwrap();
998
999        assert_eq!(decoded.context_engine_id.as_ref(), b"engine");
1000        assert_eq!(decoded.context_name.as_ref(), b"ctx");
1001        assert_eq!(decoded.pdu.request_id, 42);
1002    }
1003
1004    #[test]
1005    fn scoped_pdu_rejects_trailing_sequence_fields() {
1006        let pdu = Pdu::get_request(42, &[]);
1007        let mut buf = EncodeBuf::new();
1008        buf.push_sequence(|buf| {
1009            buf.push_integer(99);
1010            pdu.encode(buf);
1011            buf.push_octet_string(b"ctx");
1012            buf.push_octet_string(b"engine");
1013        });
1014
1015        let mut decoder = Decoder::new(buf.finish());
1016        assert!(ScopedPdu::decode(&mut decoder).is_err());
1017    }
1018
1019    #[test]
1020    fn test_v3_message_plaintext_roundtrip() {
1021        let global =
1022            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
1023        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
1024        let scoped = ScopedPdu::with_empty_context(pdu);
1025        let msg = V3Message::new(global, Bytes::from_static(b"usm-params"), scoped);
1026
1027        let encoded = msg.encode();
1028        let decoded = V3Message::decode(encoded).unwrap();
1029
1030        assert_eq!(decoded.global_data.msg_id, 100);
1031        assert_eq!(decoded.security_level(), SecurityLevel::NoAuthNoPriv);
1032        assert_eq!(decoded.security_params.as_ref(), b"usm-params");
1033
1034        let scoped_pdu = decoded.scoped_pdu().unwrap();
1035        assert_eq!(scoped_pdu.pdu.request_id, 42);
1036    }
1037
1038    #[test]
1039    fn test_v3_message_encrypted_roundtrip() {
1040        let global = MsgGlobalData::new(200, 1472, MsgFlags::new(SecurityLevel::AuthPriv, false));
1041        let msg = V3Message::new_encrypted(
1042            global,
1043            Bytes::from_static(b"usm-params"),
1044            Bytes::from_static(b"encrypted-data"),
1045        );
1046
1047        let encoded = msg.encode();
1048        let decoded = V3Message::decode(encoded).unwrap();
1049
1050        assert_eq!(decoded.global_data.msg_id, 200);
1051        assert_eq!(decoded.security_level(), SecurityLevel::AuthPriv);
1052
1053        match &decoded.data {
1054            V3MessageData::Encrypted(data) => {
1055                assert_eq!(data.as_ref(), b"encrypted-data");
1056            }
1057            V3MessageData::Plaintext(_) => panic!("expected encrypted data"),
1058        }
1059    }
1060
1061    #[test]
1062    fn test_msg_global_data_rejects_msg_max_size_below_minimum() {
1063        // Encode with invalid msgMaxSize (below 484)
1064        let global = MsgGlobalData {
1065            msg_id: 100,
1066            msg_max_size: 400, // Below RFC 3412 minimum of 484
1067            msg_flags: MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
1068            msg_security_model: SecurityModel::Usm,
1069        };
1070
1071        let mut buf = EncodeBuf::new();
1072        global.encode(&mut buf);
1073        let encoded = buf.finish();
1074
1075        let mut decoder = Decoder::new(encoded);
1076        let result = MsgGlobalData::decode(&mut decoder);
1077
1078        assert!(result.is_err());
1079        assert!(matches!(
1080            *result.unwrap_err(),
1081            Error::MalformedResponse { .. }
1082        ));
1083    }
1084
1085    #[test]
1086    fn test_msg_global_data_accepts_msg_max_size_at_minimum() {
1087        // 484 is exactly the RFC 3412 minimum
1088        let global = MsgGlobalData::new(100, 484, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
1089
1090        let mut buf = EncodeBuf::new();
1091        global.encode(&mut buf);
1092        let encoded = buf.finish();
1093
1094        let mut decoder = Decoder::new(encoded);
1095        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
1096
1097        assert_eq!(decoded.msg_max_size, 484);
1098    }
1099
1100    #[test]
1101    fn test_msg_global_data_rejects_unknown_security_model() {
1102        // Manually build encoded data with unknown security model
1103        // SEQUENCE { msg_id, msg_max_size, msgFlags, msgSecurityModel=99 }
1104        let mut buf = EncodeBuf::new();
1105        buf.push_sequence(|buf| {
1106            buf.push_integer(99); // unknown security model
1107            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
1108            buf.push_integer(1472); // msg_max_size
1109            buf.push_integer(100); // msg_id
1110        });
1111        let encoded = buf.finish();
1112
1113        let mut decoder = Decoder::new(encoded);
1114        let result = MsgGlobalData::decode(&mut decoder);
1115
1116        assert!(result.is_err());
1117        assert!(matches!(
1118            *result.unwrap_err(),
1119            Error::MalformedResponse { .. }
1120        ));
1121    }
1122
1123    #[test]
1124    fn msg_global_data_rejects_over_width_integer_aliases() {
1125        const ZERO: &[u8] = &[0x00];
1126        const MSG_MAX_SIZE: &[u8] = &[0x05, 0xC0];
1127        const USM: &[u8] = &[0x03];
1128
1129        // Each value is 2^32 plus an otherwise accepted field value.
1130        let aliased_msg_id =
1131            global_data_with_integer_contents(&[0x01, 0x00, 0x00, 0x00, 0x00], MSG_MAX_SIZE, USM);
1132        let aliased_msg_max_size =
1133            global_data_with_integer_contents(ZERO, &[0x01, 0x00, 0x00, 0x05, 0xC0], USM);
1134        let aliased_security_model =
1135            global_data_with_integer_contents(ZERO, MSG_MAX_SIZE, &[0x01, 0x00, 0x00, 0x00, 0x03]);
1136
1137        for encoded in [aliased_msg_id, aliased_msg_max_size, aliased_security_model] {
1138            let mut decoder = Decoder::new(encoded);
1139            assert!(MsgGlobalData::decode(&mut decoder).is_err());
1140        }
1141    }
1142
1143    #[test]
1144    fn test_msg_global_data_rejects_zero_length_msg_flags() {
1145        // RFC 3412 Section 6.4: msgFlags OCTET STRING (SIZE(1))
1146        // SEQUENCE { msg_id, msg_max_size, msgFlags=<empty>, msgSecurityModel=3(Usm) }
1147        let mut buf = EncodeBuf::new();
1148        buf.push_sequence(|buf| {
1149            buf.push_integer(3); // Usm
1150            buf.push_octet_string(&[]); // zero-length msgFlags
1151            buf.push_integer(1472); // msg_max_size
1152            buf.push_integer(100); // msg_id
1153        });
1154        let encoded = buf.finish();
1155
1156        let mut decoder = Decoder::new(encoded);
1157        let result = MsgGlobalData::decode(&mut decoder);
1158
1159        assert!(result.is_err());
1160        assert!(matches!(
1161            *result.unwrap_err(),
1162            Error::MalformedResponse { .. }
1163        ));
1164    }
1165
1166    #[test]
1167    fn test_msg_global_data_rejects_two_byte_msg_flags() {
1168        // RFC 3412 Section 6.4: msgFlags OCTET STRING (SIZE(1))
1169        // SEQUENCE { msg_id, msg_max_size, msgFlags=<two bytes>, msgSecurityModel=3(Usm) }
1170        let mut buf = EncodeBuf::new();
1171        buf.push_sequence(|buf| {
1172            buf.push_integer(3); // Usm
1173            buf.push_octet_string(&[0x04, 0x00]); // two-byte msgFlags
1174            buf.push_integer(1472); // msg_max_size
1175            buf.push_integer(100); // msg_id
1176        });
1177        let encoded = buf.finish();
1178
1179        let mut decoder = Decoder::new(encoded);
1180        let result = MsgGlobalData::decode(&mut decoder);
1181
1182        assert!(result.is_err());
1183        assert!(matches!(
1184            *result.unwrap_err(),
1185            Error::MalformedResponse { .. }
1186        ));
1187    }
1188
1189    #[test]
1190    fn test_msg_global_data_accepts_one_byte_msg_flags() {
1191        // Control: a valid single-byte msgFlags (reportable, noAuthNoPriv) must be accepted
1192        // SEQUENCE { msg_id, msg_max_size, msgFlags=[0x04], msgSecurityModel=3(Usm) }
1193        let mut buf = EncodeBuf::new();
1194        buf.push_sequence(|buf| {
1195            buf.push_integer(3); // Usm
1196            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
1197            buf.push_integer(1472); // msg_max_size
1198            buf.push_integer(100); // msg_id
1199        });
1200        let encoded = buf.finish();
1201
1202        let mut decoder = Decoder::new(encoded);
1203        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
1204
1205        assert_eq!(decoded.msg_flags, MsgFlags::from_byte(0x04).unwrap());
1206        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
1207    }
1208
1209    #[test]
1210    fn test_msg_global_data_accepts_usm_security_model() {
1211        // USM (3) should be accepted
1212        let global =
1213            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
1214
1215        let mut buf = EncodeBuf::new();
1216        global.encode(&mut buf);
1217        let encoded = buf.finish();
1218
1219        let mut decoder = Decoder::new(encoded);
1220        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
1221
1222        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
1223    }
1224
1225    // RFC 3412 bounds tests for msgID and msgMaxSize
1226    //
1227    // RFC 3412 HeaderData definition specifies:
1228    //   msgID INTEGER (0..2147483647)
1229    //   msgMaxSize INTEGER (484..2147483647)
1230    //
1231    // Values outside these ranges should be rejected.
1232
1233    #[test]
1234    fn test_msg_global_data_rejects_negative_msg_id() {
1235        // RFC 3412: msgID must be in range [0..2147483647]
1236        // Negative values should be rejected
1237        let mut buf = EncodeBuf::new();
1238        buf.push_sequence(|buf| {
1239            buf.push_integer(3); // USM security model
1240            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
1241            buf.push_integer(1472); // valid msg_max_size
1242            buf.push_integer(-1); // negative msg_id
1243        });
1244        let encoded = buf.finish();
1245
1246        let mut decoder = Decoder::new(encoded);
1247        let result = MsgGlobalData::decode(&mut decoder);
1248
1249        assert!(result.is_err());
1250        assert!(matches!(
1251            *result.unwrap_err(),
1252            Error::MalformedResponse { .. }
1253        ));
1254    }
1255
1256    #[test]
1257    fn test_msg_global_data_rejects_negative_msg_max_size() {
1258        // RFC 3412: msgMaxSize must be in range [484..2147483647]
1259        // Negative values (from signed integer interpretation) should be rejected
1260        let mut buf = EncodeBuf::new();
1261        buf.push_sequence(|buf| {
1262            buf.push_integer(3); // USM security model
1263            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
1264            buf.push_integer(-1); // negative msg_max_size (would be > 2^31-1 unsigned)
1265            buf.push_integer(100); // valid msg_id
1266        });
1267        let encoded = buf.finish();
1268
1269        let mut decoder = Decoder::new(encoded);
1270        let result = MsgGlobalData::decode(&mut decoder);
1271
1272        assert!(result.is_err());
1273        assert!(matches!(
1274            *result.unwrap_err(),
1275            Error::MalformedResponse { .. }
1276        ));
1277    }
1278
1279    #[test]
1280    fn test_msg_global_data_accepts_msg_id_at_zero() {
1281        // RFC 3412: msgID 0 is at the lower bound, should be accepted
1282        let mut buf = EncodeBuf::new();
1283        buf.push_sequence(|buf| {
1284            buf.push_integer(3); // USM
1285            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
1286            buf.push_integer(1472); // valid msg_max_size
1287            buf.push_integer(0); // msg_id at lower bound
1288        });
1289        let encoded = buf.finish();
1290
1291        let mut decoder = Decoder::new(encoded);
1292        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
1293
1294        assert_eq!(decoded.msg_id, 0);
1295    }
1296
1297    #[test]
1298    fn test_msg_global_data_accepts_msg_id_at_maximum() {
1299        // RFC 3412: msgID 2147483647 is at the upper bound, should be accepted
1300        let mut buf = EncodeBuf::new();
1301        buf.push_sequence(|buf| {
1302            buf.push_integer(3); // USM
1303            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
1304            buf.push_integer(1472); // valid msg_max_size
1305            buf.push_integer(i32::MAX); // msg_id at upper bound (2147483647)
1306        });
1307        let encoded = buf.finish();
1308
1309        let mut decoder = Decoder::new(encoded);
1310        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
1311
1312        assert_eq!(decoded.msg_id, i32::MAX);
1313    }
1314
1315    #[test]
1316    fn test_msg_global_data_accepts_msg_max_size_at_maximum() {
1317        // RFC 3412: msgMaxSize 2147483647 is at the upper bound, should be accepted
1318        let mut buf = EncodeBuf::new();
1319        buf.push_sequence(|buf| {
1320            buf.push_integer(3); // USM
1321            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
1322            buf.push_integer(i32::MAX); // msg_max_size at upper bound (2147483647)
1323            buf.push_integer(100); // valid msg_id
1324        });
1325        let encoded = buf.finish();
1326
1327        let mut decoder = Decoder::new(encoded);
1328        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
1329
1330        assert_eq!(decoded.msg_max_size, i32::MAX);
1331    }
1332}