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