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