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