smime_tree/key.rs
1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::SmimeError;
6
7/// Identifies the recipient of an encrypted message.
8/// Used by [`DecryptionKey::matches_recipient`] to find the right key.
9///
10/// The CMS standard defines two ways to identify a recipient certificate.
11/// Your `matches_recipient` implementation should handle both variants.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[non_exhaustive]
14pub enum RecipientIdentifier {
15 /// Identified by certificate issuer name and serial number (PKCS #7 compatibility).
16 IssuerAndSerialNumber {
17 /// DER-encoded `Name` (X.501 RDN sequence).
18 /// Obtain from `cert.tbs_certificate().issuer().to_der()?`.
19 issuer_der: Vec<u8>,
20 /// Big-endian serial number bytes.
21 /// Obtain from `cert.tbs_certificate().serial_number().as_bytes()`.
22 serial: Vec<u8>,
23 },
24 /// Identified by the raw bytes of the Subject Key Identifier extension value
25 /// (RFC 5652 §6.2.2).
26 /// Obtain from the cert's Subject Key Identifier extension:
27 /// `cert.tbs_certificate().get_extension::<SubjectKeyIdentifier>()`.
28 SubjectKeyIdentifier(Vec<u8>),
29}
30
31impl fmt::Display for RecipientIdentifier {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 RecipientIdentifier::IssuerAndSerialNumber { issuer_der, serial } => {
35 write!(
36 f,
37 "IssuerAndSerial(issuer={} bytes, serial=",
38 issuer_der.len()
39 )?;
40 for (i, b) in serial.iter().enumerate() {
41 if i > 0 {
42 write!(f, ":")?;
43 }
44 write!(f, "{b:02x}")?;
45 }
46 write!(f, ")")
47 }
48 RecipientIdentifier::SubjectKeyIdentifier(ski) => {
49 write!(f, "SubjectKeyIdentifier(")?;
50 for (i, b) in ski.iter().enumerate() {
51 if i > 0 {
52 write!(f, ":")?;
53 }
54 write!(f, "{b:02x}")?;
55 }
56 write!(f, ")")
57 }
58 }
59 }
60}
61
62/// Elliptic curve selection for ECDH key agreement.
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[non_exhaustive]
65pub enum EcCurve {
66 P256,
67 P384,
68}
69
70impl fmt::Display for EcCurve {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 match self {
73 EcCurve::P256 => f.write_str("P-256"),
74 EcCurve::P384 => f.write_str("P-384"),
75 }
76 }
77}
78
79/// Algorithm used to encrypt (wrap) the content-encryption key.
80///
81/// # ECDH / KARI
82///
83/// ECDH key agreement (`KeyAgreeRecipientInfo`, RFC 5753) uses a *separate*
84/// trait method: [`DecryptionKey::agree_ecdh`]. ECDH key agreement for
85/// decryption uses `DecryptionKey::agree_ecdh()` directly, not `decrypt_cek()`.
86/// The `EcdhEs` variant exists for completeness but `decrypt_cek()` implementations
87/// do not need to handle it.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[non_exhaustive]
90pub enum KeyEncryptionAlgorithm {
91 /// RSA PKCS#1 v1.5 key transport (RFC 8017).
92 RsaPkcs1v15,
93 /// RSA-OAEP key transport (RFC 8017).
94 RsaOaep,
95 /// Reserved for future ECDH-ES key agreement support. Never produced by
96 /// the current implementation of `decrypt()`; present for forward
97 /// compatibility only.
98 ///
99 /// ECDH key agreement is handled via [`DecryptionKey::agree_ecdh`] instead.
100 /// `decrypt_cek()` implementations do not need to handle this variant.
101 EcdhEs { curve: EcCurve },
102}
103
104impl fmt::Display for KeyEncryptionAlgorithm {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 KeyEncryptionAlgorithm::RsaPkcs1v15 => f.write_str("RSA-PKCS1v15"),
108 KeyEncryptionAlgorithm::RsaOaep => f.write_str("RSA-OAEP"),
109 KeyEncryptionAlgorithm::EcdhEs { curve } => write!(f, "ECDH-ES/{curve}"),
110 }
111 }
112}
113
114/// AES key wrap algorithm used to protect the content-encryption key in KARI.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[non_exhaustive]
117pub enum KeyWrapAlgorithm {
118 /// AES-128 key wrap (`id-aes128-Wrap`, RFC 3394).
119 Aes128Kw,
120 /// AES-256 key wrap (`id-aes256-Wrap`, RFC 3394).
121 Aes256Kw,
122}
123
124impl fmt::Display for KeyWrapAlgorithm {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 match self {
127 KeyWrapAlgorithm::Aes128Kw => f.write_str("AES-128-KW"),
128 KeyWrapAlgorithm::Aes256Kw => f.write_str("AES-256-KW"),
129 }
130 }
131}
132
133/// ECDH key derivation scheme used in `KeyAgreeRecipientInfo` (RFC 5753 §7.1.4).
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135#[non_exhaustive]
136pub enum KariKeyAgreement {
137 /// `dhSinglePass-stdDH-sha256kdf-scheme` — P-256 with X9.63 KDF using SHA-256.
138 StdDhSha256Kdf,
139 /// `dhSinglePass-stdDH-sha384kdf-scheme` — P-384 with X9.63 KDF using SHA-384.
140 StdDhSha384Kdf,
141}
142
143impl fmt::Display for KariKeyAgreement {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 match self {
146 KariKeyAgreement::StdDhSha256Kdf => f.write_str("stdDH-SHA256-KDF"),
147 KariKeyAgreement::StdDhSha384Kdf => f.write_str("stdDH-SHA384-KDF"),
148 }
149 }
150}
151
152/// Combined algorithm parameters for ECDH key agreement (`KeyAgreeRecipientInfo`).
153///
154/// Passed to [`DecryptionKey::agree_ecdh`] so the implementor knows which
155/// ECDH variant and key wrap algorithm to apply.
156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
157#[non_exhaustive]
158pub struct KariAlgorithm {
159 /// ECDH key derivation scheme (determines curve and KDF hash).
160 pub key_agreement: KariKeyAgreement,
161 /// AES key wrap algorithm used to protect the CEK.
162 pub key_wrap: KeyWrapAlgorithm,
163}
164
165impl KariAlgorithm {
166 /// Construct a `KariAlgorithm` with the given key agreement and wrap schemes.
167 ///
168 /// `KariAlgorithm` is `#[non_exhaustive]` so external callers cannot use
169 /// struct expression syntax. Use this constructor instead.
170 #[must_use]
171 pub fn new(key_agreement: KariKeyAgreement, key_wrap: KeyWrapAlgorithm) -> Self {
172 Self {
173 key_agreement,
174 key_wrap,
175 }
176 }
177}
178
179/// Digest algorithm used when creating or verifying a signature.
180#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
181#[non_exhaustive]
182pub enum DigestAlgorithm {
183 Sha256,
184 Sha384,
185 Sha512,
186}
187
188impl fmt::Display for DigestAlgorithm {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 match self {
191 DigestAlgorithm::Sha256 => f.write_str("sha-256"),
192 DigestAlgorithm::Sha384 => f.write_str("sha-384"),
193 DigestAlgorithm::Sha512 => f.write_str("sha-512"),
194 }
195 }
196}
197
198/// Abstraction over a private key capable of decrypting an S/MIME message.
199///
200/// Implementors provide the key-unwrap step: given an encrypted CEK and the
201/// algorithm that was used to wrap it, return the raw CEK bytes.
202pub trait DecryptionKey {
203 /// Decrypt an encrypted content-encryption key.
204 fn decrypt_cek(
205 &self,
206 encrypted_key: &[u8],
207 algorithm: &KeyEncryptionAlgorithm,
208 ) -> Result<Vec<u8>, SmimeError>;
209
210 /// Returns `true` if this key matches the given `RecipientIdentifier`.
211 ///
212 /// # Example
213 ///
214 /// `RecipientIdentifier` is `#[non_exhaustive]`, so your implementation
215 /// **must** include a catch-all arm (`_ => false`) to remain forward-compatible
216 /// with future identifier variants.
217 ///
218 /// ```ignore
219 /// fn matches_recipient(&self, id: &RecipientIdentifier) -> bool {
220 /// match id {
221 /// RecipientIdentifier::IssuerAndSerialNumber { issuer_der, serial } => {
222 /// // issuer_der: DER-encoded Name — compare against
223 /// // cert.tbs_certificate().issuer().to_der().unwrap_or_default()
224 /// // serial: big-endian bytes — compare against
225 /// // cert.tbs_certificate().serial_number().as_bytes()
226 /// self.cert_issuer_der() == issuer_der
227 /// && self.cert_serial() == serial
228 /// }
229 /// RecipientIdentifier::SubjectKeyIdentifier(ski) => {
230 /// // ski: raw bytes of the Subject Key Identifier extension value —
231 /// // obtain via cert.tbs_certificate()
232 /// // .get_extension::<SubjectKeyIdentifier>()
233 /// self.cert_ski() == ski
234 /// }
235 /// _ => false, // required: RecipientIdentifier is #[non_exhaustive]
236 /// }
237 /// }
238 /// ```
239 fn matches_recipient(&self, id: &RecipientIdentifier) -> bool;
240
241 /// Perform ECDH key agreement and unwrap the content-encryption key.
242 ///
243 /// Called when the `EnvelopedData` contains a KARI (`KeyAgreeRecipientInfo`)
244 /// entry that matches this key (as determined by `matches_recipient`).
245 ///
246 /// The implementor must:
247 /// 1. Perform ECDH using the recipient's private key and
248 /// `ephemeral_public_key_bytes` (SEC1 uncompressed EC point) to obtain
249 /// the shared secret *Z*.
250 /// 2. Apply the X9.63 KDF (RFC 5753 §3.6.1): hash *Z* concatenated with a
251 /// counter and the `EccCmsSharedInfo` structure to derive the
252 /// key-encryption key (KEK). `SharedInfo` encodes `alg.key_wrap` OID in
253 /// `keyInfo` and, when `ukm` is `Some`, the UKM bytes in
254 /// `entityUInfo [0] EXPLICIT`. When `ukm` is `None` (which is always
255 /// the case when this crate's `encrypt()` produced the message, as this
256 /// crate never generates UKM), omit `entityUInfo`.
257 /// 3. Unwrap `enc_cek` with AES-128-KW or AES-256-KW (per `alg.key_wrap`)
258 /// using the derived KEK.
259 /// 4. Return the raw CEK bytes.
260 ///
261 /// # Default — IMPORTANT for EC key implementors
262 ///
263 /// Returns `Err(SmimeError::UnsupportedAlgorithm(...))`. **EC key
264 /// implementors MUST override this method** or KARI-encrypted messages
265 /// will silently fail to decrypt (the error is caught per-recipient
266 /// and the next `RecipientInfo` is tried; if no other recipient matches,
267 /// `NoMatchingRecipient` is returned with no indication that ECDH was
268 /// attempted). RSA-only keys can leave the default.
269 fn agree_ecdh(
270 &self,
271 ephemeral_public_key_bytes: &[u8],
272 ukm: Option<&[u8]>,
273 enc_cek: &[u8],
274 alg: &KariAlgorithm,
275 ) -> Result<Vec<u8>, SmimeError> {
276 let _ = (ephemeral_public_key_bytes, ukm, enc_cek, alg);
277 Err(SmimeError::UnsupportedAlgorithm(
278 "KARI (ECDH key agreement) not supported by this key".into(),
279 ))
280 }
281
282 /// Returns a hint about which key encryption algorithms this key supports.
283 ///
284 /// This crate does **not** consult this hint internally — `decrypt()` always
285 /// tries all recipients and lets `decrypt_cek()` / `agree_ecdh()` reject
286 /// unsupported algorithms. This method is provided as an extension point for
287 /// callers that want to pre-screen or log algorithm capabilities.
288 ///
289 /// Default: returns `None` (no hint; let `decrypt_cek()` reject unsupported ones).
290 fn supported_key_enc_algorithm(&self) -> Option<KeyEncryptionAlgorithm> {
291 None
292 }
293}
294
295/// Trait for checking certificate revocation status during signature verification.
296///
297/// This crate makes no network calls. Callers that need OCSP or CRL checking
298/// implement this trait and inject it into [`crate::verify()`].
299///
300/// The no-op implementation [`NoRevocationCheck`] is provided for cases where
301/// revocation checking is not needed (e.g. tests, trusted internal PKI,
302/// air-gapped deployments).
303pub trait RevocationChecker {
304 /// Check whether `cert` has been revoked.
305 ///
306 /// Return `Ok(())` if the certificate is valid (not revoked or revocation
307 /// status is acceptable). Return `Err(SmimeError::CertChain(...))` if the
308 /// certificate is revoked or its revocation status cannot be confirmed.
309 fn check(&self, cert: &x509_cert::Certificate) -> Result<(), SmimeError>;
310}
311
312/// A no-op [`RevocationChecker`] that accepts all certificates without
313/// consulting OCSP or CRL.
314///
315/// Use this when revocation checking is managed out-of-band, not required
316/// for the deployment, or during testing with synthetic certificates.
317///
318/// # Security
319///
320/// **This disables all certificate revocation checks.** Use only in environments where
321/// revocation is verified by other means, or in testing. Never use in production without
322/// explicit awareness that revoked certificates will be accepted.
323#[derive(Debug)]
324pub struct NoRevocationCheck;
325
326impl RevocationChecker for NoRevocationCheck {
327 fn check(&self, _cert: &x509_cert::Certificate) -> Result<(), SmimeError> {
328 Ok(())
329 }
330}
331
332/// Abstraction over a private key capable of signing an S/MIME message.
333///
334/// Implementors supply the raw signature bytes and the signer's certificate,
335/// which is embedded in the `SignedData` structure.
336pub trait SigningKey {
337 /// Sign `data` and return the signature bytes.
338 ///
339 /// The `data` argument is the DER-encoded `SignedAttributes` structure (a SET OF Attribute
340 /// per RFC 5652 §5.4), with tag byte `0x31`. The implementor must sign exactly these bytes
341 /// — do not pre-hash, do not add framing. For HSM integrations, the entire byte slice must
342 /// be transmitted as-is to the signing operation.
343 ///
344 /// **ECDSA format requirement**: For ECDSA keys, the returned bytes MUST be DER-encoded
345 /// (ASN.1 `SEQUENCE { r INTEGER, s INTEGER }`), as produced by RustCrypto `p256`/`p384`
346 /// signers. Raw fixed-width (`r || s`) format (IEEE P1363) is NOT accepted — `verify()`
347 /// will reject it when calling `DerSignature::try_from`.
348 fn sign(&self, data: &[u8], algorithm: &DigestAlgorithm) -> Result<Vec<u8>, SmimeError>;
349
350 /// The signer's X.509 certificate, included in `SignedData`.
351 fn certificate(&self) -> &x509_cert::Certificate;
352
353 /// Returns the preferred digest algorithm for this key.
354 ///
355 /// [`crate::sign`] consults this method when selecting the digest algorithm:
356 /// if `Some(alg)` is returned, that algorithm is used; otherwise the default
357 /// is derived from the key's certificate (P-256 → SHA-256, P-384 → SHA-384,
358 /// P-521 → SHA-512, RSA → SHA-256).
359 ///
360 /// Override to force a specific algorithm regardless of the certificate's key type.
361 ///
362 /// Default: returns `None` (let `sign()` derive the algorithm from the cert).
363 fn preferred_digest_algorithm(&self) -> Option<DigestAlgorithm> {
364 None
365 }
366}