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