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