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};
18pub use engine::{
19    DEFAULT_MSG_MAX_SIZE, EngineCache, EngineState, MAX_ENGINE_TIME, TIME_WINDOW,
20    parse_discovery_response, 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, PrivacyError, PrivacyResult, SaltCounter};
27pub use usm::UsmSecurityParams;
28
29/// Key extension strategy for privacy key derivation.
30///
31/// This is an internal type used to select the appropriate key extension
32/// algorithm when deriving privacy keys. The correct algorithm is auto-detected
33/// based on the auth/priv protocol combination.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub(crate) enum KeyExtension {
36    /// No key extension. Use standard RFC 3414 key derivation.
37    #[default]
38    None,
39    /// Blumenthal key extension (draft-blumenthal-aes-usm-04) for AES-192/256.
40    Blumenthal,
41    /// Reeder key extension (draft-reeder-snmpv3-usm-3desede-00) for 3DES.
42    Reeder,
43}
44
45/// Error returned when parsing a protocol name fails.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ParseProtocolError {
48    input: String,
49    kind: ProtocolKind,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53enum ProtocolKind {
54    Auth,
55    Priv,
56}
57
58impl std::fmt::Display for ParseProtocolError {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        match self.kind {
61            ProtocolKind::Auth => write!(
62                f,
63                "unknown authentication protocol '{}'; expected one of: MD5, SHA, SHA-224, SHA-256, SHA-384, SHA-512",
64                self.input
65            ),
66            ProtocolKind::Priv => write!(
67                f,
68                "unknown privacy protocol '{}'; expected one of: DES, AES, AES-128, AES-192, AES-256",
69                self.input
70            ),
71        }
72    }
73}
74
75impl std::error::Error for ParseProtocolError {}
76
77/// Authentication protocol identifiers.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum AuthProtocol {
80    /// HMAC-MD5-96 (RFC 3414)
81    Md5,
82    /// HMAC-SHA-96 (RFC 3414)
83    Sha1,
84    /// HMAC-SHA-224 (RFC 7860)
85    Sha224,
86    /// HMAC-SHA-256 (RFC 7860)
87    Sha256,
88    /// HMAC-SHA-384 (RFC 7860)
89    Sha384,
90    /// HMAC-SHA-512 (RFC 7860)
91    Sha512,
92}
93
94impl std::fmt::Display for AuthProtocol {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::Md5 => write!(f, "MD5"),
98            Self::Sha1 => write!(f, "SHA"),
99            Self::Sha224 => write!(f, "SHA-224"),
100            Self::Sha256 => write!(f, "SHA-256"),
101            Self::Sha384 => write!(f, "SHA-384"),
102            Self::Sha512 => write!(f, "SHA-512"),
103        }
104    }
105}
106
107impl std::str::FromStr for AuthProtocol {
108    type Err = ParseProtocolError;
109
110    fn from_str(s: &str) -> Result<Self, Self::Err> {
111        match s.to_ascii_uppercase().as_str() {
112            "MD5" => Ok(Self::Md5),
113            "SHA" | "SHA1" | "SHA-1" => Ok(Self::Sha1),
114            "SHA224" | "SHA-224" => Ok(Self::Sha224),
115            "SHA256" | "SHA-256" => Ok(Self::Sha256),
116            "SHA384" | "SHA-384" => Ok(Self::Sha384),
117            "SHA512" | "SHA-512" => Ok(Self::Sha512),
118            _ => Err(ParseProtocolError {
119                input: s.to_string(),
120                kind: ProtocolKind::Auth,
121            }),
122        }
123    }
124}
125
126impl AuthProtocol {
127    /// Get the digest output length in bytes.
128    ///
129    /// This is also the key length produced by the key localization algorithm,
130    /// which is used for privacy key derivation.
131    pub fn digest_len(self) -> usize {
132        match self {
133            Self::Md5 => 16,
134            Self::Sha1 => 20,
135            Self::Sha224 => 28,
136            Self::Sha256 => 32,
137            Self::Sha384 => 48,
138            Self::Sha512 => 64,
139        }
140    }
141
142    /// Get the truncated MAC length for authentication parameters.
143    pub fn mac_len(self) -> usize {
144        match self {
145            Self::Md5 | Self::Sha1 => 12, // HMAC-96
146            Self::Sha224 => 16,           // RFC 7860
147            Self::Sha256 => 24,           // RFC 7860
148            Self::Sha384 => 32,           // RFC 7860
149            Self::Sha512 => 48,           // RFC 7860
150        }
151    }
152}
153
154/// Privacy protocol identifiers.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum PrivProtocol {
157    /// DES-CBC (RFC 3414).
158    ///
159    /// Insecure: 56-bit keys are brute-forceable. Also slower than AES, which
160    /// benefits from hardware acceleration.
161    Des,
162    /// 3DES-EDE in "Outside" CBC mode (draft-reeder-snmpv3-usm-3desede-00).
163    ///
164    /// Uses three 56-bit keys for 168-bit effective security (112-bit against
165    /// meet-in-the-middle). Slower than AES and lacks hardware acceleration.
166    Des3,
167    /// AES-128-CFB (RFC 3826)
168    Aes128,
169    /// AES-192-CFB (RFC 3826)
170    Aes192,
171    /// AES-256-CFB (RFC 3826)
172    Aes256,
173}
174
175impl std::fmt::Display for PrivProtocol {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        match self {
178            Self::Des => write!(f, "DES"),
179            Self::Des3 => write!(f, "3DES"),
180            Self::Aes128 => write!(f, "AES"),
181            Self::Aes192 => write!(f, "AES-192"),
182            Self::Aes256 => write!(f, "AES-256"),
183        }
184    }
185}
186
187impl std::str::FromStr for PrivProtocol {
188    type Err = ParseProtocolError;
189
190    fn from_str(s: &str) -> Result<Self, Self::Err> {
191        match s.to_ascii_uppercase().as_str() {
192            "DES" => Ok(Self::Des),
193            "3DES" | "3DES-EDE" | "DES3" | "TDES" => Ok(Self::Des3),
194            "AES" | "AES128" | "AES-128" => Ok(Self::Aes128),
195            "AES192" | "AES-192" => Ok(Self::Aes192),
196            "AES256" | "AES-256" => Ok(Self::Aes256),
197            _ => Err(ParseProtocolError {
198                input: s.to_string(),
199                kind: ProtocolKind::Priv,
200            }),
201        }
202    }
203}
204
205impl PrivProtocol {
206    /// Get the key length in bytes.
207    pub fn key_len(self) -> usize {
208        match self {
209            Self::Des => 16,  // 8 key + 8 pre-IV
210            Self::Des3 => 32, // 24 key + 8 pre-IV
211            Self::Aes128 => 16,
212            Self::Aes192 => 24,
213            Self::Aes256 => 32,
214        }
215    }
216
217    /// Get the IV/salt length in bytes.
218    pub fn salt_len(self) -> usize {
219        8 // All protocols use 8-byte salt
220    }
221
222    /// Returns the key extension algorithm to use for this privacy protocol
223    /// given the authentication protocol.
224    ///
225    /// Key extension is needed when the auth protocol's digest is shorter than
226    /// the privacy protocol's key requirement. The algorithm is determined by
227    /// the privacy protocol:
228    /// - AES-192/256: Blumenthal (draft-blumenthal-aes-usm-04)
229    /// - 3DES: Reeder (draft-reeder-snmpv3-usm-3desede-00)
230    pub(crate) fn key_extension_for(self, auth_protocol: AuthProtocol) -> KeyExtension {
231        let auth_len = auth_protocol.digest_len();
232        let priv_len = self.key_len();
233
234        if auth_len >= priv_len {
235            return KeyExtension::None;
236        }
237
238        match self {
239            Self::Des3 => KeyExtension::Reeder,
240            Self::Aes192 | Self::Aes256 => KeyExtension::Blumenthal,
241            Self::Des | Self::Aes128 => KeyExtension::None, // Never need extension
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_auth_protocol_display() {
252        assert_eq!(format!("{}", AuthProtocol::Md5), "MD5");
253        assert_eq!(format!("{}", AuthProtocol::Sha1), "SHA");
254        assert_eq!(format!("{}", AuthProtocol::Sha224), "SHA-224");
255        assert_eq!(format!("{}", AuthProtocol::Sha256), "SHA-256");
256        assert_eq!(format!("{}", AuthProtocol::Sha384), "SHA-384");
257        assert_eq!(format!("{}", AuthProtocol::Sha512), "SHA-512");
258    }
259
260    #[test]
261    fn test_auth_protocol_from_str() {
262        assert_eq!("MD5".parse::<AuthProtocol>().unwrap(), AuthProtocol::Md5);
263        assert_eq!("md5".parse::<AuthProtocol>().unwrap(), AuthProtocol::Md5);
264        assert_eq!("SHA".parse::<AuthProtocol>().unwrap(), AuthProtocol::Sha1);
265        assert_eq!("sha1".parse::<AuthProtocol>().unwrap(), AuthProtocol::Sha1);
266        assert_eq!("SHA-1".parse::<AuthProtocol>().unwrap(), AuthProtocol::Sha1);
267        assert_eq!(
268            "sha-224".parse::<AuthProtocol>().unwrap(),
269            AuthProtocol::Sha224
270        );
271        assert_eq!(
272            "SHA256".parse::<AuthProtocol>().unwrap(),
273            AuthProtocol::Sha256
274        );
275        assert_eq!(
276            "SHA-256".parse::<AuthProtocol>().unwrap(),
277            AuthProtocol::Sha256
278        );
279        assert_eq!(
280            "sha384".parse::<AuthProtocol>().unwrap(),
281            AuthProtocol::Sha384
282        );
283        assert_eq!(
284            "SHA-512".parse::<AuthProtocol>().unwrap(),
285            AuthProtocol::Sha512
286        );
287
288        assert!("invalid".parse::<AuthProtocol>().is_err());
289    }
290
291    #[test]
292    fn test_priv_protocol_display() {
293        assert_eq!(format!("{}", PrivProtocol::Des), "DES");
294        assert_eq!(format!("{}", PrivProtocol::Des3), "3DES");
295        assert_eq!(format!("{}", PrivProtocol::Aes128), "AES");
296        assert_eq!(format!("{}", PrivProtocol::Aes192), "AES-192");
297        assert_eq!(format!("{}", PrivProtocol::Aes256), "AES-256");
298    }
299
300    #[test]
301    fn test_priv_protocol_from_str() {
302        assert_eq!("DES".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des);
303        assert_eq!("des".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des);
304        assert_eq!("3DES".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des3);
305        assert_eq!("3des".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des3);
306        assert_eq!(
307            "3DES-EDE".parse::<PrivProtocol>().unwrap(),
308            PrivProtocol::Des3
309        );
310        assert_eq!("DES3".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des3);
311        assert_eq!("TDES".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des3);
312        assert_eq!("AES".parse::<PrivProtocol>().unwrap(), PrivProtocol::Aes128);
313        assert_eq!("aes".parse::<PrivProtocol>().unwrap(), PrivProtocol::Aes128);
314        assert_eq!(
315            "AES128".parse::<PrivProtocol>().unwrap(),
316            PrivProtocol::Aes128
317        );
318        assert_eq!(
319            "AES-128".parse::<PrivProtocol>().unwrap(),
320            PrivProtocol::Aes128
321        );
322        assert_eq!(
323            "aes192".parse::<PrivProtocol>().unwrap(),
324            PrivProtocol::Aes192
325        );
326        assert_eq!(
327            "AES-192".parse::<PrivProtocol>().unwrap(),
328            PrivProtocol::Aes192
329        );
330        assert_eq!(
331            "aes256".parse::<PrivProtocol>().unwrap(),
332            PrivProtocol::Aes256
333        );
334        assert_eq!(
335            "AES-256".parse::<PrivProtocol>().unwrap(),
336            PrivProtocol::Aes256
337        );
338
339        assert!("invalid".parse::<PrivProtocol>().is_err());
340    }
341
342    #[test]
343    fn test_parse_protocol_error_display() {
344        let err = "bogus".parse::<AuthProtocol>().unwrap_err();
345        assert!(err.to_string().contains("bogus"));
346        assert!(err.to_string().contains("authentication protocol"));
347
348        let err = "bogus".parse::<PrivProtocol>().unwrap_err();
349        assert!(err.to_string().contains("bogus"));
350        assert!(err.to_string().contains("privacy protocol"));
351    }
352}