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/// SNMPv3 security model identifiers.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31#[repr(i32)]
32pub enum SecurityModel {
33    /// User-based Security Model (RFC 3414)
34    Usm = 3,
35}
36
37impl SecurityModel {
38    /// Create from raw value.
39    pub fn from_i32(value: i32) -> Option<Self> {
40        match value {
41            3 => Some(Self::Usm),
42            _ => None,
43        }
44    }
45
46    /// Get the raw value.
47    pub fn as_i32(self) -> i32 {
48        self as i32
49    }
50}
51
52/// SNMPv3 security level.
53///
54/// The variants are ordered from least secure to most secure,
55/// supporting VACM-style level comparisons (e.g., `actual >= required`).
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
57pub enum SecurityLevel {
58    /// No authentication, no privacy
59    NoAuthNoPriv,
60    /// Authentication only
61    AuthNoPriv,
62    /// Authentication and privacy (encryption)
63    AuthPriv,
64}
65
66impl SecurityLevel {
67    /// Decode from msgFlags byte.
68    pub fn from_flags(flags: u8) -> Option<Self> {
69        let auth = flags & 0x01 != 0;
70        let priv_ = flags & 0x02 != 0;
71
72        match (auth, priv_) {
73            (false, false) => Some(Self::NoAuthNoPriv),
74            (true, false) => Some(Self::AuthNoPriv),
75            (true, true) => Some(Self::AuthPriv),
76            (false, true) => None, // Invalid: priv without auth
77        }
78    }
79
80    /// Encode to msgFlags byte (without reportable flag).
81    pub fn to_flags(self) -> u8 {
82        match self {
83            Self::NoAuthNoPriv => 0x00,
84            Self::AuthNoPriv => 0x01,
85            Self::AuthPriv => 0x03,
86        }
87    }
88
89    /// Check if authentication is required.
90    pub fn requires_auth(self) -> bool {
91        matches!(self, Self::AuthNoPriv | Self::AuthPriv)
92    }
93
94    /// Check if privacy (encryption) is required.
95    pub fn requires_priv(self) -> bool {
96        matches!(self, Self::AuthPriv)
97    }
98}
99
100impl TryFrom<u8> for SecurityLevel {
101    type Error = u8;
102
103    fn try_from(flags: u8) -> std::result::Result<Self, u8> {
104        Self::from_flags(flags).ok_or(flags)
105    }
106}
107
108impl From<SecurityLevel> for u8 {
109    fn from(level: SecurityLevel) -> u8 {
110        level.to_flags()
111    }
112}
113
114/// Message flags (RFC 3412 Section 6.4).
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub struct MsgFlags {
117    /// Security level
118    pub security_level: SecurityLevel,
119    /// Whether a report PDU may be sent on error
120    pub reportable: bool,
121}
122
123impl MsgFlags {
124    /// Create new message flags.
125    pub fn new(security_level: SecurityLevel, reportable: bool) -> Self {
126        Self {
127            security_level,
128            reportable,
129        }
130    }
131
132    /// Decode from byte.
133    pub fn from_byte(byte: u8) -> Result<Self> {
134        let security_level = SecurityLevel::from_flags(byte).ok_or_else(|| {
135            tracing::debug!(target: "async_snmp::v3", { byte, kind = %DecodeErrorKind::InvalidMsgFlags }, "decode error");
136            Error::MalformedResponse {
137                target: UNKNOWN_TARGET,
138            }
139            .boxed()
140        })?;
141        let reportable = byte & 0x04 != 0;
142        Ok(Self {
143            security_level,
144            reportable,
145        })
146    }
147
148    /// Encode to byte.
149    pub fn to_byte(self) -> u8 {
150        let mut flags = self.security_level.to_flags();
151        if self.reportable {
152            flags |= 0x04;
153        }
154        flags
155    }
156}
157
158/// Message global data header (msgGlobalData).
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct MsgGlobalData {
161    /// Message identifier for request/response correlation
162    pub msg_id: i32,
163    /// Maximum message size the sender can accept
164    pub msg_max_size: i32,
165    /// Message flags (security level + reportable)
166    pub msg_flags: MsgFlags,
167    /// Security model (always USM=3 for our implementation)
168    pub msg_security_model: SecurityModel,
169}
170
171impl MsgGlobalData {
172    /// Create new global data.
173    pub fn new(msg_id: i32, msg_max_size: i32, msg_flags: MsgFlags) -> Self {
174        Self {
175            msg_id,
176            msg_max_size,
177            msg_flags,
178            msg_security_model: SecurityModel::Usm,
179        }
180    }
181
182    /// Encode to buffer.
183    pub fn encode(&self, buf: &mut EncodeBuf) {
184        buf.push_sequence(|buf| {
185            buf.push_integer(self.msg_security_model.as_i32());
186            // msgFlags is a 1-byte OCTET STRING
187            buf.push_octet_string(&[self.msg_flags.to_byte()]);
188            buf.push_integer(self.msg_max_size);
189            buf.push_integer(self.msg_id);
190        });
191    }
192
193    /// Decode from decoder.
194    ///
195    /// Validates that:
196    /// - `msgID` is in range 0..2147483647 (RFC 3412 HeaderData)
197    /// - `msgMaxSize` is in range 484..2147483647 (RFC 3412 HeaderData)
198    /// - `msgSecurityModel` is a known value (currently only USM=3)
199    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
200        const MSG_MAX_SIZE_MINIMUM: i32 = 484;
201
202        let mut seq = decoder.read_sequence()?;
203
204        let msg_id = seq.read_integer()?;
205        let msg_max_size = seq.read_integer()?;
206
207        // RFC 3412 HeaderData: msgID INTEGER (0..2147483647)
208        if msg_id < 0 {
209            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), value = msg_id, kind = %DecodeErrorKind::InvalidMsgId { value: msg_id } }, "decode error");
210            return Err(Error::MalformedResponse {
211                target: UNKNOWN_TARGET,
212            }
213            .boxed());
214        }
215
216        // RFC 3412 HeaderData: msgMaxSize INTEGER (484..2147483647)
217        // Negative values indicate the sender encoded a value > 2^31-1
218        if msg_max_size < 0 {
219            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), value = msg_max_size, kind = %DecodeErrorKind::MsgMaxSizeTooLarge { value: msg_max_size } }, "decode error");
220            return Err(Error::MalformedResponse {
221                target: UNKNOWN_TARGET,
222            }
223            .boxed());
224        }
225
226        if msg_max_size < MSG_MAX_SIZE_MINIMUM {
227            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");
228            return Err(Error::MalformedResponse {
229                target: UNKNOWN_TARGET,
230            }
231            .boxed());
232        }
233
234        let flags_bytes = seq.read_octet_string()?;
235        if flags_bytes.len() != 1 {
236            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), expected = 1, actual = flags_bytes.len() }, "invalid msgFlags length");
237            return Err(Error::MalformedResponse {
238                target: UNKNOWN_TARGET,
239            }
240            .boxed());
241        }
242        let msg_flags = MsgFlags::from_byte(flags_bytes[0])?;
243
244        let msg_security_model_raw = seq.read_integer()?;
245        // Reject unknown security models per RFC 3412 Section 7.2
246        let msg_security_model =
247            SecurityModel::from_i32(msg_security_model_raw).ok_or_else(|| {
248                tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), model = msg_security_model_raw, kind = %DecodeErrorKind::UnknownSecurityModel(msg_security_model_raw) }, "decode error");
249                Error::MalformedResponse {
250                    target: UNKNOWN_TARGET,
251                }
252                .boxed()
253            })?;
254
255        Ok(Self {
256            msg_id,
257            msg_max_size,
258            msg_flags,
259            msg_security_model,
260        })
261    }
262}
263
264/// Scoped PDU (contextEngineID + contextName + PDU).
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct ScopedPdu {
267    /// Context engine ID (typically same as authoritative engine ID)
268    pub context_engine_id: Bytes,
269    /// Context name (typically empty string)
270    pub context_name: Bytes,
271    /// The actual PDU
272    pub pdu: Pdu,
273}
274
275impl ScopedPdu {
276    /// Create a new scoped PDU.
277    pub fn new(
278        context_engine_id: impl Into<Bytes>,
279        context_name: impl Into<Bytes>,
280        pdu: Pdu,
281    ) -> Self {
282        Self {
283            context_engine_id: context_engine_id.into(),
284            context_name: context_name.into(),
285            pdu,
286        }
287    }
288
289    /// Create with empty context (most common case).
290    pub fn with_empty_context(pdu: Pdu) -> Self {
291        Self {
292            context_engine_id: Bytes::new(),
293            context_name: Bytes::new(),
294            pdu,
295        }
296    }
297
298    /// Encode to buffer.
299    pub fn encode(&self, buf: &mut EncodeBuf) {
300        buf.push_sequence(|buf| {
301            self.pdu.encode(buf);
302            buf.push_octet_string(&self.context_name);
303            buf.push_octet_string(&self.context_engine_id);
304        });
305    }
306
307    /// Encode to bytes.
308    pub fn encode_to_bytes(&self) -> Bytes {
309        let mut buf = EncodeBuf::new();
310        self.encode(&mut buf);
311        buf.finish()
312    }
313
314    /// Decode from decoder.
315    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
316        let mut seq = decoder.read_sequence()?;
317
318        let context_engine_id = seq.read_octet_string()?;
319        let context_name = seq.read_octet_string()?;
320        let pdu = Pdu::decode(&mut seq)?;
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 ciphertext.
445    /// The caller must decrypt using USM before accessing the scoped PDU.
446    pub fn decode(data: Bytes) -> Result<Self> {
447        let mut decoder = Decoder::new(data);
448        let mut seq = decoder.read_sequence()?;
449
450        // Version
451        let version = seq.read_integer()?;
452        if version != 3 {
453            tracing::debug!(target: "async_snmp::v3", { offset = seq.offset(), version, kind = %DecodeErrorKind::UnknownVersion(version) }, "decode error");
454            return Err(Error::MalformedResponse {
455                target: UNKNOWN_TARGET,
456            }
457            .boxed());
458        }
459
460        Self::decode_from_sequence(&mut seq)
461    }
462
463    /// Decode from a sequence decoder where version has already been read.
464    pub(crate) fn decode_from_sequence(seq: &mut Decoder) -> Result<Self> {
465        // msgGlobalData
466        let global_data = MsgGlobalData::decode(seq)?;
467
468        // msgSecurityParameters (OCTET STRING containing USM params)
469        let security_params = seq.read_octet_string()?;
470
471        // msgData - either plaintext SEQUENCE or encrypted OCTET STRING
472        let data = if global_data.msg_flags.security_level.requires_priv() {
473            // Encrypted: expect OCTET STRING
474            let encrypted = seq.read_octet_string()?;
475            V3MessageData::Encrypted(encrypted)
476        } else {
477            // Plaintext: expect SEQUENCE (ScopedPDU)
478            let scoped_pdu = ScopedPdu::decode(seq)?;
479            V3MessageData::Plaintext(scoped_pdu)
480        };
481
482        Ok(Self {
483            global_data,
484            security_params,
485            data,
486        })
487    }
488
489    /// Create a discovery request message.
490    ///
491    /// This is sent to discover the engine ID and time of a remote SNMP engine.
492    /// Uses empty security parameters and no authentication.
493    pub fn discovery_request(msg_id: i32) -> Self {
494        let global_data = MsgGlobalData::new(
495            msg_id,
496            65507, // max UDP size
497            MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
498        );
499
500        // Empty USM security parameters for discovery
501        let security_params = crate::v3::UsmSecurityParams::empty().encode();
502
503        // Empty scoped PDU with Report request
504        let pdu = Pdu::get_request(0, &[]);
505        let scoped_pdu = ScopedPdu::with_empty_context(pdu);
506
507        Self::new(global_data, security_params, scoped_pdu)
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use super::*;
514    use crate::oid;
515
516    #[test]
517    fn test_security_level_flags() {
518        assert_eq!(SecurityLevel::NoAuthNoPriv.to_flags(), 0x00);
519        assert_eq!(SecurityLevel::AuthNoPriv.to_flags(), 0x01);
520        assert_eq!(SecurityLevel::AuthPriv.to_flags(), 0x03);
521
522        assert_eq!(
523            SecurityLevel::from_flags(0x00),
524            Some(SecurityLevel::NoAuthNoPriv)
525        );
526        assert_eq!(
527            SecurityLevel::from_flags(0x01),
528            Some(SecurityLevel::AuthNoPriv)
529        );
530        assert_eq!(
531            SecurityLevel::from_flags(0x03),
532            Some(SecurityLevel::AuthPriv)
533        );
534        assert_eq!(SecurityLevel::from_flags(0x02), None); // Invalid
535    }
536
537    #[test]
538    fn security_level_try_from_u8() {
539        assert_eq!(
540            SecurityLevel::try_from(0x00),
541            Ok(SecurityLevel::NoAuthNoPriv)
542        );
543        assert_eq!(SecurityLevel::try_from(0x01), Ok(SecurityLevel::AuthNoPriv));
544        assert_eq!(SecurityLevel::try_from(0x03), Ok(SecurityLevel::AuthPriv));
545        assert_eq!(SecurityLevel::try_from(0x02), Err(0x02));
546    }
547
548    #[test]
549    fn security_level_into_u8() {
550        assert_eq!(u8::from(SecurityLevel::NoAuthNoPriv), 0x00);
551        assert_eq!(u8::from(SecurityLevel::AuthNoPriv), 0x01);
552        assert_eq!(u8::from(SecurityLevel::AuthPriv), 0x03);
553    }
554
555    #[test]
556    fn test_msg_flags_roundtrip() {
557        let flags = MsgFlags::new(SecurityLevel::AuthPriv, true);
558        let byte = flags.to_byte();
559        assert_eq!(byte, 0x07); // auth=1, priv=1, reportable=1
560
561        let decoded = MsgFlags::from_byte(byte).unwrap();
562        assert_eq!(decoded.security_level, SecurityLevel::AuthPriv);
563        assert!(decoded.reportable);
564    }
565
566    #[test]
567    fn test_msg_global_data_roundtrip() {
568        let global =
569            MsgGlobalData::new(12345, 1472, MsgFlags::new(SecurityLevel::AuthNoPriv, true));
570
571        let mut buf = EncodeBuf::new();
572        global.encode(&mut buf);
573        let encoded = buf.finish();
574
575        let mut decoder = Decoder::new(encoded);
576        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
577
578        assert_eq!(decoded.msg_id, 12345);
579        assert_eq!(decoded.msg_max_size, 1472);
580        assert_eq!(decoded.msg_flags.security_level, SecurityLevel::AuthNoPriv);
581        assert!(decoded.msg_flags.reportable);
582        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
583    }
584
585    #[test]
586    fn test_scoped_pdu_roundtrip() {
587        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
588        let scoped = ScopedPdu::new(b"engine".as_slice(), b"ctx".as_slice(), pdu);
589
590        let mut buf = EncodeBuf::new();
591        scoped.encode(&mut buf);
592        let encoded = buf.finish();
593
594        let mut decoder = Decoder::new(encoded);
595        let decoded = ScopedPdu::decode(&mut decoder).unwrap();
596
597        assert_eq!(decoded.context_engine_id.as_ref(), b"engine");
598        assert_eq!(decoded.context_name.as_ref(), b"ctx");
599        assert_eq!(decoded.pdu.request_id, 42);
600    }
601
602    #[test]
603    fn test_v3_message_plaintext_roundtrip() {
604        let global =
605            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
606        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
607        let scoped = ScopedPdu::with_empty_context(pdu);
608        let msg = V3Message::new(global, Bytes::from_static(b"usm-params"), scoped);
609
610        let encoded = msg.encode();
611        let decoded = V3Message::decode(encoded).unwrap();
612
613        assert_eq!(decoded.global_data.msg_id, 100);
614        assert_eq!(decoded.security_level(), SecurityLevel::NoAuthNoPriv);
615        assert_eq!(decoded.security_params.as_ref(), b"usm-params");
616
617        let scoped_pdu = decoded.scoped_pdu().unwrap();
618        assert_eq!(scoped_pdu.pdu.request_id, 42);
619    }
620
621    #[test]
622    fn test_v3_message_encrypted_roundtrip() {
623        let global = MsgGlobalData::new(200, 1472, MsgFlags::new(SecurityLevel::AuthPriv, false));
624        let msg = V3Message::new_encrypted(
625            global,
626            Bytes::from_static(b"usm-params"),
627            Bytes::from_static(b"encrypted-data"),
628        );
629
630        let encoded = msg.encode();
631        let decoded = V3Message::decode(encoded).unwrap();
632
633        assert_eq!(decoded.global_data.msg_id, 200);
634        assert_eq!(decoded.security_level(), SecurityLevel::AuthPriv);
635
636        match &decoded.data {
637            V3MessageData::Encrypted(data) => {
638                assert_eq!(data.as_ref(), b"encrypted-data");
639            }
640            V3MessageData::Plaintext(_) => panic!("expected encrypted data"),
641        }
642    }
643
644    #[test]
645    fn test_msg_global_data_rejects_msg_max_size_below_minimum() {
646        // Encode with invalid msgMaxSize (below 484)
647        let global = MsgGlobalData {
648            msg_id: 100,
649            msg_max_size: 400, // Below RFC 3412 minimum of 484
650            msg_flags: MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
651            msg_security_model: SecurityModel::Usm,
652        };
653
654        let mut buf = EncodeBuf::new();
655        global.encode(&mut buf);
656        let encoded = buf.finish();
657
658        let mut decoder = Decoder::new(encoded);
659        let result = MsgGlobalData::decode(&mut decoder);
660
661        assert!(result.is_err());
662        assert!(matches!(
663            *result.unwrap_err(),
664            Error::MalformedResponse { .. }
665        ));
666    }
667
668    #[test]
669    fn test_msg_global_data_accepts_msg_max_size_at_minimum() {
670        // 484 is exactly the RFC 3412 minimum
671        let global = MsgGlobalData::new(100, 484, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
672
673        let mut buf = EncodeBuf::new();
674        global.encode(&mut buf);
675        let encoded = buf.finish();
676
677        let mut decoder = Decoder::new(encoded);
678        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
679
680        assert_eq!(decoded.msg_max_size, 484);
681    }
682
683    #[test]
684    fn test_msg_global_data_rejects_unknown_security_model() {
685        // Manually build encoded data with unknown security model
686        // SEQUENCE { msg_id, msg_max_size, msgFlags, msgSecurityModel=99 }
687        let mut buf = EncodeBuf::new();
688        buf.push_sequence(|buf| {
689            buf.push_integer(99); // unknown security model
690            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
691            buf.push_integer(1472); // msg_max_size
692            buf.push_integer(100); // msg_id
693        });
694        let encoded = buf.finish();
695
696        let mut decoder = Decoder::new(encoded);
697        let result = MsgGlobalData::decode(&mut decoder);
698
699        assert!(result.is_err());
700        assert!(matches!(
701            *result.unwrap_err(),
702            Error::MalformedResponse { .. }
703        ));
704    }
705
706    #[test]
707    fn test_msg_global_data_accepts_usm_security_model() {
708        // USM (3) should be accepted
709        let global =
710            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
711
712        let mut buf = EncodeBuf::new();
713        global.encode(&mut buf);
714        let encoded = buf.finish();
715
716        let mut decoder = Decoder::new(encoded);
717        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
718
719        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
720    }
721
722    // RFC 3412 bounds tests for msgID and msgMaxSize
723    //
724    // RFC 3412 HeaderData definition specifies:
725    //   msgID INTEGER (0..2147483647)
726    //   msgMaxSize INTEGER (484..2147483647)
727    //
728    // Values outside these ranges should be rejected.
729
730    #[test]
731    fn test_msg_global_data_rejects_negative_msg_id() {
732        // RFC 3412: msgID must be in range [0..2147483647]
733        // Negative values should be rejected
734        let mut buf = EncodeBuf::new();
735        buf.push_sequence(|buf| {
736            buf.push_integer(3); // USM security model
737            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
738            buf.push_integer(1472); // valid msg_max_size
739            buf.push_integer(-1); // negative msg_id
740        });
741        let encoded = buf.finish();
742
743        let mut decoder = Decoder::new(encoded);
744        let result = MsgGlobalData::decode(&mut decoder);
745
746        assert!(result.is_err());
747        assert!(matches!(
748            *result.unwrap_err(),
749            Error::MalformedResponse { .. }
750        ));
751    }
752
753    #[test]
754    fn test_msg_global_data_rejects_negative_msg_max_size() {
755        // RFC 3412: msgMaxSize must be in range [484..2147483647]
756        // Negative values (from signed integer interpretation) should be rejected
757        let mut buf = EncodeBuf::new();
758        buf.push_sequence(|buf| {
759            buf.push_integer(3); // USM security model
760            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
761            buf.push_integer(-1); // negative msg_max_size (would be > 2^31-1 unsigned)
762            buf.push_integer(100); // valid msg_id
763        });
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_id_at_zero() {
778        // RFC 3412: msgID 0 is at the lower bound, should be accepted
779        let mut buf = EncodeBuf::new();
780        buf.push_sequence(|buf| {
781            buf.push_integer(3); // USM
782            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
783            buf.push_integer(1472); // valid msg_max_size
784            buf.push_integer(0); // msg_id at lower bound
785        });
786        let encoded = buf.finish();
787
788        let mut decoder = Decoder::new(encoded);
789        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
790
791        assert_eq!(decoded.msg_id, 0);
792    }
793
794    #[test]
795    fn test_msg_global_data_accepts_msg_id_at_maximum() {
796        // RFC 3412: msgID 2147483647 is at the upper bound, should be accepted
797        let mut buf = EncodeBuf::new();
798        buf.push_sequence(|buf| {
799            buf.push_integer(3); // USM
800            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
801            buf.push_integer(1472); // valid msg_max_size
802            buf.push_integer(i32::MAX); // msg_id at upper bound (2147483647)
803        });
804        let encoded = buf.finish();
805
806        let mut decoder = Decoder::new(encoded);
807        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
808
809        assert_eq!(decoded.msg_id, i32::MAX);
810    }
811
812    #[test]
813    fn test_msg_global_data_accepts_msg_max_size_at_maximum() {
814        // RFC 3412: msgMaxSize 2147483647 is at the upper bound, should be accepted
815        let mut buf = EncodeBuf::new();
816        buf.push_sequence(|buf| {
817            buf.push_integer(3); // USM
818            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
819            buf.push_integer(i32::MAX); // msg_max_size at upper bound (2147483647)
820            buf.push_integer(100); // valid msg_id
821        });
822        let encoded = buf.finish();
823
824        let mut decoder = Decoder::new(encoded);
825        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
826
827        assert_eq!(decoded.msg_max_size, i32::MAX);
828    }
829}