pdf_oxide/crypto/provider.rs
1//! The [`CryptoProvider`] trait family.
2//!
3//! These traits decouple PDF encryption and signature paths from any
4//! one cryptography crate, so deployments that need a FIPS 140-3
5//! validated module (`aws-lc-rs` with the `fips` feature) or a
6//! sovereign-jurisdiction provider (GOST R 34.11/34.10, Chinese
7//! SM2/SM3/SM4) can swap in a different backend without touching the
8//! parsing or signature-construction code.
9//!
10//! See `docs/CRYPTO_PROVIDERS.md` (added in Phase 8) for the
11//! end-to-end story; tracking issue #236.
12//!
13//! # Trait shape
14//!
15//! Three sub-traits handle independent concerns:
16//!
17//! - [`Hasher`] — incremental hashing (`update` / `finalize`).
18//! - [`SymmetricCipher`] — AES-CBC (PKCS#7 and no-padding) and RC4.
19//! - [`SignatureVerifier`] — RSA-PKCS#1-v1.5 / RSA-PSS / ECDSA verify.
20//! - [`Signer`] — opaque signing handle (decouples PEM/DER loading
21//! from the call site so HSM / PKCS#11 providers can plug in).
22//!
23//! [`CryptoProvider`] composes them and adds policy
24//! ([`is_legacy_allowed`]) plus secure RNG.
25//!
26//! # FIPS posture
27//!
28//! Every provider documents what it permits via
29//! [`CryptoProvider::is_legacy_allowed`]. When `false`, MD5, SHA-1
30//! signing, RC4, and RSA-PKCS#1-v1.5 with SHA-1 return
31//! [`Error::AlgorithmNotPermitted`]. SHA-1 *verification* of
32//! historical signatures is permitted (NIST SP 800-131A) — the policy
33//! split happens in [`SignatureVerifier`] vs [`Signer`].
34
35use super::error::Result;
36use super::types::{
37 AesKeySize, EcCurve, HashAlgorithm, Padding, RsaPublicKey, RsaScheme, SigningAlgorithm,
38};
39
40/// Incremental hashing.
41///
42/// Modeled after the `digest` crate's `DynDigest` so providers can
43/// trivially adapt — but stripped to just the operations PDF needs
44/// (no XOF, no variable-output, no reset).
45pub trait Hasher: Send {
46 /// Feed input into the hash state. May be called any number of
47 /// times before [`Self::finalize`].
48 fn update(&mut self, data: &[u8]);
49
50 /// Finalize the hash, consuming `self`. The returned `Vec` is
51 /// exactly [`HashAlgorithm::output_size`] bytes long.
52 ///
53 /// Boxed receiver lets implementors live behind `Box<dyn Hasher>`
54 /// without paying for `Sized` constraints up the call stack.
55 fn finalize(self: Box<Self>) -> Vec<u8>;
56
57 /// Reports the algorithm so callers can sanity-check the output
58 /// size or feed the right OID into a CMS construction.
59 fn algorithm(&self) -> HashAlgorithm;
60}
61
62/// Symmetric encryption operations PDF needs.
63///
64/// All methods return owned `Vec<u8>` to match the existing
65/// `src/encryption/aes.rs` / `src/encryption/rc4.rs` shape so Phase 3
66/// migration is mechanical. Performance-critical callers can be
67/// converted to streaming later (in-place CBC, etc.) without breaking
68/// the trait — adding methods is non-breaking.
69pub trait SymmetricCipher: Send + Sync {
70 /// AES-CBC encrypt.
71 ///
72 /// `key.len()` must equal `key_size.key_bytes()`; `iv.len()` must
73 /// be 16. With [`Padding::None`], `data.len()` must be a multiple
74 /// of 16.
75 fn aes_cbc_encrypt(
76 &self,
77 key_size: AesKeySize,
78 key: &[u8],
79 iv: &[u8],
80 data: &[u8],
81 padding: Padding,
82 ) -> Result<Vec<u8>>;
83
84 /// AES-CBC decrypt. Same argument constraints as
85 /// [`Self::aes_cbc_encrypt`].
86 fn aes_cbc_decrypt(
87 &self,
88 key_size: AesKeySize,
89 key: &[u8],
90 iv: &[u8],
91 data: &[u8],
92 padding: Padding,
93 ) -> Result<Vec<u8>>;
94
95 /// RC4 encrypt/decrypt (the operation is symmetric so one method
96 /// covers both directions).
97 ///
98 /// Required for PDF Standard Security R≤4 (ISO 32000-1 §7.6.3).
99 /// Returns [`super::error::Error::AlgorithmNotPermitted`] under FIPS providers.
100 fn rc4(&self, key: &[u8], data: &[u8]) -> Result<Vec<u8>>;
101}
102
103/// Verify a digital signature.
104///
105/// SHA-1 is permitted here per NIST SP 800-131A (verification of
106/// historical signatures). Use [`Signer`] for generation — that path
107/// rejects SHA-1 under FIPS.
108pub trait SignatureVerifier: Send + Sync {
109 /// Verify an RSA-PKCS#1-v1.5 signature over the raw *message* bytes.
110 ///
111 /// The implementation hashes `message` with `hash` internally — the
112 /// same convention as `verify_rsa_pss` and `verify_ecdsa`. Passing
113 /// the message (rather than a pre-computed digest) allows providers
114 /// that only expose a message-level API (e.g., aws-lc-rs 1.x) to
115 /// implement this without a lower-level primitive.
116 fn verify_rsa_pkcs1v15(
117 &self,
118 pubkey: &RsaPublicKey<'_>,
119 hash: HashAlgorithm,
120 message: &[u8],
121 signature: &[u8],
122 ) -> Result<()>;
123
124 /// Verify an RSA-PSS signature over the *message* bytes (PSS
125 /// internally applies the hash; salt length defaults to digest
126 /// size per RFC 8017 §9.1).
127 fn verify_rsa_pss(
128 &self,
129 pubkey: &RsaPublicKey<'_>,
130 hash: HashAlgorithm,
131 message: &[u8],
132 signature: &[u8],
133 ) -> Result<()>;
134
135 /// Verify an ECDSA signature over the *message* bytes. The
136 /// implementation applies the standard hash for the curve
137 /// (SHA-256 for P-256, SHA-384 for P-384) — `aws-lc-rs` and the
138 /// `p256` / `p384` crates' `Verifier::verify` already hash
139 /// internally.
140 ///
141 /// `pubkey_sec1` is the SEC1-encoded uncompressed public point
142 /// (`0x04 || X || Y`); `signature_der` is the ASN.1 DER-encoded
143 /// signature (the form CMS / X.509 carry).
144 fn verify_ecdsa(
145 &self,
146 curve: EcCurve,
147 pubkey_sec1: &[u8],
148 message: &[u8],
149 signature_der: &[u8],
150 ) -> Result<()>;
151}
152
153/// Opaque signing handle.
154///
155/// Separating the handle type from the trait lets a provider back
156/// `Signer` with anything: a software RSA key parsed from PKCS#8, an
157/// HSM session, a Cloud KMS reference, a smart-card PKCS#11 slot. The
158/// handle just has to remember its [`SigningAlgorithm`] and
159/// `sign(message)`.
160///
161/// # PDF context
162///
163/// `signer.rs::create_pkcs7_signature` only needs the final signing
164/// step to be opaque — DER/PEM key loading + CMS construction stay in
165/// non-trait code. The trait is therefore intentionally narrow.
166pub trait Signer: Send {
167 /// Reports which `(asymmetric algo, hash)` pair this signer
168 /// produces. Callers use this to populate the CMS
169 /// `digestAlgorithm` and `signatureAlgorithm` fields.
170 fn algorithm(&self) -> SigningAlgorithm;
171
172 /// Sign `message`. For RSA-PKCS#1-v1.5 the caller passes a
173 /// pre-built `DigestInfo` (algorithm OID + hashed bytes) and the
174 /// signer applies raw RSA. For RSA-PSS and ECDSA the caller
175 /// passes either the raw message (PSS internally hashes) or the
176 /// pre-computed digest (ECDSA), as documented per scheme.
177 ///
178 /// The exact "what does `message` mean" contract follows what the
179 /// existing CMS construction expects, so Phase 4 can keep
180 /// `signer.rs` byte-equal.
181 fn sign(&self, message: &[u8]) -> Result<Vec<u8>>;
182}
183
184/// The root trait that ties everything together.
185///
186/// Implementations live in:
187///
188/// - [`super::rust_provider::RustCryptoProvider`] (Phase 2) — default,
189/// uses `sha2`/`sha1`/`md-5`/`aes`/`rsa`/`p256`/`p384`. Permits
190/// legacy.
191/// - `super::aws_lc_provider::AwsLcProvider` (Phase 6, behind
192/// `--features fips`) — FIPS 140-3 validated. Refuses
193/// legacy.
194pub trait CryptoProvider: Send + Sync + 'static {
195 /// Human-readable provider name for logs / SBOM annotations.
196 fn name(&self) -> &'static str;
197
198 /// Whether legacy algorithms (MD5, SHA-1 sign, RC4) are allowed.
199 /// FIPS providers return `false`.
200 ///
201 /// Note: SHA-1 *verification* is allowed regardless — a separate
202 /// policy via [`SignatureVerifier`].
203 fn is_legacy_allowed(&self) -> bool;
204
205 /// Construct a hasher for `algo`. Returns
206 /// [`super::error::Error::AlgorithmNotPermitted`] if the
207 /// algorithm is forbidden under this provider's policy.
208 fn hasher(&self, algo: HashAlgorithm) -> Result<Box<dyn Hasher>>;
209
210 /// Returns the symmetric cipher implementation. Always Some — the
211 /// provider trait guarantees AES support; only `rc4()` may fail
212 /// at call time under FIPS.
213 fn symmetric(&self) -> &dyn SymmetricCipher;
214
215 /// Returns the signature verification implementation.
216 fn verifier(&self) -> &dyn SignatureVerifier;
217
218 /// Fill `out` with cryptographically strong random bytes. Both
219 /// providers source this from the OS RNG; `RustCryptoProvider`
220 /// uses `getrandom`, `AwsLcProvider` uses
221 /// `aws_lc_rs::rand::SystemRandom`.
222 fn random_bytes(&self, out: &mut [u8]) -> Result<()>;
223
224 /// Build a [`Signer`] from the provided [`SigningKeyMaterial`].
225 /// Software providers parse the PEM/DER bytes; HSM/PKCS#11
226 /// providers ignore the bytes path and instead consume their
227 /// own handle variant (the enum is `#[non_exhaustive]` so adding
228 /// `Pkcs11Slot` later isn't a breaking change).
229 fn signer(&self, key: &SigningKeyMaterial<'_>) -> Result<Box<dyn Signer>>;
230}
231
232/// Material for constructing a [`Signer`].
233///
234/// `#[non_exhaustive]` so HSM/Cloud-KMS providers can add their own
235/// variants in a follow-up release.
236#[non_exhaustive]
237#[derive(Debug)]
238pub enum SigningKeyMaterial<'a> {
239 /// PKCS#8 DER-encoded private key bytes. Software providers
240 /// (`RustCryptoProvider`, `AwsLcProvider`) parse this directly.
241 Pkcs8Der {
242 /// Algorithm hint so the provider knows which scheme + hash
243 /// to bind into the resulting signer.
244 algo: SigningAlgorithm,
245 /// PKCS#8 DER bytes.
246 bytes: &'a [u8],
247 },
248 /// PKCS#1 DER-encoded RSA private key (legacy software keys).
249 Pkcs1Der {
250 /// RSA padding scheme the resulting signer should apply.
251 scheme: RsaScheme,
252 /// Message digest the signer should use.
253 hash: HashAlgorithm,
254 /// PKCS#1 DER bytes.
255 bytes: &'a [u8],
256 },
257}