async_snmp/v3/
mod.rs

1//! SNMPv3 security module.
2//!
3//! This module implements the User-based Security Model (USM) as defined
4//! in RFC 3414 and RFC 7860, including:
5//!
6//! - USM security parameters encoding/decoding
7//! - Key localization (password-to-key derivation)
8//! - Authentication (HMAC-MD5-96, HMAC-SHA-96, HMAC-SHA-224/256/384/512)
9//! - Privacy (DES-CBC, AES-128/192/256-CFB)
10//! - Engine discovery and time synchronization
11
12pub mod auth;
13mod engine;
14mod privacy;
15mod usm;
16
17pub use auth::{LocalizedKey, MasterKey, MasterKeys, extend_key};
18pub use engine::{
19    DEFAULT_MSG_MAX_SIZE, EngineCache, EngineState, TIME_WINDOW, parse_discovery_response,
20    parse_discovery_response_with_limits,
21};
22pub use engine::{
23    is_decryption_error_report, is_not_in_time_window_report, is_unknown_engine_id_report,
24    is_unknown_user_name_report, is_unsupported_sec_level_report, is_wrong_digest_report,
25};
26pub use privacy::{PrivKey, SaltCounter};
27pub use usm::UsmSecurityParams;
28
29/// Key extension strategy for privacy key derivation.
30///
31/// When using AES-192 or AES-256 with authentication protocols that produce
32/// shorter digests (e.g., SHA-1), a key extension algorithm is needed to
33/// generate sufficient key material.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub enum KeyExtension {
37    /// No key extension. Requires compatible auth/priv protocol combinations.
38    /// This is the default and will panic if insufficient key material.
39    #[default]
40    None,
41    /// Use the Blumenthal key extension algorithm (draft-blumenthal-aes-usm-04).
42    /// Extends keys by iteratively hashing: Kul' = Kul || H(Kul) || H(Kul||H(Kul)) || ...
43    Blumenthal,
44}
45
46/// Error returned when parsing a protocol name fails.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ParseProtocolError {
49    input: String,
50    kind: ProtocolKind,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54enum ProtocolKind {
55    Auth,
56    Priv,
57}
58
59impl std::fmt::Display for ParseProtocolError {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        match self.kind {
62            ProtocolKind::Auth => write!(
63                f,
64                "unknown authentication protocol '{}'; expected one of: MD5, SHA, SHA-224, SHA-256, SHA-384, SHA-512",
65                self.input
66            ),
67            ProtocolKind::Priv => write!(
68                f,
69                "unknown privacy protocol '{}'; expected one of: DES, AES, AES-128, AES-192, AES-256",
70                self.input
71            ),
72        }
73    }
74}
75
76impl std::error::Error for ParseProtocolError {}
77
78/// Authentication protocol identifiers.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
81pub enum AuthProtocol {
82    /// HMAC-MD5-96 (RFC 3414)
83    Md5,
84    /// HMAC-SHA-96 (RFC 3414)
85    Sha1,
86    /// HMAC-SHA-224 (RFC 7860)
87    Sha224,
88    /// HMAC-SHA-256 (RFC 7860)
89    Sha256,
90    /// HMAC-SHA-384 (RFC 7860)
91    Sha384,
92    /// HMAC-SHA-512 (RFC 7860)
93    Sha512,
94}
95
96impl std::fmt::Display for AuthProtocol {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        match self {
99            Self::Md5 => write!(f, "MD5"),
100            Self::Sha1 => write!(f, "SHA"),
101            Self::Sha224 => write!(f, "SHA-224"),
102            Self::Sha256 => write!(f, "SHA-256"),
103            Self::Sha384 => write!(f, "SHA-384"),
104            Self::Sha512 => write!(f, "SHA-512"),
105        }
106    }
107}
108
109impl std::str::FromStr for AuthProtocol {
110    type Err = ParseProtocolError;
111
112    fn from_str(s: &str) -> Result<Self, Self::Err> {
113        match s.to_ascii_uppercase().as_str() {
114            "MD5" => Ok(Self::Md5),
115            "SHA" | "SHA1" | "SHA-1" => Ok(Self::Sha1),
116            "SHA224" | "SHA-224" => Ok(Self::Sha224),
117            "SHA256" | "SHA-256" => Ok(Self::Sha256),
118            "SHA384" | "SHA-384" => Ok(Self::Sha384),
119            "SHA512" | "SHA-512" => Ok(Self::Sha512),
120            _ => Err(ParseProtocolError {
121                input: s.to_string(),
122                kind: ProtocolKind::Auth,
123            }),
124        }
125    }
126}
127
128impl AuthProtocol {
129    /// Get the digest output length in bytes.
130    ///
131    /// This is also the key length produced by the key localization algorithm,
132    /// which is used for privacy key derivation.
133    pub fn digest_len(self) -> usize {
134        match self {
135            Self::Md5 => 16,
136            Self::Sha1 => 20,
137            Self::Sha224 => 28,
138            Self::Sha256 => 32,
139            Self::Sha384 => 48,
140            Self::Sha512 => 64,
141        }
142    }
143
144    /// Get the truncated MAC length for authentication parameters.
145    pub fn mac_len(self) -> usize {
146        match self {
147            Self::Md5 | Self::Sha1 => 12, // HMAC-96
148            Self::Sha224 => 16,           // RFC 7860
149            Self::Sha256 => 24,           // RFC 7860
150            Self::Sha384 => 32,           // RFC 7860
151            Self::Sha512 => 48,           // RFC 7860
152        }
153    }
154
155    /// Check if this authentication protocol produces sufficient key material
156    /// for the given privacy protocol.
157    ///
158    /// Privacy keys are derived from the localized authentication key, so the
159    /// auth protocol must produce at least as many bytes as the privacy
160    /// protocol requires:
161    ///
162    /// | Privacy Protocol | Required Key Length | Compatible Auth Protocols |
163    /// |------------------|--------------------|-----------------------|
164    /// | DES              | 16 bytes           | All (MD5+)           |
165    /// | AES-128          | 16 bytes           | All (MD5+)           |
166    /// | AES-192          | 24 bytes           | SHA-224, SHA-256, SHA-384, SHA-512 |
167    /// | AES-256          | 32 bytes           | SHA-256, SHA-384, SHA-512 |
168    ///
169    /// # Interoperability with net-snmp
170    ///
171    /// Some implementations (notably net-snmp) support AES-192/256 with shorter
172    /// authentication protocols using key extension. To interoperate with these
173    /// systems, use [`PrivKey::from_password_extended`] with
174    /// [`KeyExtension::Blumenthal`].
175    ///
176    /// # Example
177    ///
178    /// ```rust
179    /// use async_snmp::{AuthProtocol, PrivProtocol};
180    ///
181    /// // SHA-256 works with all privacy protocols
182    /// assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Aes256));
183    ///
184    /// // SHA-1 doesn't produce enough key material for AES-256
185    /// assert!(!AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Aes256));
186    /// ```
187    pub fn is_compatible_with(self, priv_protocol: PrivProtocol) -> bool {
188        self.digest_len() >= priv_protocol.key_len()
189    }
190}
191
192/// Privacy protocol identifiers.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
195pub enum PrivProtocol {
196    /// DES-CBC (RFC 3414)
197    Des,
198    /// AES-128-CFB (RFC 3826)
199    Aes128,
200    /// AES-192-CFB (RFC 3826)
201    Aes192,
202    /// AES-256-CFB (RFC 3826)
203    Aes256,
204}
205
206impl std::fmt::Display for PrivProtocol {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        match self {
209            Self::Des => write!(f, "DES"),
210            Self::Aes128 => write!(f, "AES"),
211            Self::Aes192 => write!(f, "AES-192"),
212            Self::Aes256 => write!(f, "AES-256"),
213        }
214    }
215}
216
217impl std::str::FromStr for PrivProtocol {
218    type Err = ParseProtocolError;
219
220    fn from_str(s: &str) -> Result<Self, Self::Err> {
221        match s.to_ascii_uppercase().as_str() {
222            "DES" => Ok(Self::Des),
223            "AES" | "AES128" | "AES-128" => Ok(Self::Aes128),
224            "AES192" | "AES-192" => Ok(Self::Aes192),
225            "AES256" | "AES-256" => Ok(Self::Aes256),
226            _ => Err(ParseProtocolError {
227                input: s.to_string(),
228                kind: ProtocolKind::Priv,
229            }),
230        }
231    }
232}
233
234impl PrivProtocol {
235    /// Get the key length in bytes.
236    pub fn key_len(self) -> usize {
237        match self {
238            Self::Des => 16, // 8 key + 8 pre-IV
239            Self::Aes128 => 16,
240            Self::Aes192 => 24,
241            Self::Aes256 => 32,
242        }
243    }
244
245    /// Get the IV/salt length in bytes.
246    pub fn salt_len(self) -> usize {
247        8 // All protocols use 8-byte salt
248    }
249
250    /// Get the minimum authentication protocol required for this privacy protocol.
251    ///
252    /// Returns the weakest auth protocol that produces sufficient key material.
253    ///
254    /// | Privacy Protocol | Minimum Auth Protocol |
255    /// |------------------|-----------------------|
256    /// | DES, AES-128     | MD5 (16 bytes)       |
257    /// | AES-192          | SHA-224 (28 bytes)   |
258    /// | AES-256          | SHA-256 (32 bytes)   |
259    pub fn min_auth_protocol(self) -> AuthProtocol {
260        match self {
261            Self::Des | Self::Aes128 => AuthProtocol::Md5,
262            Self::Aes192 => AuthProtocol::Sha224,
263            Self::Aes256 => AuthProtocol::Sha256,
264        }
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_auth_protocol_compatibility_all_with_des() {
274        // All auth protocols work with DES (requires 16 bytes)
275        assert!(AuthProtocol::Md5.is_compatible_with(PrivProtocol::Des));
276        assert!(AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Des));
277        assert!(AuthProtocol::Sha224.is_compatible_with(PrivProtocol::Des));
278        assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Des));
279        assert!(AuthProtocol::Sha384.is_compatible_with(PrivProtocol::Des));
280        assert!(AuthProtocol::Sha512.is_compatible_with(PrivProtocol::Des));
281    }
282
283    #[test]
284    fn test_auth_protocol_compatibility_all_with_aes128() {
285        // All auth protocols work with AES-128 (requires 16 bytes)
286        assert!(AuthProtocol::Md5.is_compatible_with(PrivProtocol::Aes128));
287        assert!(AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Aes128));
288        assert!(AuthProtocol::Sha224.is_compatible_with(PrivProtocol::Aes128));
289        assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Aes128));
290        assert!(AuthProtocol::Sha384.is_compatible_with(PrivProtocol::Aes128));
291        assert!(AuthProtocol::Sha512.is_compatible_with(PrivProtocol::Aes128));
292    }
293
294    #[test]
295    fn test_auth_protocol_compatibility_with_aes192() {
296        // AES-192 requires 24 bytes - only SHA-224+ work
297        assert!(!AuthProtocol::Md5.is_compatible_with(PrivProtocol::Aes192)); // 16 < 24
298        assert!(!AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Aes192)); // 20 < 24
299        assert!(AuthProtocol::Sha224.is_compatible_with(PrivProtocol::Aes192)); // 28 >= 24
300        assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Aes192)); // 32 >= 24
301        assert!(AuthProtocol::Sha384.is_compatible_with(PrivProtocol::Aes192)); // 48 >= 24
302        assert!(AuthProtocol::Sha512.is_compatible_with(PrivProtocol::Aes192)); // 64 >= 24
303    }
304
305    #[test]
306    fn test_auth_protocol_compatibility_with_aes256() {
307        // AES-256 requires 32 bytes - only SHA-256+ work
308        assert!(!AuthProtocol::Md5.is_compatible_with(PrivProtocol::Aes256)); // 16 < 32
309        assert!(!AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Aes256)); // 20 < 32
310        assert!(!AuthProtocol::Sha224.is_compatible_with(PrivProtocol::Aes256)); // 28 < 32
311        assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Aes256)); // 32 >= 32
312        assert!(AuthProtocol::Sha384.is_compatible_with(PrivProtocol::Aes256)); // 48 >= 32
313        assert!(AuthProtocol::Sha512.is_compatible_with(PrivProtocol::Aes256)); // 64 >= 32
314    }
315
316    #[test]
317    fn test_priv_protocol_min_auth_protocol() {
318        assert_eq!(PrivProtocol::Des.min_auth_protocol(), AuthProtocol::Md5);
319        assert_eq!(PrivProtocol::Aes128.min_auth_protocol(), AuthProtocol::Md5);
320        assert_eq!(
321            PrivProtocol::Aes192.min_auth_protocol(),
322            AuthProtocol::Sha224
323        );
324        assert_eq!(
325            PrivProtocol::Aes256.min_auth_protocol(),
326            AuthProtocol::Sha256
327        );
328    }
329
330    #[test]
331    fn test_auth_protocol_display() {
332        assert_eq!(format!("{}", AuthProtocol::Md5), "MD5");
333        assert_eq!(format!("{}", AuthProtocol::Sha1), "SHA");
334        assert_eq!(format!("{}", AuthProtocol::Sha224), "SHA-224");
335        assert_eq!(format!("{}", AuthProtocol::Sha256), "SHA-256");
336        assert_eq!(format!("{}", AuthProtocol::Sha384), "SHA-384");
337        assert_eq!(format!("{}", AuthProtocol::Sha512), "SHA-512");
338    }
339
340    #[test]
341    fn test_auth_protocol_from_str() {
342        assert_eq!("MD5".parse::<AuthProtocol>().unwrap(), AuthProtocol::Md5);
343        assert_eq!("md5".parse::<AuthProtocol>().unwrap(), AuthProtocol::Md5);
344        assert_eq!("SHA".parse::<AuthProtocol>().unwrap(), AuthProtocol::Sha1);
345        assert_eq!("sha1".parse::<AuthProtocol>().unwrap(), AuthProtocol::Sha1);
346        assert_eq!("SHA-1".parse::<AuthProtocol>().unwrap(), AuthProtocol::Sha1);
347        assert_eq!(
348            "sha-224".parse::<AuthProtocol>().unwrap(),
349            AuthProtocol::Sha224
350        );
351        assert_eq!(
352            "SHA256".parse::<AuthProtocol>().unwrap(),
353            AuthProtocol::Sha256
354        );
355        assert_eq!(
356            "SHA-256".parse::<AuthProtocol>().unwrap(),
357            AuthProtocol::Sha256
358        );
359        assert_eq!(
360            "sha384".parse::<AuthProtocol>().unwrap(),
361            AuthProtocol::Sha384
362        );
363        assert_eq!(
364            "SHA-512".parse::<AuthProtocol>().unwrap(),
365            AuthProtocol::Sha512
366        );
367
368        assert!("invalid".parse::<AuthProtocol>().is_err());
369    }
370
371    #[test]
372    fn test_priv_protocol_display() {
373        assert_eq!(format!("{}", PrivProtocol::Des), "DES");
374        assert_eq!(format!("{}", PrivProtocol::Aes128), "AES");
375        assert_eq!(format!("{}", PrivProtocol::Aes192), "AES-192");
376        assert_eq!(format!("{}", PrivProtocol::Aes256), "AES-256");
377    }
378
379    #[test]
380    fn test_priv_protocol_from_str() {
381        assert_eq!("DES".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des);
382        assert_eq!("des".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des);
383        assert_eq!("AES".parse::<PrivProtocol>().unwrap(), PrivProtocol::Aes128);
384        assert_eq!("aes".parse::<PrivProtocol>().unwrap(), PrivProtocol::Aes128);
385        assert_eq!(
386            "AES128".parse::<PrivProtocol>().unwrap(),
387            PrivProtocol::Aes128
388        );
389        assert_eq!(
390            "AES-128".parse::<PrivProtocol>().unwrap(),
391            PrivProtocol::Aes128
392        );
393        assert_eq!(
394            "aes192".parse::<PrivProtocol>().unwrap(),
395            PrivProtocol::Aes192
396        );
397        assert_eq!(
398            "AES-192".parse::<PrivProtocol>().unwrap(),
399            PrivProtocol::Aes192
400        );
401        assert_eq!(
402            "aes256".parse::<PrivProtocol>().unwrap(),
403            PrivProtocol::Aes256
404        );
405        assert_eq!(
406            "AES-256".parse::<PrivProtocol>().unwrap(),
407            PrivProtocol::Aes256
408        );
409
410        assert!("invalid".parse::<PrivProtocol>().is_err());
411    }
412
413    #[test]
414    fn test_parse_protocol_error_display() {
415        let err = "bogus".parse::<AuthProtocol>().unwrap_err();
416        assert!(err.to_string().contains("bogus"));
417        assert!(err.to_string().contains("authentication protocol"));
418
419        let err = "bogus".parse::<PrivProtocol>().unwrap_err();
420        assert!(err.to_string().contains("bogus"));
421        assert!(err.to_string().contains("privacy protocol"));
422    }
423}