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 crate::ber::{Decoder, EncodeBuf};
23use crate::error::{DecodeErrorKind, Error, Result};
24use crate::pdu::Pdu;
25use bytes::Bytes;
26
27/// SNMPv3 security model identifiers.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[repr(i32)]
30pub enum SecurityModel {
31    /// User-based Security Model (RFC 3414)
32    Usm = 3,
33}
34
35impl SecurityModel {
36    /// Create from raw value.
37    pub fn from_i32(value: i32) -> Option<Self> {
38        match value {
39            3 => Some(Self::Usm),
40            _ => None,
41        }
42    }
43
44    /// Get the raw value.
45    pub fn as_i32(self) -> i32 {
46        self as i32
47    }
48}
49
50/// SNMPv3 security level.
51///
52/// The variants are ordered from least secure to most secure,
53/// supporting VACM-style level comparisons (e.g., `actual >= required`).
54#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
55pub enum SecurityLevel {
56    /// No authentication, no privacy
57    NoAuthNoPriv,
58    /// Authentication only
59    AuthNoPriv,
60    /// Authentication and privacy (encryption)
61    AuthPriv,
62}
63
64impl SecurityLevel {
65    /// Decode from msgFlags byte.
66    pub fn from_flags(flags: u8) -> Option<Self> {
67        let auth = flags & 0x01 != 0;
68        let priv_ = flags & 0x02 != 0;
69
70        match (auth, priv_) {
71            (false, false) => Some(Self::NoAuthNoPriv),
72            (true, false) => Some(Self::AuthNoPriv),
73            (true, true) => Some(Self::AuthPriv),
74            (false, true) => None, // Invalid: priv without auth
75        }
76    }
77
78    /// Encode to msgFlags byte (without reportable flag).
79    pub fn to_flags(self) -> u8 {
80        match self {
81            Self::NoAuthNoPriv => 0x00,
82            Self::AuthNoPriv => 0x01,
83            Self::AuthPriv => 0x03,
84        }
85    }
86
87    /// Check if authentication is required.
88    pub fn requires_auth(self) -> bool {
89        matches!(self, Self::AuthNoPriv | Self::AuthPriv)
90    }
91
92    /// Check if privacy (encryption) is required.
93    pub fn requires_priv(self) -> bool {
94        matches!(self, Self::AuthPriv)
95    }
96}
97
98/// Message flags (RFC 3412 Section 6.4).
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct MsgFlags {
101    /// Security level
102    pub security_level: SecurityLevel,
103    /// Whether a report PDU may be sent on error
104    pub reportable: bool,
105}
106
107impl MsgFlags {
108    /// Create new message flags.
109    pub fn new(security_level: SecurityLevel, reportable: bool) -> Self {
110        Self {
111            security_level,
112            reportable,
113        }
114    }
115
116    /// Decode from byte.
117    pub fn from_byte(byte: u8) -> Result<Self> {
118        let security_level = SecurityLevel::from_flags(byte)
119            .ok_or_else(|| Error::decode(0, DecodeErrorKind::InvalidMsgFlags))?;
120        let reportable = byte & 0x04 != 0;
121        Ok(Self {
122            security_level,
123            reportable,
124        })
125    }
126
127    /// Encode to byte.
128    pub fn to_byte(self) -> u8 {
129        let mut flags = self.security_level.to_flags();
130        if self.reportable {
131            flags |= 0x04;
132        }
133        flags
134    }
135}
136
137/// Message global data header (msgGlobalData).
138#[derive(Debug, Clone)]
139pub struct MsgGlobalData {
140    /// Message identifier for request/response correlation
141    pub msg_id: i32,
142    /// Maximum message size the sender can accept
143    pub msg_max_size: i32,
144    /// Message flags (security level + reportable)
145    pub msg_flags: MsgFlags,
146    /// Security model (always USM=3 for our implementation)
147    pub msg_security_model: SecurityModel,
148}
149
150impl MsgGlobalData {
151    /// Create new global data.
152    pub fn new(msg_id: i32, msg_max_size: i32, msg_flags: MsgFlags) -> Self {
153        Self {
154            msg_id,
155            msg_max_size,
156            msg_flags,
157            msg_security_model: SecurityModel::Usm,
158        }
159    }
160
161    /// Encode to buffer.
162    pub fn encode(&self, buf: &mut EncodeBuf) {
163        buf.push_sequence(|buf| {
164            buf.push_integer(self.msg_security_model.as_i32());
165            // msgFlags is a 1-byte OCTET STRING
166            buf.push_octet_string(&[self.msg_flags.to_byte()]);
167            buf.push_integer(self.msg_max_size);
168            buf.push_integer(self.msg_id);
169        });
170    }
171
172    /// Decode from decoder.
173    ///
174    /// Validates that:
175    /// - `msgMaxSize` is at least 484 octets (RFC 3412 Section 6.1)
176    /// - `msgSecurityModel` is a known value (currently only USM=3)
177    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
178        /// RFC 3412 minimum msgMaxSize (484 octets).
179        const MSG_MAX_SIZE_MINIMUM: i32 = 484;
180
181        let mut seq = decoder.read_sequence()?;
182
183        let msg_id = seq.read_integer()?;
184        let msg_max_size = seq.read_integer()?;
185
186        // RFC 3412 Section 6.1: msgMaxSize MUST be at least 484
187        if msg_max_size < MSG_MAX_SIZE_MINIMUM {
188            return Err(Error::decode(
189                seq.offset(),
190                DecodeErrorKind::MsgMaxSizeTooSmall {
191                    value: msg_max_size,
192                    minimum: MSG_MAX_SIZE_MINIMUM,
193                },
194            ));
195        }
196
197        let flags_bytes = seq.read_octet_string()?;
198        if flags_bytes.len() != 1 {
199            return Err(Error::decode(
200                seq.offset(),
201                DecodeErrorKind::UnexpectedTag {
202                    expected: 1,
203                    actual: flags_bytes.len() as u8,
204                },
205            ));
206        }
207        let msg_flags = MsgFlags::from_byte(flags_bytes[0])?;
208
209        let msg_security_model_raw = seq.read_integer()?;
210        // Reject unknown security models per RFC 3412 Section 7.2
211        let msg_security_model =
212            SecurityModel::from_i32(msg_security_model_raw).ok_or_else(|| {
213                Error::decode(
214                    seq.offset(),
215                    DecodeErrorKind::UnknownSecurityModel(msg_security_model_raw),
216                )
217            })?;
218
219        Ok(Self {
220            msg_id,
221            msg_max_size,
222            msg_flags,
223            msg_security_model,
224        })
225    }
226}
227
228/// Scoped PDU (contextEngineID + contextName + PDU).
229#[derive(Debug, Clone)]
230pub struct ScopedPdu {
231    /// Context engine ID (typically same as authoritative engine ID)
232    pub context_engine_id: Bytes,
233    /// Context name (typically empty string)
234    pub context_name: Bytes,
235    /// The actual PDU
236    pub pdu: Pdu,
237}
238
239impl ScopedPdu {
240    /// Create a new scoped PDU.
241    pub fn new(
242        context_engine_id: impl Into<Bytes>,
243        context_name: impl Into<Bytes>,
244        pdu: Pdu,
245    ) -> Self {
246        Self {
247            context_engine_id: context_engine_id.into(),
248            context_name: context_name.into(),
249            pdu,
250        }
251    }
252
253    /// Create with empty context (most common case).
254    pub fn with_empty_context(pdu: Pdu) -> Self {
255        Self {
256            context_engine_id: Bytes::new(),
257            context_name: Bytes::new(),
258            pdu,
259        }
260    }
261
262    /// Encode to buffer.
263    pub fn encode(&self, buf: &mut EncodeBuf) {
264        buf.push_sequence(|buf| {
265            self.pdu.encode(buf);
266            buf.push_octet_string(&self.context_name);
267            buf.push_octet_string(&self.context_engine_id);
268        });
269    }
270
271    /// Encode to bytes.
272    pub fn encode_to_bytes(&self) -> Bytes {
273        let mut buf = EncodeBuf::new();
274        self.encode(&mut buf);
275        buf.finish()
276    }
277
278    /// Decode from decoder.
279    pub fn decode(decoder: &mut Decoder) -> Result<Self> {
280        let mut seq = decoder.read_sequence()?;
281
282        let context_engine_id = seq.read_octet_string()?;
283        let context_name = seq.read_octet_string()?;
284        let pdu = Pdu::decode(&mut seq)?;
285
286        Ok(Self {
287            context_engine_id,
288            context_name,
289            pdu,
290        })
291    }
292}
293
294/// SNMPv3 message.
295#[derive(Debug, Clone)]
296pub struct V3Message {
297    /// Global data (header)
298    pub global_data: MsgGlobalData,
299    /// Security parameters (opaque, USM-encoded)
300    pub security_params: Bytes,
301    /// Message data - either plaintext ScopedPdu or encrypted bytes
302    pub data: V3MessageData,
303}
304
305/// Message data payload.
306#[derive(Debug, Clone)]
307pub enum V3MessageData {
308    /// Plaintext scoped PDU (noAuthNoPriv or authNoPriv)
309    Plaintext(ScopedPdu),
310    /// Encrypted scoped PDU (authPriv) - raw ciphertext
311    Encrypted(Bytes),
312}
313
314impl V3Message {
315    /// Create a new V3 message with plaintext data.
316    pub fn new(global_data: MsgGlobalData, security_params: Bytes, scoped_pdu: ScopedPdu) -> Self {
317        Self {
318            global_data,
319            security_params,
320            data: V3MessageData::Plaintext(scoped_pdu),
321        }
322    }
323
324    /// Create a new V3 message with encrypted data.
325    pub fn new_encrypted(
326        global_data: MsgGlobalData,
327        security_params: Bytes,
328        encrypted: Bytes,
329    ) -> Self {
330        Self {
331            global_data,
332            security_params,
333            data: V3MessageData::Encrypted(encrypted),
334        }
335    }
336
337    /// Get the scoped PDU if available (plaintext only).
338    pub fn scoped_pdu(&self) -> Option<&ScopedPdu> {
339        match &self.data {
340            V3MessageData::Plaintext(pdu) => Some(pdu),
341            V3MessageData::Encrypted(_) => None,
342        }
343    }
344
345    /// Consume and return the scoped PDU if available.
346    pub fn into_scoped_pdu(self) -> Option<ScopedPdu> {
347        match self.data {
348            V3MessageData::Plaintext(pdu) => Some(pdu),
349            V3MessageData::Encrypted(_) => None,
350        }
351    }
352
353    /// Get the PDU if available (convenience method).
354    pub fn pdu(&self) -> Option<&Pdu> {
355        self.scoped_pdu().map(|s| &s.pdu)
356    }
357
358    /// Consume and return the PDU.
359    pub fn into_pdu(self) -> Option<Pdu> {
360        self.into_scoped_pdu().map(|s| s.pdu)
361    }
362
363    /// Get the message ID.
364    pub fn msg_id(&self) -> i32 {
365        self.global_data.msg_id
366    }
367
368    /// Get the security level.
369    pub fn security_level(&self) -> SecurityLevel {
370        self.global_data.msg_flags.security_level
371    }
372
373    /// Encode to BER.
374    ///
375    /// Note: For authenticated messages, the caller must:
376    /// 1. Encode with placeholder auth params (12 zero bytes for HMAC-96)
377    /// 2. Compute HMAC over the entire encoded message
378    /// 3. Replace the placeholder with the actual HMAC
379    pub fn encode(&self) -> Bytes {
380        let mut buf = EncodeBuf::new();
381
382        buf.push_sequence(|buf| {
383            // msgData
384            match &self.data {
385                V3MessageData::Plaintext(scoped_pdu) => {
386                    scoped_pdu.encode(buf);
387                }
388                V3MessageData::Encrypted(ciphertext) => {
389                    buf.push_octet_string(ciphertext);
390                }
391            }
392
393            // msgSecurityParameters (as OCTET STRING)
394            buf.push_octet_string(&self.security_params);
395
396            // msgGlobalData
397            self.global_data.encode(buf);
398
399            // version
400            buf.push_integer(3);
401        });
402
403        buf.finish()
404    }
405
406    /// Decode from BER.
407    ///
408    /// For encrypted messages, returns `V3MessageData::Encrypted` with the raw ciphertext.
409    /// The caller must decrypt using USM before accessing the scoped PDU.
410    pub fn decode(data: Bytes) -> Result<Self> {
411        let mut decoder = Decoder::new(data);
412        let mut seq = decoder.read_sequence()?;
413
414        // Version
415        let version = seq.read_integer()?;
416        if version != 3 {
417            return Err(Error::decode(
418                seq.offset(),
419                DecodeErrorKind::UnknownVersion(version),
420            ));
421        }
422
423        // msgGlobalData
424        let global_data = MsgGlobalData::decode(&mut seq)?;
425
426        // msgSecurityParameters (OCTET STRING containing USM params)
427        let security_params = seq.read_octet_string()?;
428
429        // msgData - either plaintext SEQUENCE or encrypted OCTET STRING
430        let data = if global_data.msg_flags.security_level.requires_priv() {
431            // Encrypted: expect OCTET STRING
432            let encrypted = seq.read_octet_string()?;
433            V3MessageData::Encrypted(encrypted)
434        } else {
435            // Plaintext: expect SEQUENCE (ScopedPDU)
436            let scoped_pdu = ScopedPdu::decode(&mut seq)?;
437            V3MessageData::Plaintext(scoped_pdu)
438        };
439
440        Ok(Self {
441            global_data,
442            security_params,
443            data,
444        })
445    }
446
447    /// Create a discovery request message.
448    ///
449    /// This is sent to discover the engine ID and time of a remote SNMP engine.
450    /// Uses empty security parameters and no authentication.
451    pub fn discovery_request(msg_id: i32) -> Self {
452        let global_data = MsgGlobalData::new(
453            msg_id,
454            65507, // max UDP size
455            MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
456        );
457
458        // Empty USM security parameters for discovery
459        let security_params = crate::v3::UsmSecurityParams::empty().encode();
460
461        // Empty scoped PDU with Report request
462        let pdu = Pdu::get_request(0, &[]);
463        let scoped_pdu = ScopedPdu::with_empty_context(pdu);
464
465        Self::new(global_data, security_params, scoped_pdu)
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472    use crate::oid;
473
474    #[test]
475    fn test_security_level_flags() {
476        assert_eq!(SecurityLevel::NoAuthNoPriv.to_flags(), 0x00);
477        assert_eq!(SecurityLevel::AuthNoPriv.to_flags(), 0x01);
478        assert_eq!(SecurityLevel::AuthPriv.to_flags(), 0x03);
479
480        assert_eq!(
481            SecurityLevel::from_flags(0x00),
482            Some(SecurityLevel::NoAuthNoPriv)
483        );
484        assert_eq!(
485            SecurityLevel::from_flags(0x01),
486            Some(SecurityLevel::AuthNoPriv)
487        );
488        assert_eq!(
489            SecurityLevel::from_flags(0x03),
490            Some(SecurityLevel::AuthPriv)
491        );
492        assert_eq!(SecurityLevel::from_flags(0x02), None); // Invalid
493    }
494
495    #[test]
496    fn test_msg_flags_roundtrip() {
497        let flags = MsgFlags::new(SecurityLevel::AuthPriv, true);
498        let byte = flags.to_byte();
499        assert_eq!(byte, 0x07); // auth=1, priv=1, reportable=1
500
501        let decoded = MsgFlags::from_byte(byte).unwrap();
502        assert_eq!(decoded.security_level, SecurityLevel::AuthPriv);
503        assert!(decoded.reportable);
504    }
505
506    #[test]
507    fn test_msg_global_data_roundtrip() {
508        let global =
509            MsgGlobalData::new(12345, 1472, MsgFlags::new(SecurityLevel::AuthNoPriv, true));
510
511        let mut buf = EncodeBuf::new();
512        global.encode(&mut buf);
513        let encoded = buf.finish();
514
515        let mut decoder = Decoder::new(encoded);
516        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
517
518        assert_eq!(decoded.msg_id, 12345);
519        assert_eq!(decoded.msg_max_size, 1472);
520        assert_eq!(decoded.msg_flags.security_level, SecurityLevel::AuthNoPriv);
521        assert!(decoded.msg_flags.reportable);
522        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
523    }
524
525    #[test]
526    fn test_scoped_pdu_roundtrip() {
527        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
528        let scoped = ScopedPdu::new(b"engine".as_slice(), b"ctx".as_slice(), pdu);
529
530        let mut buf = EncodeBuf::new();
531        scoped.encode(&mut buf);
532        let encoded = buf.finish();
533
534        let mut decoder = Decoder::new(encoded);
535        let decoded = ScopedPdu::decode(&mut decoder).unwrap();
536
537        assert_eq!(decoded.context_engine_id.as_ref(), b"engine");
538        assert_eq!(decoded.context_name.as_ref(), b"ctx");
539        assert_eq!(decoded.pdu.request_id, 42);
540    }
541
542    #[test]
543    fn test_v3_message_plaintext_roundtrip() {
544        let global =
545            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
546        let pdu = Pdu::get_request(42, &[oid!(1, 3, 6, 1, 2, 1, 1, 1, 0)]);
547        let scoped = ScopedPdu::with_empty_context(pdu);
548        let msg = V3Message::new(global, Bytes::from_static(b"usm-params"), scoped);
549
550        let encoded = msg.encode();
551        let decoded = V3Message::decode(encoded).unwrap();
552
553        assert_eq!(decoded.global_data.msg_id, 100);
554        assert_eq!(decoded.security_level(), SecurityLevel::NoAuthNoPriv);
555        assert_eq!(decoded.security_params.as_ref(), b"usm-params");
556
557        let scoped_pdu = decoded.scoped_pdu().unwrap();
558        assert_eq!(scoped_pdu.pdu.request_id, 42);
559    }
560
561    #[test]
562    fn test_v3_message_encrypted_roundtrip() {
563        let global = MsgGlobalData::new(200, 1472, MsgFlags::new(SecurityLevel::AuthPriv, false));
564        let msg = V3Message::new_encrypted(
565            global,
566            Bytes::from_static(b"usm-params"),
567            Bytes::from_static(b"encrypted-data"),
568        );
569
570        let encoded = msg.encode();
571        let decoded = V3Message::decode(encoded).unwrap();
572
573        assert_eq!(decoded.global_data.msg_id, 200);
574        assert_eq!(decoded.security_level(), SecurityLevel::AuthPriv);
575
576        match &decoded.data {
577            V3MessageData::Encrypted(data) => {
578                assert_eq!(data.as_ref(), b"encrypted-data");
579            }
580            V3MessageData::Plaintext(_) => panic!("expected encrypted data"),
581        }
582    }
583
584    #[test]
585    fn test_msg_global_data_rejects_msg_max_size_below_minimum() {
586        // Encode with invalid msgMaxSize (below 484)
587        let global = MsgGlobalData {
588            msg_id: 100,
589            msg_max_size: 400, // Below RFC 3412 minimum of 484
590            msg_flags: MsgFlags::new(SecurityLevel::NoAuthNoPriv, true),
591            msg_security_model: SecurityModel::Usm,
592        };
593
594        let mut buf = EncodeBuf::new();
595        global.encode(&mut buf);
596        let encoded = buf.finish();
597
598        let mut decoder = Decoder::new(encoded);
599        let result = MsgGlobalData::decode(&mut decoder);
600
601        assert!(result.is_err());
602        match result.unwrap_err() {
603            Error::Decode {
604                kind: DecodeErrorKind::MsgMaxSizeTooSmall { value, minimum },
605                ..
606            } => {
607                assert_eq!(value, 400);
608                assert_eq!(minimum, 484);
609            }
610            e => panic!("expected MsgMaxSizeTooSmall error, got {:?}", e),
611        }
612    }
613
614    #[test]
615    fn test_msg_global_data_accepts_msg_max_size_at_minimum() {
616        // 484 is exactly the RFC 3412 minimum
617        let global = MsgGlobalData::new(100, 484, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
618
619        let mut buf = EncodeBuf::new();
620        global.encode(&mut buf);
621        let encoded = buf.finish();
622
623        let mut decoder = Decoder::new(encoded);
624        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
625
626        assert_eq!(decoded.msg_max_size, 484);
627    }
628
629    #[test]
630    fn test_msg_global_data_rejects_unknown_security_model() {
631        // Manually build encoded data with unknown security model
632        // SEQUENCE { msg_id, msg_max_size, msgFlags, msgSecurityModel=99 }
633        let mut buf = EncodeBuf::new();
634        buf.push_sequence(|buf| {
635            buf.push_integer(99); // unknown security model
636            buf.push_octet_string(&[0x04]); // reportable, noAuthNoPriv
637            buf.push_integer(1472); // msg_max_size
638            buf.push_integer(100); // msg_id
639        });
640        let encoded = buf.finish();
641
642        let mut decoder = Decoder::new(encoded);
643        let result = MsgGlobalData::decode(&mut decoder);
644
645        assert!(result.is_err());
646        match result.unwrap_err() {
647            Error::Decode {
648                kind: DecodeErrorKind::UnknownSecurityModel(model),
649                ..
650            } => {
651                assert_eq!(model, 99);
652            }
653            e => panic!("expected UnknownSecurityModel error, got {:?}", e),
654        }
655    }
656
657    #[test]
658    fn test_msg_global_data_accepts_usm_security_model() {
659        // USM (3) should be accepted
660        let global =
661            MsgGlobalData::new(100, 1472, MsgFlags::new(SecurityLevel::NoAuthNoPriv, true));
662
663        let mut buf = EncodeBuf::new();
664        global.encode(&mut buf);
665        let encoded = buf.finish();
666
667        let mut decoder = Decoder::new(encoded);
668        let decoded = MsgGlobalData::decode(&mut decoder).unwrap();
669
670        assert_eq!(decoded.msg_security_model, SecurityModel::Usm);
671    }
672}