async_snmp/v3/crypto/mod.rs
1//! Pluggable cryptographic provider for `SNMPv3` security operations.
2//!
3//! This module defines the [`CryptoProvider`] trait that captures the primitive
4//! cryptographic operations needed by the USM layer, and provides two built-in
5//! implementations:
6//!
7//! - [`RustCryptoProvider`] (feature `crypto-rustcrypto`, **default**) - backed by
8//! the `RustCrypto` crate ecosystem. Supports all auth and privacy protocols.
9//! - [`AwsLcFipsProvider`] (feature `crypto-fips`) - backed by aws-lc-rs for
10//! FIPS 140-3 compliance. Rejects MD5, DES, and 3DES as non-FIPS algorithms.
11//!
12//! The active provider is selected at compile time via mutually exclusive feature
13//! flags. Only one provider can be active per build. See the crate-level
14//! documentation for usage.
15
16use super::{AuthProtocol, PrivProtocol};
17
18/// Error type for cryptographic provider operations.
19///
20/// This covers failures that originate from the crypto backend itself:
21/// unsupported algorithms, invalid key material, and cipher-level errors.
22/// Protocol-level framing errors (e.g., wrong privParameters length) live
23/// in [`PrivacyError`](super::privacy::PrivacyError).
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum CryptoError {
26 /// The crypto backend does not support the requested algorithm.
27 ///
28 /// For example, the FIPS provider does not support MD5 or DES.
29 UnsupportedAlgorithm(&'static str),
30 /// The key length is invalid for the requested operation.
31 InvalidKeyLength,
32 /// The cipher operation failed internally.
33 CipherError,
34 /// The OS random source is unavailable.
35 RandomSource,
36 /// The supplied password is shorter than the RFC 3414 minimum (8 octets).
37 ///
38 /// RFC 3414 Section 11.2 requires passwords of at least 8 octets, and
39 /// net-snmp rejects shorter passwords with `USM_PASSWORDTOOSHORT`.
40 PasswordTooShort,
41}
42
43impl std::fmt::Display for CryptoError {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::UnsupportedAlgorithm(name) => {
47 write!(f, "unsupported algorithm: {name}")
48 }
49 Self::InvalidKeyLength => write!(f, "invalid key length"),
50 Self::CipherError => write!(f, "cipher operation failed"),
51 Self::RandomSource => write!(f, "OS random source unavailable"),
52 Self::PasswordTooShort => write!(
53 f,
54 "password is shorter than the RFC 3414 minimum of 8 octets"
55 ),
56 }
57 }
58}
59
60impl std::error::Error for CryptoError {}
61
62/// Result type for cryptographic provider operations.
63pub type CryptoResult<T> = Result<T, CryptoError>;
64
65#[cfg(all(feature = "crypto-rustcrypto", feature = "crypto-fips"))]
66compile_error!(
67 "Features \"crypto-rustcrypto\" and \"crypto-fips\" are mutually exclusive. If you used --all-features, specify features explicitly instead."
68);
69
70#[cfg(not(any(feature = "crypto-rustcrypto", feature = "crypto-fips")))]
71compile_error!(
72 "A crypto backend is required. Enable either \"crypto-rustcrypto\" (default) or \"crypto-fips\"."
73);
74
75#[cfg(feature = "crypto-rustcrypto")]
76mod rustcrypto;
77#[cfg(feature = "crypto-rustcrypto")]
78pub use rustcrypto::RustCryptoProvider;
79
80#[cfg(feature = "crypto-fips")]
81mod fips;
82#[cfg(feature = "crypto-fips")]
83pub use fips::AwsLcFipsProvider;
84
85/// Trait defining the cryptographic primitives needed by the `SNMPv3` USM layer.
86///
87/// This trait captures the six core operations that vary between crypto backends:
88/// hashing, password-to-key derivation, key localization, HMAC computation, and
89/// symmetric encryption/decryption.
90///
91/// # Implementors
92///
93/// Methods take `&self` to allow stateful providers (HSM handles, FFI contexts).
94/// The default [`RustCryptoProvider`] is a stateless unit struct.
95///
96/// # Thread Safety
97///
98/// Implementations must be `Send + Sync + 'static` to support use across
99/// async tasks and threads.
100///
101/// # Security Requirements
102///
103/// Implementations must uphold the following properties for correct and secure
104/// `SNMPv3` operation:
105///
106/// - **Constant-time HMAC comparison.** The caller (`verify_message`) performs
107/// the constant-time comparison, but `compute_hmac` must not short-circuit
108/// or leak timing information about intermediate state.
109/// - **IV/salt uniqueness.** Encryption callers supply the IV, but if your
110/// provider wraps a stateful cipher context that generates IVs internally,
111/// it must guarantee uniqueness across calls (RFC 3826 Section 3.1.4.1).
112/// - **Key material hygiene.** Intermediate key material (e.g., expanded
113/// password hashes) should be zeroized after use. Implementations backed
114/// by HSMs or FIPS modules should keep keys in hardware where possible.
115/// - **Algorithm support errors.** Return [`CryptoError::UnsupportedAlgorithm`]
116/// rather than panicking when asked to perform an operation the backend
117/// does not support.
118pub trait CryptoProvider: Send + Sync + 'static {
119 /// Derive a master key from a password using the RFC 3414 Section A.2.1 algorithm.
120 ///
121 /// Expands the password to 1MB by repetition and hashes it with the protocol's
122 /// hash function. Returns the raw digest bytes.
123 ///
124 /// Empty passwords should return an all-zero key of the protocol's digest length.
125 ///
126 /// Returns [`CryptoError::UnsupportedAlgorithm`] if the backend does not
127 /// support the requested authentication protocol.
128 fn password_to_key(&self, protocol: AuthProtocol, password: &[u8]) -> CryptoResult<Vec<u8>>;
129
130 /// Localize a master key to a specific engine ID (RFC 3414 Section A.2.2).
131 ///
132 /// Computes: `H(master_key || engine_id || master_key)`
133 ///
134 /// Returns [`CryptoError::UnsupportedAlgorithm`] if the backend does not
135 /// support the requested authentication protocol.
136 fn localize_key(
137 &self,
138 protocol: AuthProtocol,
139 master_key: &[u8],
140 engine_id: &[u8],
141 ) -> CryptoResult<Vec<u8>>;
142
143 /// Compute HMAC over one or more data slices, truncated to `truncate_len` bytes.
144 ///
145 /// The multi-slice interface avoids allocations when computing HMACs over
146 /// non-contiguous data (e.g., message verification with zeroed auth params).
147 ///
148 /// Returns [`CryptoError::UnsupportedAlgorithm`] if the backend does not
149 /// support the requested authentication protocol.
150 fn compute_hmac(
151 &self,
152 protocol: AuthProtocol,
153 key: &[u8],
154 slices: &[&[u8]],
155 truncate_len: usize,
156 ) -> CryptoResult<Vec<u8>>;
157
158 /// Encrypt data in place using the specified privacy protocol.
159 ///
160 /// The caller is responsible for key extraction and IV construction.
161 /// For block ciphers (DES, 3DES), the implementation pads unaligned
162 /// plaintext to the next block boundary per RFC 3414 ยง8.1.1.2,
163 /// extending the Vec as needed.
164 fn encrypt(
165 &self,
166 protocol: PrivProtocol,
167 key: &[u8],
168 iv: &[u8],
169 data: &mut Vec<u8>,
170 ) -> CryptoResult<()>;
171
172 /// Compute a bare hash digest using the protocol's hash function.
173 ///
174 /// Returns [`CryptoError::UnsupportedAlgorithm`] if the backend does not
175 /// support the requested authentication protocol.
176 fn hash(&self, protocol: AuthProtocol, data: &[u8]) -> CryptoResult<Vec<u8>>;
177
178 /// Decrypt data in place using the specified privacy protocol.
179 ///
180 /// The caller is responsible for key extraction and IV reconstruction.
181 fn decrypt(
182 &self,
183 protocol: PrivProtocol,
184 key: &[u8],
185 iv: &[u8],
186 data: &mut [u8],
187 ) -> CryptoResult<()>;
188}
189
190/// Returns the active crypto provider.
191///
192/// The provider is selected at compile time. The default uses [`RustCryptoProvider`].
193/// Alternative backends can be feature-gated here.
194#[cfg(feature = "crypto-rustcrypto")]
195pub(crate) fn provider() -> &'static RustCryptoProvider {
196 &RustCryptoProvider
197}
198
199#[cfg(feature = "crypto-fips")]
200pub(crate) fn provider() -> &'static AwsLcFipsProvider {
201 &AwsLcFipsProvider
202}