Skip to main content

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