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