Skip to main content

oxideav_pdf/pubsec/
mod.rs

1//! PDF public-key security handler — ISO 32000-1 §7.6.4 +
2//! ISO 32000-2 §7.6.5.
3//!
4//! Round-10 work is **decoder-side**: a recipient with an
5//! X.509 certificate + RSA private key can open a PDF whose
6//! `/Encrypt /Filter` selects one of the public-key SubFilters:
7//!
8//! | SubFilter             | Symmetric algorithm            | Hash  |
9//! |-----------------------|--------------------------------|-------|
10//! | `adbe.pkcs7.s3`       | RC4-40 (per-object Algorithm 1) | SHA-1 |
11//! | `adbe.pkcs7.s4`       | RC4-128 (per-object Algorithm 1) | SHA-1 |
12//! | `adbe.pkcs7.s5` V≤4   | RC4-128 / AES-128 via crypt-filter `CFM`        | SHA-1 |
13//! | `adbe.pkcs7.s5` V=5   | AES-256 (no per-object derivation)              | SHA-256 |
14//!
15//! ## Algorithm summary
16//!
17//! 1. The trailer's `/Encrypt /Recipients` array (or, for `s5`, the
18//!    `/Encrypt /CF /<name> /Recipients` array) is one CMS
19//!    `EnvelopedData` per access-permission set. Each envelope's
20//!    `RecipientInfos` SET lists every certificate that may open
21//!    that permission set; the corresponding `encryptedKey` is the
22//!    content-encryption key (CEK) wrapped to that recipient's
23//!    public RSA key with `RSAES-PKCS1-v1_5`.
24//! 2. The reader matches one of its certificates against the
25//!    `IssuerAndSerialNumber` recipient identifier, RSA-decrypts the
26//!    CEK with the matching private key, and uses the CEK to decrypt
27//!    the envelope's `encryptedContent` (AES-CBC or RC4 per the
28//!    envelope's `contentEncryptionAlgorithm`).
29//! 3. The decrypted envelope is a 20-byte random seed followed by
30//!    optional 4 bytes of permission flags (least-significant byte
31//!    first, per ISO 32000-1 §7.6.4.3 — corrected to most-significant
32//!    byte first in ISO 32000-2:2020 §7.6.5.3).
33//! 4. The file encryption key is the first `n/8` bytes of the digest
34//!    over `seed || every_recipient_blob_in_array_order
35//!    [|| 0xFFFFFFFF if EncryptMetadata=false]`. The digest is
36//!    SHA-1 for the AES-128 / RC4 paths and SHA-256 for the AES-256
37//!    path (per ISO 32000-2:2020 §7.6.5.3).
38//! 5. The reader hands the resulting [`StandardHandler`] back to the
39//!    common decrypt path — string + stream payloads are decrypted
40//!    with `Algorithm 1` (V≤4) or with the file key directly (V=5),
41//!    exactly as the standard-handler reader already does.
42//!
43//! ## Provenance
44//!
45//! Implemented from spec PDFs only:
46//! `docs/document/pdf/PDF32000_2008.pdf` §7.6.4 + ISO 32000-2:2020
47//! §7.6.5; CMS DER from RFC 5652 §6; X.509 issuer/serial matching
48//! from RFC 5280 §4.1.2; RSA-PKCS1-v1.5 from RFC 8017 (PKCS#1).
49//!
50//! ## Round-11 additions
51//!
52//! * **Writer / encoder side** — the writer can now emit
53//!   public-key-encrypted PDFs symmetric to the round-10 reader.
54//!   See [`PubSecEncoderConfig`] + [`PubSecRecipient`] +
55//!   [`crate::write_pdf_from_scene_pubsec_encrypted`].
56//! * **`SubjectKeyIdentifier` recipient matching** — CMS v2
57//!   RecipientIdentifier wired through the parser + matcher per
58//!   RFC 5652 §6.2.1 + RFC 5280 §4.2.1.2 method 1. Both forms
59//!   are supported on read; the writer emits IAS by default but
60//!   accepts SKI per-recipient.
61//!
62//! ## Round-12 additions
63//!
64//! * **Per-crypt-filter recipient lists** — multiple named crypt
65//!   filters under `/CF`, each with its own `/Recipients` array. The
66//!   matcher tries every CF in turn; the first CF that contains a
67//!   recipient slot matching the user's certificate determines the
68//!   permissions surfaced. ISO 32000-1 §7.6.4.2 + §7.6.5.4 explicitly
69//!   permit different permission masks per recipient set (the "read
70//!   only" recipient is in one CF, the "full access" recipient in
71//!   another — both can decrypt with their respective rights). The
72//!   public read-side API is unchanged (the matched CF's permissions
73//!   are surfaced through the returned [`StandardHandler`] same as
74//!   before); the [`open_with_certificate_with_permissions`] variant
75//!   surfaces both the handler and the per-CF P value.
76//! * **CMS KARI variant** (decoder side, RFC 5652 §6.2.2) — KeyAgree
77//!   recipients (ECDH / DH) are now parsed structurally. The
78//!   originator + UKM + recipientEncryptedKeys fields are surfaced via
79//!   [`crate::pubsec::cms::RecipientInfoVariant::KeyAgree`]; the
80//!   [`open_with_certificate`] handler still requires KTRI for actual
81//!   unwrap because RFC 5753 KDF + key-wrap implementations are out of
82//!   scope here. Mixed-recipient envelopes (KTRI + KARI) decode
83//!   correctly via the KTRI side.
84//!
85//! ## Round-14 additions
86//!
87//! * **KARI unwrap** (RFC 5753 §7.1 + RFC 3394) — closes the round-12
88//!   deferral. P-256 ECDH + X9.63-SHA-256 KDF + AES Key Wrap (128 /
89//!   192 / 256 bit). Surfaces as the `OID_DH_SINGLE_PASS_STDDH_SHA256_KDF`
90//!   KEA OID.
91//! * **`PubSecCredential::from_parsed_ec_p256`** + `with_ec_p256_scalar`
92//!   constructors — populate the EC private scalar slot so a
93//!   credential can open both KTRI (RSA) and KARI (ECDH) envelopes.
94//!
95//! ## Round-15 additions
96//!
97//! * **P-384 + X25519 KARI variants** (RFC 5753 §7.1.4 +
98//!   RFC 8418 §2.1) — `dhSinglePass-stdDH-sha384kdf-scheme` (P-384) +
99//!   X25519 with the secg-scheme `dhSinglePass-stdDH-sha256kdf-scheme`
100//!   binding. Generic [`kari::x963_kdf`] + curve-tagged
101//!   [`kari::EcRecipient`]; the legacy P-256 entry point
102//!   [`kari::unwrap_kari_p256`] still works.
103//! * **`PubSecCredential::from_parsed_ec`** + `with_ec_scalar` —
104//!   populate the EC slot for any supported curve via
105//!   [`kari::KariCurve`]. The round-14 `_p256` variants forward here.
106//! * **Writer-side `crate::write_pdf_from_scene_pubsec_kari`** — the
107//!   symmetric encode-side helper for KARI envelopes (the round-11/12
108//!   pubsec writer was KTRI-only). Each [`crate::KariRecipient`] picks
109//!   the curve + cert; the writer derives the ephemeral keypair, runs
110//!   the right ECDH primitive, KDFs the KEK, and AES-KWs the CEK.
111//!
112//! ## Round-16 additions
113//!
114//! * **P-521 KARI** (RFC 5753 §7.1.4) — `dhSinglePass-stdDH-sha512kdf-scheme`,
115//!   OID 1.3.132.1.11.3. Closes the NIST KARI curve coverage; same
116//!   builder + reader path as P-256 / P-384 with X9.63-SHA-512 KDF +
117//!   AES-128/192/256-WRAP.
118//! * **RFC 8418 §2.2 HKDF binding for X25519** —
119//!   `dhSinglePass-stdDH-hkdf-sha256/384/512-scheme`, OIDs
120//!   `1.2.840.113549.1.9.16.3.{19,20,21}`. The X25519
121//!   `KariRecipient::x25519_hkdf_*` constructors switch the KDF on the
122//!   writer side; the reader auto-routes by parsing the KEA OID into a
123//!   [`kari::KariKdf`].
124//!
125//! ## Round-24 additions
126//!
127//! * **X448 KARI** (RFC 7748 §5 + RFC 8410 §3 + RFC 8418 §2.1 + §2.2).
128//!   `KariCurve::X448` (OID `1.3.101.111`, 56-byte raw u-coordinate,
129//!   224-bit security level) joins the existing P-256 / P-384 / P-521 /
130//!   X25519 dispatch. Default KDF binding is X9.63-SHA-512
131//!   (security-strength match); HKDF-SHA-256 / 384 / 512 are also valid
132//!   per RFC 8418 §2 via the new `KariRecipient::x448_hkdf_*`
133//!   constructors. RFC 7748 §6.2 Alice/Bob shared-secret vector
134//!   cross-checked. Backed by the pure-Rust `x448` (RustCrypto /
135//!   `ed448-goldilocks`) crate.
136//!
137//! ## Round-19 additions
138//!
139//! * **CMS `SignedData` parser scaffolding** (RFC 5652 §5 — PKCS#7).
140//!   Builds on the existing CMS DER + X.509 + EnvelopedData
141//!   infrastructure to add parser-side recognition of `id-signedData`
142//!   (OID `1.2.840.113549.1.7.2`) — the content type that wraps every
143//!   PDF digital signature (ISO 32000-1 §12.8). New
144//!   [`signed_data::SignedData`] + [`signed_data::SignerInfo`] +
145//!   [`signed_data::SignerIdentifier`] types; new
146//!   [`signed_data::parse_signed_data`] one-shot accessor. Surfaces
147//!   the certs[], crls[], digest_algorithms, encap_content, and
148//!   per-signer (sid, signed_attrs, signature_algorithm, signature)
149//!   fields. Signature **verification** (hash-then-verify dispatch
150//!   per algorithm) is deferred to round 20.
151//!
152//! ## Round-18 additions
153//!
154//! * **`OriginatorInfo certs[] / crls[]` surface** (RFC 5652 §10.2.1).
155//!   The `EnvelopedData.originatorInfo` field — previously parsed and
156//!   silently dropped — is now exposed via
157//!   [`cms::EnvelopedData::originator_info`] returning
158//!   `Option<&cms::OriginatorInfo>`. Each entry is the raw DER bytes of
159//!   one CertificateChoices / RevocationInfoChoices alternative.
160//! * **`RecipientKeyIdentifier { date, other }` parse**
161//!   (RFC 5652 §6.2.2). The OPTIONAL `date GeneralizedTime` and
162//!   `other OtherKeyAttribute` fields of an RKID — previously dropped
163//!   — are now captured. New
164//!   [`TrustStore::find_with_temporal_validity`] uses the RKID `date`
165//!   to pick among multiple certs sharing an SKI the one whose
166//!   validity window contains the instant. Useful for long-lived
167//!   archives where the same recipient identity has been re-certified
168//!   multiple times.
169//! * **`Certificate.validity` extraction** (RFC 5280 §4.1.2.5). The
170//!   `notBefore` / `notAfter` window is now captured, with `UTCTime`
171//!   normalised to `GeneralizedTime` (RFC 5280 §4.1.2.5.1's 1950..2049
172//!   pivot) so envelope `GeneralizedTime` instants byte-compare
173//!   directly. New helper [`x509::time_within`].
174//!
175//! ## Round-20 additions
176//!
177//! * **`SignedData` signature verification** (RFC 5652 §5.4 + §11.2 +
178//!   RFC 5754 + RFC 5758 + RFC 8017) — closes the round-19 deferral.
179//!   New [`verify::verify_signature`] entry point dispatches on the
180//!   per-`SignerInfo` `(digestAlgorithm, signatureAlgorithm)` OID pair:
181//!   * Hash side — SHA-1 / SHA-256 / SHA-384 / SHA-512.
182//!   * Signature side — RSA-PKCS#1 v1.5, RSA-PSS, and ECDSA on the
183//!     P-256 / P-384 / P-521 curves (curve dispatch by the cert SPKI's
184//!     named-curve OID, RFC 5480 §2.1.1.1).
185//!   * Re-encodes the `[0] IMPLICIT signedAttrs` body with the
186//!     universal SET tag before hashing, per RFC 5652 §5.4.
187//!   * Cross-checks the `messageDigest` signed attribute against the
188//!     hash of the encapsulated content (RFC 5652 §11.2), so a
189//!     tampered eContent fails even when the outer signature hashes
190//!     intact attrs.
191//! * **`x509::Certificate.spki_algorithm_oid` + `spki_algorithm_params`** —
192//!   the SPKI's `AlgorithmIdentifier` is now captured (in addition to
193//!   the BIT STRING contents already extracted in round 11) so the
194//!   verifier can route ECDSA on the named-curve OID without re-parsing
195//!   the certificate.
196//!
197//! ## Remaining deferrals
198//!
199//! * RC2 `rc2ParameterVersion` writer-side encode (read-only currently;
200//!   PDF 2.0 deprecates RC2 so the writer always emits AES — exposing
201//!   an RC2 encoder would only serve archive-replay tooling).
202//! * Full `CertificateChoices` CHOICE dispatch on `OriginatorInfo`
203//!   entries — round 18 surfaces them as opaque DER; future work could
204//!   tag-dispatch each entry into X.509 v3 / extended-cert /
205//!   attribute-cert / other-cert variants per RFC 5652 §10.2.2.
206//! * **Round-21 follow-ups** — PDF `/Sig` annotation **writer** (the
207//!   round-21 reader path already lands via
208//!   [`crate::reader::sig::signatures`]; the writer-side path that
209//!   lays out an `/AcroForm /Fields [.. Sig ..]` + signature dict with
210//!   reservable `/Contents` / `/ByteRange` slots is still deferred);
211//!   Ed25519 / Ed448 signature dispatch in the verifier (no Ed25519
212//!   / Ed448 dep yet); full `id-RSASSA-PSS` parameter parsing
213//!   (round-20 accepts the OID with default SHA-1 / SHA-256 /
214//!   SHA-384 / SHA-512 via `digestAlgorithm` but doesn't yet honour
215//!   explicit MGF1 hash / salt-length parameters if they differ from
216//!   the digest hash).
217
218pub mod cms;
219pub mod cms_build;
220pub mod der;
221pub mod encode;
222pub mod kari;
223pub mod signed_data;
224pub mod trust;
225pub mod verify;
226pub mod x509;
227
228pub use encode::{
229    KariRecipient, PubSecCfGroup, PubSecEncoderConfig, PubSecEncryptionState, PubSecKariConfig,
230    PubSecMultiCfConfig, PubSecRecipient,
231};
232pub use trust::{CertRef, TrustStore};
233
234use crate::decrypt::{CryptMethod, StandardHandler};
235use crate::error::PdfError;
236use crate::objects::{Dict, Object};
237
238/// Identifies the public-key SubFilter the encryption dictionary
239/// declares. Maps to the symmetric algorithm + key length the
240/// resulting `StandardHandler` will use for per-object encryption.
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum PubSecSubFilter {
243    /// `adbe.pkcs7.s3` — RC4-40, V=1.
244    Pkcs7S3,
245    /// `adbe.pkcs7.s4` — RC4-128, V=2.
246    Pkcs7S4,
247    /// `adbe.pkcs7.s5` with V=4 — RC4-128 or AES-128 via crypt-filter `CFM`.
248    Pkcs7S5V4 { aes: bool },
249    /// `adbe.pkcs7.s5` with V=5 — AES-256, `CFM=AESV3`.
250    Pkcs7S5V5,
251}
252
253impl PubSecSubFilter {
254    fn from_dict(d: &Dict) -> Result<Self, PdfError> {
255        let lookup = |k: &str| {
256            d.entries()
257                .iter()
258                .find(|(kk, _)| kk == k)
259                .map(|(_, v)| v.clone())
260        };
261        let sub =
262            match lookup("SubFilter") {
263                Some(Object::Name(n)) => n,
264                _ => return Err(PdfError::other(
265                    "PDF pubsec: /Encrypt missing /SubFilter (required for public-key handlers)",
266                )),
267            };
268        let v = match lookup("V") {
269            Some(Object::Integer(n)) => n,
270            _ => {
271                return Err(PdfError::other(
272                    "PDF pubsec: /Encrypt missing /V (required)",
273                ))
274            }
275        };
276        match (sub.as_str(), v) {
277            ("adbe.pkcs7.s3", _) => Ok(Self::Pkcs7S3),
278            ("adbe.pkcs7.s4", _) => Ok(Self::Pkcs7S4),
279            ("adbe.pkcs7.s5", 4) => {
280                let aes = matches!(stmf_cfm(d).as_deref(), Some("AESV2"));
281                Ok(Self::Pkcs7S5V4 { aes })
282            }
283            ("adbe.pkcs7.s5", 5) => Ok(Self::Pkcs7S5V5),
284            ("adbe.pkcs7.s5", other) => Err(PdfError::other(format!(
285                "PDF pubsec: adbe.pkcs7.s5 with /V={other} not supported (V∈{{4,5}})"
286            ))),
287            (other, _) => Err(PdfError::other(format!(
288                "PDF pubsec: SubFilter={other} not recognised"
289            ))),
290        }
291    }
292}
293
294/// Resolve `/CF /<StmF> /CFM` from an `/Encrypt` dict. Returns `None`
295/// when any link in the chain is missing.
296fn stmf_cfm(d: &Dict) -> Option<String> {
297    let entries = d.entries();
298    let stmf = entries
299        .iter()
300        .find(|(k, _)| k == "StmF")
301        .and_then(|(_, v)| {
302            if let Object::Name(s) = v {
303                Some(s.clone())
304            } else {
305                None
306            }
307        })?;
308    let cf = entries.iter().find(|(k, _)| k == "CF").and_then(|(_, v)| {
309        if let Object::Dict(d) = v {
310            Some(d.clone())
311        } else {
312            None
313        }
314    })?;
315    let filter = cf
316        .entries()
317        .iter()
318        .find(|(k, _)| k == &stmf)
319        .and_then(|(_, v)| {
320            if let Object::Dict(d) = v {
321                Some(d.clone())
322            } else {
323                None
324            }
325        })?;
326    filter
327        .entries()
328        .iter()
329        .find(|(k, _)| k == "CFM")
330        .and_then(|(_, v)| {
331            if let Object::Name(s) = v {
332                Some(s.clone())
333            } else {
334                None
335            }
336        })
337}
338
339/// User-supplied credential — an X.509 certificate (DER-encoded) and
340/// the matching private key (RSA for KTRI envelopes, EC for round-14
341/// KARI envelopes). The certificate identifier (`IssuerAndSerialNumber`
342/// from RFC 5280) is extracted from the certificate's DER body.
343///
344/// Round 14 adds the optional `ec_private_scalar` slot — the
345/// recipient's raw P-256 SEC1 scalar (32 bytes). When present, KARI
346/// envelopes matching the same certificate can also be unwrapped (RFC
347/// 5753 §7.1 + RFC 3394). When absent, KARI envelopes are skipped
348/// (the original round-12 behaviour).
349///
350/// Round 15 generalises the EC slot to carry a [`kari::KariCurve`] tag
351/// alongside the scalar, so the same credential can open KARI
352/// envelopes on any of the supported curves (P-256 / P-384 / P-521 /
353/// X25519 / X448 — round 24). The `from_parsed_ec_p256` /
354/// `with_ec_p256_scalar` round-14 helpers keep working — they default
355/// the curve to [`kari::KariCurve::P256`].
356pub struct PubSecCredential {
357    pub(crate) cert: x509::Certificate,
358    pub(crate) private_key: Option<rsa::RsaPrivateKey>,
359    /// Optional EC private scalar + curve tag — populates the KARI
360    /// unwrap path. When `None`, KARI recipient slots that match this
361    /// certificate's RID are silently skipped.
362    pub(crate) ec_private: Option<(kari::KariCurve, Vec<u8>)>,
363}
364
365impl PubSecCredential {
366    /// Build a credential from a DER-encoded X.509 certificate and a
367    /// PKCS#8-encoded RSA private key (DER, the `PrivateKeyInfo` form
368    /// of RFC 5958).
369    pub fn from_der(cert_der: &[u8], pkcs8_der: &[u8]) -> Result<Self, PdfError> {
370        use rsa::pkcs8::DecodePrivateKey;
371        let cert = x509::Certificate::parse(cert_der)?;
372        let private_key = rsa::RsaPrivateKey::from_pkcs8_der(pkcs8_der)
373            .map_err(|e| PdfError::other(format!("PDF pubsec: RSA private key parse: {e}")))?;
374        Ok(Self {
375            cert,
376            private_key: Some(private_key),
377            ec_private: None,
378        })
379    }
380
381    /// Build directly from a parsed certificate + RSA key — used by
382    /// fixture builders inside the crate (and by integration tests
383    /// in `tests/pubsec.rs`).
384    #[doc(hidden)]
385    pub fn from_parsed(cert: x509::Certificate, private_key: rsa::RsaPrivateKey) -> Self {
386        Self {
387            cert,
388            private_key: Some(private_key),
389            ec_private: None,
390        }
391    }
392
393    /// Round-14: build a credential from a parsed certificate + a
394    /// P-256 SEC1 raw private scalar (32 bytes). Used to open a
395    /// KARI-encrypted PDF whose recipient slot matches this certificate.
396    ///
397    /// The `cert.spki_pubkey_bits` slot — when populated — is used to
398    /// match the recipient's `RecipientKeyIdentifier(SKI)` form. For
399    /// an EC certificate, `spki_pubkey_bits` is the SEC1-encoded
400    /// public point.
401    pub fn from_parsed_ec_p256(cert: x509::Certificate, ec_private_scalar: Vec<u8>) -> Self {
402        Self::from_parsed_ec(cert, kari::KariCurve::P256, ec_private_scalar)
403    }
404
405    /// Round-15: build a credential from a parsed certificate + an EC
406    /// private scalar on the supplied curve. Pass [`kari::KariCurve::P384`]
407    /// or [`kari::KariCurve::X25519`] for the round-15 curves; round-16
408    /// adds [`kari::KariCurve::P521`] and round-24 adds
409    /// [`kari::KariCurve::X448`] to the same surface.
410    pub fn from_parsed_ec(
411        cert: x509::Certificate,
412        curve: kari::KariCurve,
413        ec_private_scalar: Vec<u8>,
414    ) -> Self {
415        Self {
416            cert,
417            private_key: None,
418            ec_private: Some((curve, ec_private_scalar)),
419        }
420    }
421
422    /// Round-14: extend an existing credential with a P-256 EC private
423    /// scalar. Allows a single credential to unwrap both KTRI (RSA)
424    /// and KARI (ECDH) envelopes — typical for a recipient who carries
425    /// both a long-term RSA cert and a separate EC cert under the same
426    /// identity.
427    pub fn with_ec_p256_scalar(self, ec_private_scalar: Vec<u8>) -> Self {
428        self.with_ec_scalar(kari::KariCurve::P256, ec_private_scalar)
429    }
430
431    /// Round-15: extend an existing credential with an EC scalar on
432    /// the supplied curve.
433    pub fn with_ec_scalar(mut self, curve: kari::KariCurve, ec_private_scalar: Vec<u8>) -> Self {
434        self.ec_private = Some((curve, ec_private_scalar));
435        self
436    }
437}
438
439/// Per-CF surface returned by [`open_with_certificate_with_permissions`].
440/// The standard [`open_with_certificate`] discards the permission /
441/// CF-name fields and surfaces only the [`StandardHandler`] for
442/// backwards compatibility with round-10 callers.
443#[derive(Debug, Clone)]
444pub struct PubSecMatch {
445    /// File-encryption handler the matched CF derived. Feeds straight
446    /// into the per-object decrypt path.
447    pub handler: StandardHandler,
448    /// Permission mask carried by the matched envelope's plaintext
449    /// trailer (4-byte signed integer, per ISO 32000-1 §7.6.4.3 / ISO
450    /// 32000-2 §7.6.5.3). `None` when the envelope plaintext is the
451    /// 20-byte seed alone.
452    pub permissions: Option<i32>,
453    /// Name of the crypt filter under `/CF` whose `/Recipients` slot
454    /// matched. `None` for the document-level `/Recipients` path used
455    /// by `s3` / `s4` (no per-CF differentiation possible).
456    pub crypt_filter_name: Option<String>,
457}
458
459/// Open a public-key-encrypted PDF given the trailer's `/Encrypt`
460/// dict and the user's credential. Returns the file-encryption
461/// handler the matched recipient set produced.
462///
463/// For `s5` envelopes that thread different permission sets through
464/// distinct named crypt filters (per ISO 32000-1 §7.6.4.2 + §7.6.5.4),
465/// every `/CF /<name> /Recipients` array is tried in declaration
466/// order. The first CF whose recipient slot matches the user's
467/// certificate determines the file encryption key + permissions.
468///
469/// Returns `Ok(None)` when no recipient slot in any envelope matches
470/// the supplied certificate (analogous to a wrong password).
471pub fn open_with_certificate(
472    encrypt: &Dict,
473    credential: &PubSecCredential,
474) -> Result<Option<StandardHandler>, PdfError> {
475    Ok(open_with_certificate_with_permissions(encrypt, credential)?.map(|m| m.handler))
476}
477
478/// Round-17: variant of [`open_with_certificate`] that consults a
479/// [`TrustStore`] for KARI envelopes whose `OriginatorIdentifierOrKey`
480/// is `IssuerAndSerial` or `SubjectKeyIdentifier` (RFC 5652 §6.2.2)
481/// rather than the in-band `OriginatorPublicKey` form.
482///
483/// When the originator side is a long-term cert reference, the trust
484/// store provides the originator's public point (extracted from the
485/// referenced certificate's SPKI BIT STRING contents). The recipient's
486/// own credential supplies the EC private scalar as before.
487///
488/// In-band `OriginatorPublicKey` envelopes still work without
489/// consulting the trust store — the lookup path is only triggered for
490/// the long-term-cert forms.
491pub fn open_with_certificate_and_trust_store(
492    encrypt: &Dict,
493    credential: &PubSecCredential,
494    trust_store: &TrustStore,
495) -> Result<Option<StandardHandler>, PdfError> {
496    Ok(
497        open_with_certificate_and_trust_store_with_permissions(encrypt, credential, trust_store)?
498            .map(|m| m.handler),
499    )
500}
501
502/// Round-17: extended trust-store entry point that surfaces the matched
503/// CF's name + envelope permissions alongside the handler. Same role as
504/// [`open_with_certificate_with_permissions`] but for the trust-store
505/// path.
506pub fn open_with_certificate_and_trust_store_with_permissions(
507    encrypt: &Dict,
508    credential: &PubSecCredential,
509    trust_store: &TrustStore,
510) -> Result<Option<PubSecMatch>, PdfError> {
511    open_inner(encrypt, credential, Some(trust_store))
512}
513
514/// Round-12 extended entry point — same matching rules as
515/// [`open_with_certificate`] but surfaces the matched CF's name +
516/// envelope permissions alongside the handler. Lets a caller display
517/// "you have read-only access via the `ReadOnlyCF` recipient set"
518/// without re-parsing the trailer.
519pub fn open_with_certificate_with_permissions(
520    encrypt: &Dict,
521    credential: &PubSecCredential,
522) -> Result<Option<PubSecMatch>, PdfError> {
523    open_inner(encrypt, credential, None)
524}
525
526fn open_inner(
527    encrypt: &Dict,
528    credential: &PubSecCredential,
529    trust_store: Option<&TrustStore>,
530) -> Result<Option<PubSecMatch>, PdfError> {
531    let sub_filter = PubSecSubFilter::from_dict(encrypt)?;
532    let candidates = collect_recipient_arrays(encrypt)?;
533    if candidates.is_empty() {
534        return Err(PdfError::other(
535            "PDF pubsec: no /Recipients arrays found (document-level or per-CF)",
536        ));
537    }
538
539    let encrypt_metadata = match encrypt
540        .entries()
541        .iter()
542        .find(|(k, _)| k == "EncryptMetadata")
543    {
544        Some((_, Object::Bool(b))) => *b,
545        _ => true,
546    };
547
548    // Walk every CF candidate, then within it walk every recipient
549    // blob until one matches. ISO 32000-1 §7.6.4.2: "There shall be
550    // only one PKCS#7 object per unique set of access permissions; if
551    // a recipient appears in more than one list, the permissions used
552    // shall be those in the first matching list."
553    for candidate in &candidates {
554        for blob in &candidate.blobs {
555            let envelope = cms::parse_envelope(blob)?;
556            let Some(plaintext) = try_unwrap(&envelope, credential, trust_store)? else {
557                continue;
558            };
559            // Plaintext is `seed (20 bytes) [|| 4 bytes permissions]`.
560            if plaintext.len() < 20 {
561                return Err(PdfError::other(format!(
562                    "PDF pubsec: enveloped content too short ({} < 20 bytes)",
563                    plaintext.len()
564                )));
565            }
566            let seed = &plaintext[..20];
567            let permissions = if plaintext.len() >= 24 {
568                // ISO 32000-2 stores MSB-first; ISO 32000-1 stores
569                // LSB-first. We pick by SubFilter.
570                let p_bytes = &plaintext[20..24];
571                let p_arr: [u8; 4] = [p_bytes[0], p_bytes[1], p_bytes[2], p_bytes[3]];
572                let p = match sub_filter {
573                    PubSecSubFilter::Pkcs7S5V5 => i32::from_be_bytes(p_arr),
574                    _ => i32::from_le_bytes(p_arr),
575                };
576                Some(p)
577            } else {
578                None
579            };
580
581            // Per-CF candidate determines its own algorithm + key
582            // length (the dict-level CFM is overridden when the
583            // matched filter has its own CFM).
584            let (method, revision) = candidate
585                .method_revision
586                .unwrap_or_else(|| default_method_revision(sub_filter));
587            let default_key_bits = match sub_filter {
588                PubSecSubFilter::Pkcs7S3 => 40,
589                PubSecSubFilter::Pkcs7S4 => 128,
590                PubSecSubFilter::Pkcs7S5V4 { .. } => 128,
591                PubSecSubFilter::Pkcs7S5V5 => 256,
592            };
593            let key_bits = candidate.key_length_bits.unwrap_or(default_key_bits);
594            let key = derive_file_key(
595                sub_filter,
596                seed,
597                &candidate.blobs,
598                encrypt_metadata,
599                key_bits,
600            );
601            return Ok(Some(PubSecMatch {
602                handler: StandardHandler {
603                    key,
604                    method,
605                    revision,
606                },
607                permissions,
608                crypt_filter_name: candidate.cf_name.clone(),
609            }));
610        }
611    }
612    Ok(None)
613}
614
615fn default_method_revision(sub_filter: PubSecSubFilter) -> (CryptMethod, u8) {
616    match sub_filter {
617        PubSecSubFilter::Pkcs7S3 => (CryptMethod::Rc4, 2u8),
618        PubSecSubFilter::Pkcs7S4 => (CryptMethod::Rc4, 3),
619        PubSecSubFilter::Pkcs7S5V4 { aes } => (
620            if aes {
621                CryptMethod::Aes128
622            } else {
623                CryptMethod::Rc4
624            },
625            4,
626        ),
627        PubSecSubFilter::Pkcs7S5V5 => (CryptMethod::Aes256, 6),
628    }
629}
630
631/// One candidate recipient set — either the document-level
632/// `/Recipients` (for `s3` / `s4`) or one named crypt filter under
633/// `/CF` (for `s5`). For per-CF candidates the CFM + Length determine
634/// the symmetric algorithm; the document-level fallback inherits from
635/// the encrypt dict (`StmF` lookup or default 128-bit RC4).
636struct RecipientCandidate {
637    /// Named crypt filter the candidate originated from. `None` for
638    /// the document-level `/Recipients` slot.
639    cf_name: Option<String>,
640    /// One PKCS#7 EnvelopedData blob per "permission set".
641    blobs: Vec<Vec<u8>>,
642    /// Per-CF key length override (None = inherit from dict-level).
643    key_length_bits: Option<usize>,
644    /// Per-CF (method, revision) override (None = inherit from
645    /// dict-level via `default_method_revision`).
646    method_revision: Option<(CryptMethod, u8)>,
647}
648
649/// Collect every `/Recipients` candidate the encrypt dict references.
650///
651/// Round 12 generalises the round-10/11 single-CF lookup: we walk
652/// every named crypt filter under `/CF`, surfacing each filter's
653/// `/Recipients` (when present) plus its `/CFM` + `/Length` overrides.
654/// The document-level `/Recipients` slot is also surfaced where
655/// applicable (always for `s3` / `s4`; as a "compatibility-fallback"
656/// for `s5` when no per-CF list matched).
657///
658/// Returned candidates are walked in order — the first one whose
659/// recipient slot matches the user's certificate wins, mirroring ISO
660/// 32000-1 §7.6.4.2's first-match rule.
661fn collect_recipient_arrays(encrypt: &Dict) -> Result<Vec<RecipientCandidate>, PdfError> {
662    let lookup = |dict: &Dict, k: &str| {
663        dict.entries()
664            .iter()
665            .find(|(kk, _)| kk == k)
666            .map(|(_, v)| v.clone())
667    };
668    let sub = match lookup(encrypt, "SubFilter") {
669        Some(Object::Name(n)) => n,
670        _ => return Err(PdfError::other("PDF pubsec: /SubFilter required")),
671    };
672    let mut out: Vec<RecipientCandidate> = Vec::new();
673
674    if sub == "adbe.pkcs7.s5" {
675        // Walk every CF entry. Track the StmF candidate so it lands
676        // first (callers typically default to the StmF crypt filter).
677        let cf = match lookup(encrypt, "CF") {
678            Some(Object::Dict(d)) => d,
679            _ => return Err(PdfError::other("PDF pubsec: s5 requires /CF dictionary")),
680        };
681        let stmf_name: Option<String> = match lookup(encrypt, "StmF") {
682            Some(Object::Name(n)) => Some(n),
683            _ => None,
684        };
685        let mut entries = cf.entries().to_vec();
686        // Move the StmF entry to the front so its candidate is tried
687        // first — round-10 single-CF callers rely on the StmF being
688        // the "default" recipient set.
689        if let Some(name) = stmf_name.as_ref() {
690            if let Some(pos) = entries.iter().position(|(k, _)| k == name) {
691                let entry = entries.remove(pos);
692                entries.insert(0, entry);
693            }
694        }
695        for (name, entry) in &entries {
696            let Object::Dict(filter) = entry else {
697                continue;
698            };
699            let Some(recipients_obj) = lookup(filter, "Recipients") else {
700                continue;
701            };
702            let blobs = recipients_to_blobs(&recipients_obj)?;
703            if blobs.is_empty() {
704                continue;
705            }
706            let cfm = match lookup(filter, "CFM") {
707                Some(Object::Name(n)) => Some(n),
708                _ => None,
709            };
710            let length = match lookup(filter, "Length") {
711                Some(Object::Integer(n)) => Some(n as usize),
712                _ => None,
713            };
714            // Map CFM to (method, revision, default key length).
715            let method_revision = cfm.as_deref().and_then(|c| match c {
716                "V2" => Some((CryptMethod::Rc4, 4u8)),
717                "AESV2" => Some((CryptMethod::Aes128, 4u8)),
718                "AESV3" => Some((CryptMethod::Aes256, 6u8)),
719                _ => None,
720            });
721            let key_length_bits = match cfm.as_deref() {
722                Some("AESV2") => Some(128),
723                Some("AESV3") => Some(256),
724                _ => length.map(|len| {
725                    // /CF /Length is in bytes per Table 25 of ISO
726                    // 32000-1 (the dict-level /Length is in bits).
727                    len * 8
728                }),
729            };
730            out.push(RecipientCandidate {
731                cf_name: Some(name.clone()),
732                blobs,
733                key_length_bits,
734                method_revision,
735            });
736        }
737        // Compatibility fallback: top-level /Recipients (some s5
738        // writers — including round-11's own — emit it for legacy
739        // readers).
740        if let Some(top) = lookup(encrypt, "Recipients") {
741            let blobs = recipients_to_blobs(&top)?;
742            // Avoid duplicating a CF candidate's blobs.
743            let already = out.iter().any(|c| c.blobs == blobs);
744            if !already && !blobs.is_empty() {
745                out.push(RecipientCandidate {
746                    cf_name: None,
747                    blobs,
748                    key_length_bits: None,
749                    method_revision: None,
750                });
751            }
752        }
753    } else {
754        // s3 / s4 — document-level /Recipients only.
755        let top = lookup(encrypt, "Recipients").ok_or_else(|| {
756            PdfError::other("PDF pubsec: /Recipients missing for s3/s4 SubFilter")
757        })?;
758        let blobs = recipients_to_blobs(&top)?;
759        out.push(RecipientCandidate {
760            cf_name: None,
761            blobs,
762            key_length_bits: None,
763            method_revision: None,
764        });
765    }
766    Ok(out)
767}
768
769fn recipients_to_blobs(array: &Object) -> Result<Vec<Vec<u8>>, PdfError> {
770    match array {
771        Object::Array(items) => items
772            .iter()
773            .map(|item| match item {
774                Object::LiteralString(s) | Object::HexString(s) => Ok(s.clone()),
775                other => Err(PdfError::other(format!(
776                    "PDF pubsec: /Recipients element must be a string (got {other:?})"
777                ))),
778            })
779            .collect(),
780        // PDF 2.0 accepts a single string for per-stream recipients;
781        // surface as a one-element list.
782        Object::LiteralString(s) | Object::HexString(s) => Ok(vec![s.clone()]),
783        other => Err(PdfError::other(format!(
784            "PDF pubsec: /Recipients must be an array of strings (got {other:?})"
785        ))),
786    }
787}
788
789/// Find a recipient slot in `envelope` whose RecipientIdentifier
790/// matches `credential.cert`, derive the CEK (KTRI: RSA decrypt;
791/// KARI: ECDH + KDF + AES Key Wrap unwrap), and use it to decrypt the
792/// envelope's encrypted content. Returns the plaintext (the seed +
793/// permissions blob), or `None` if no recipient matched.
794///
795/// Two RecipientIdentifier forms are matched (RFC 5652 §6.2.1 + RFC
796/// 5280 §4.2.1.2):
797/// 1. **IssuerAndSerialNumber (CMS v0)** — byte-compare the recipient
798///    slot's `(issuer_der, serial)` against the user cert's same pair.
799/// 2. **SubjectKeyIdentifier (CMS v2)** — byte-compare the recipient
800///    slot's SKI octet string against `SHA-1(SPKI BIT STRING contents)`
801///    of the user cert (RFC 5280 §4.2.1.2 method 1).
802///
803/// Round 14: KARI variants (RFC 5652 §6.2.2 + RFC 5753 §7.1) are
804/// unwrapped when the credential carries an EC private scalar (see
805/// [`PubSecCredential::from_parsed_ec_p256`]). KARI envelopes whose
806/// scheme isn't `dhSinglePass-stdDH-sha256kdf-scheme` (P-256 +
807/// X9.63-SHA-256 KDF + AES-KW) are skipped silently — a future round
808/// extends this matcher with P-384 / P-521 / X25519.
809///
810/// Round 17: when the KARI envelope's `OriginatorIdentifierOrKey` is
811/// `IssuerAndSerial` or `SubjectKeyIdentifier`, the supplied optional
812/// `trust_store` is consulted to recover the originator's public point
813/// from the long-term cert. `None` keeps the round-14 behaviour
814/// (long-term-cert KARIs are skipped silently).
815fn try_unwrap(
816    envelope: &cms::EnvelopedData,
817    credential: &PubSecCredential,
818    trust_store: Option<&TrustStore>,
819) -> Result<Option<Vec<u8>>, PdfError> {
820    let our_issuer = &credential.cert.issuer_der;
821    let our_serial = &credential.cert.serial;
822    let our_ski = credential.cert.subject_key_identifier();
823
824    // Walk every RecipientInfo in declaration order — KTRI + KARI.
825    for variant in &envelope.all_recipients {
826        match variant {
827            cms::RecipientInfoVariant::KeyTrans(recipient) => {
828                let matched = match &recipient.rid {
829                    cms::RecipientId::IssuerAndSerial(ias) => {
830                        &ias.issuer_der == our_issuer && &ias.serial == our_serial
831                    }
832                    cms::RecipientId::SubjectKeyIdentifier(ski) => match &our_ski {
833                        Some(our) => ski == our,
834                        None => false,
835                    },
836                };
837                if !matched {
838                    continue;
839                }
840                let Some(rsa_key) = credential.private_key.as_ref() else {
841                    // No RSA key on this credential — can't open KTRI.
842                    continue;
843                };
844                let cek = rsa_key
845                    .decrypt(rsa::Pkcs1v15Encrypt, &recipient.encrypted_key)
846                    .map_err(|e| PdfError::other(format!("PDF pubsec: RSA decrypt failed: {e}")))?;
847                let plaintext = decrypt_envelope_content(
848                    &envelope.content_encryption,
849                    &cek,
850                    &envelope.encrypted_content,
851                )?;
852                return Ok(Some(plaintext));
853            }
854            cms::RecipientInfoVariant::KeyAgree(kari) => {
855                // KARI: round-14 P-256 + round-15 P-384 / X25519 +
856                // round-16 P-521 + RFC 8418 §2.2 HKDF-X25519 paths.
857                // Skip envelopes whose KEA OID names an unsupported
858                // KDF, or one not paired with the credential's curve.
859                let Some((curve, ec_scalar)) = credential.ec_private.as_ref() else {
860                    continue;
861                };
862                let Some(kdf) = kari::KariKdf::from_kea_oid(&kari.key_encryption_oid) else {
863                    continue;
864                };
865                if !kdf.is_valid_for(*curve) {
866                    continue;
867                }
868                let Some(slot) = kari::match_kari_slot(
869                    kari,
870                    our_issuer,
871                    our_serial,
872                    credential.cert.spki_pubkey_bits.as_deref(),
873                ) else {
874                    continue;
875                };
876                let recipient = kari::EcRecipient {
877                    curve: *curve,
878                    private_scalar: ec_scalar.clone(),
879                    public_point_sec1: credential.cert.spki_pubkey_bits.clone().unwrap_or_default(),
880                };
881                let cek = kari::unwrap_kari_with_trust_store(kari, slot, &recipient, trust_store)?;
882                let plaintext = decrypt_envelope_content(
883                    &envelope.content_encryption,
884                    &cek,
885                    &envelope.encrypted_content,
886                )?;
887                return Ok(Some(plaintext));
888            }
889        }
890    }
891    Ok(None)
892}
893
894fn decrypt_envelope_content(
895    alg: &cms::ContentEncryption,
896    cek: &[u8],
897    ciphertext: &[u8],
898) -> Result<Vec<u8>, PdfError> {
899    match alg {
900        cms::ContentEncryption::Rc4 => Ok(crate::decrypt::rc4(cek, ciphertext)),
901        cms::ContentEncryption::Aes128Cbc { iv } => {
902            if cek.len() != 16 {
903                return Err(PdfError::other(format!(
904                    "PDF pubsec: AES-128 CEK must be 16 bytes (got {})",
905                    cek.len()
906                )));
907            }
908            aes_cbc_decrypt::<aes::Aes128>(cek, iv, ciphertext)
909        }
910        cms::ContentEncryption::Aes256Cbc { iv } => {
911            if cek.len() != 32 {
912                return Err(PdfError::other(format!(
913                    "PDF pubsec: AES-256 CEK must be 32 bytes (got {})",
914                    cek.len()
915                )));
916            }
917            aes_cbc_decrypt::<aes::Aes256>(cek, iv, ciphertext)
918        }
919        // Round-17 read-only legacy CMS content encryption — RC2 / 3DES.
920        // PDF 2.0 deprecates both; we accept on decode only so legacy
921        // archives still open. Keying material lengths follow RFC 3370.
922        cms::ContentEncryption::Rc2Cbc {
923            effective_key_bits,
924            iv,
925        } => rc2_cbc_decrypt(cek, *effective_key_bits, iv, ciphertext),
926        cms::ContentEncryption::DesEde3Cbc { iv } => des_ede3_cbc_decrypt(cek, iv, ciphertext),
927    }
928}
929
930fn aes_cbc_decrypt<C>(key: &[u8], iv: &[u8; 16], ct: &[u8]) -> Result<Vec<u8>, PdfError>
931where
932    C: aes::cipher::BlockCipher
933        + aes::cipher::BlockEncrypt
934        + aes::cipher::BlockDecrypt
935        + aes::cipher::KeyInit
936        + aes::cipher::BlockSizeUser<BlockSize = aes::cipher::consts::U16>,
937{
938    use aes::cipher::{BlockDecryptMut, KeyIvInit};
939    type Dec<C> = cbc::Decryptor<C>;
940    if ct.len() % 16 != 0 {
941        return Err(PdfError::other(format!(
942            "PDF pubsec: AES-CBC ciphertext {} not block-aligned",
943            ct.len()
944        )));
945    }
946    let dec = <Dec<C> as KeyIvInit>::new_from_slices(key, iv)
947        .map_err(|e| PdfError::other(format!("PDF pubsec: AES init failed: {e}")))?;
948    let mut buf = ct.to_vec();
949    let pt = dec
950        .decrypt_padded_mut::<aes::cipher::block_padding::Pkcs7>(&mut buf)
951        .map_err(|e| PdfError::other(format!("PDF pubsec: AES-CBC unpad: {e:?}")))?;
952    Ok(pt.to_vec())
953}
954
955/// Round-17 read-only: decrypt an RC2-CBC envelope content. RC2 is a
956/// 64-bit block cipher (RFC 2268) — the IV is 8 bytes, blocks are
957/// 8 bytes, padding is PKCS#7. The CEK length is the raw key length;
958/// `effective_key_bits` is the RC2 effective-key parameter from RFC 2268
959/// §6 (configured independently of the raw key length per RFC 3370 §5.1).
960///
961/// PDF 2.0 deprecates RC2 entirely; this path exists to open legacy
962/// archives only. No encode-side support.
963fn rc2_cbc_decrypt(
964    cek: &[u8],
965    effective_key_bits: u32,
966    iv: &[u8; 8],
967    ct: &[u8],
968) -> Result<Vec<u8>, PdfError> {
969    use cbc::cipher::{BlockDecryptMut, InnerIvInit};
970    use rc2::Rc2;
971    if ct.len() % 8 != 0 {
972        return Err(PdfError::other(format!(
973            "PDF pubsec: RC2-CBC ciphertext {} not block-aligned (8-byte blocks)",
974            ct.len()
975        )));
976    }
977    if cek.is_empty() || cek.len() > 128 {
978        return Err(PdfError::other(format!(
979            "PDF pubsec: RC2 CEK length {} out of RFC 2268 range (1..=128 bytes)",
980            cek.len()
981        )));
982    }
983    // `rc2`'s public `KeyInit::new_from_slice` always sets eff_key_len =
984    // 8 * key.len(). To honour RFC 3370's separate effective-key
985    // parameter we construct the cipher via `new_with_eff_key_len` and
986    // wrap it into a CBC decryptor through `InnerIvInit`.
987    let cipher = Rc2::new_with_eff_key_len(cek, effective_key_bits as usize);
988    let dec = cbc::Decryptor::<Rc2>::inner_iv_slice_init(cipher, iv)
989        .map_err(|e| PdfError::other(format!("PDF pubsec: RC2-CBC IV init failed: {e}")))?;
990    let mut buf = ct.to_vec();
991    let pt = dec
992        .decrypt_padded_mut::<cbc::cipher::block_padding::Pkcs7>(&mut buf)
993        .map_err(|e| PdfError::other(format!("PDF pubsec: RC2-CBC unpad: {e:?}")))?;
994    Ok(pt.to_vec())
995}
996
997/// Round-17 read-only: decrypt a 3DES-CBC (DES-EDE3-CBC) envelope
998/// content. The CEK is the 24-byte concatenation of the three single-DES
999/// keys (RFC 3370 §5.2 / RFC 5652 §12.4); the IV is 8 bytes; blocks are
1000/// 8 bytes; padding is PKCS#7.
1001///
1002/// PDF 2.0 deprecates 3DES; this path exists to open legacy archives
1003/// only. No encode-side support.
1004fn des_ede3_cbc_decrypt(cek: &[u8], iv: &[u8; 8], ct: &[u8]) -> Result<Vec<u8>, PdfError> {
1005    use cbc::cipher::{BlockDecryptMut, KeyIvInit};
1006    use des::TdesEde3;
1007    if ct.len() % 8 != 0 {
1008        return Err(PdfError::other(format!(
1009            "PDF pubsec: 3DES-CBC ciphertext {} not block-aligned (8-byte blocks)",
1010            ct.len()
1011        )));
1012    }
1013    if cek.len() != 24 {
1014        return Err(PdfError::other(format!(
1015            "PDF pubsec: 3DES (TdesEde3) CEK must be 24 bytes (got {})",
1016            cek.len()
1017        )));
1018    }
1019    type Dec = cbc::Decryptor<TdesEde3>;
1020    let dec = <Dec as KeyIvInit>::new_from_slices(cek, iv)
1021        .map_err(|e| PdfError::other(format!("PDF pubsec: 3DES-CBC init failed: {e}")))?;
1022    let mut buf = ct.to_vec();
1023    let pt = dec
1024        .decrypt_padded_mut::<cbc::cipher::block_padding::Pkcs7>(&mut buf)
1025        .map_err(|e| PdfError::other(format!("PDF pubsec: 3DES-CBC unpad: {e:?}")))?;
1026    Ok(pt.to_vec())
1027}
1028
1029/// Derive the file encryption key per ISO 32000-1 §7.6.4.3 / ISO
1030/// 32000-2 §7.6.5.3. Hash is SHA-1 for V≤4 paths and SHA-256 for the
1031/// V=5 (AES-256) path.
1032fn derive_file_key(
1033    sub: PubSecSubFilter,
1034    seed: &[u8],
1035    recipients_blobs: &[Vec<u8>],
1036    encrypt_metadata: bool,
1037    key_length_bits: usize,
1038) -> Vec<u8> {
1039    let n = key_length_bits / 8;
1040    let mut input =
1041        Vec::with_capacity(20 + recipients_blobs.iter().map(|v| v.len()).sum::<usize>() + 4);
1042    input.extend_from_slice(seed);
1043    for blob in recipients_blobs {
1044        input.extend_from_slice(blob);
1045    }
1046    if !encrypt_metadata {
1047        input.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0xFF]);
1048    }
1049    let digest: Vec<u8> = match sub {
1050        PubSecSubFilter::Pkcs7S5V5 => {
1051            use sha2::Digest;
1052            sha2::Sha256::digest(&input).to_vec()
1053        }
1054        _ => {
1055            use sha1::Digest;
1056            sha1::Sha1::digest(&input).to_vec()
1057        }
1058    };
1059    digest[..n.min(digest.len())].to_vec()
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::cms_build::{
1065        build_envelope_aes128, build_envelope_aes256, build_envelope_rc4, rsa_pkcs1_encrypt,
1066        RecipientPlain,
1067    };
1068    use super::*;
1069    use crate::objects::Dict;
1070
1071    fn rsa_keypair() -> (rsa::RsaPrivateKey, rsa::RsaPublicKey) {
1072        let mut rng = rsa::rand_core::OsRng;
1073        let priv_key = rsa::RsaPrivateKey::new(&mut rng, 2048).expect("RSA keypair");
1074        let pub_key = rsa::RsaPublicKey::from(&priv_key);
1075        (priv_key, pub_key)
1076    }
1077
1078    fn fake_cert(issuer: &[u8], serial: &[u8]) -> super::x509::Certificate {
1079        super::x509::Certificate {
1080            issuer_der: issuer.to_vec(),
1081            serial: serial.to_vec(),
1082            spki_pubkey_bits: None,
1083            validity: None,
1084            spki_algorithm_oid: None,
1085            spki_algorithm_params: None,
1086        }
1087    }
1088
1089    fn make_encrypt_dict(sub_filter: &str, v: i64, recipients: &[Vec<u8>]) -> Dict {
1090        let mut d = Dict::default();
1091        d.set("Filter", Object::Name("Adobe.PPKLite".into()));
1092        d.set("SubFilter", Object::Name(sub_filter.into()));
1093        d.set("V", Object::Integer(v));
1094        d.set("P", Object::Integer(-4));
1095        let arr = recipients
1096            .iter()
1097            .map(|r| Object::LiteralString(r.clone()))
1098            .collect();
1099        d.set("Recipients", Object::Array(arr));
1100        d
1101    }
1102
1103    #[test]
1104    fn s4_open_round_trip() {
1105        // adbe.pkcs7.s4 → RC4-128, SHA-1 hash, V=2.
1106        let (priv_key, pub_key) = rsa_keypair();
1107        let issuer_der = super::der::write_sequence(b"O=Test");
1108        let serial = vec![0x01, 0x42];
1109        // CEK is the AES-128 key — for the s4 RC4 envelope we use a
1110        // 128-bit RC4 key; ISO accepts up to 256 bits.
1111        let cek = [0x66u8; 16];
1112        // Plaintext = 20-byte seed + 4-byte permissions LE.
1113        let mut plaintext = vec![0u8; 24];
1114        plaintext[..20].copy_from_slice(&[0xAB; 20]);
1115        plaintext[20..24].copy_from_slice(&((-4i32) as u32).to_le_bytes());
1116        let encrypted_key = rsa_pkcs1_encrypt(&pub_key, &cek).unwrap();
1117        let envelope_der = build_envelope_rc4(
1118            &[RecipientPlain::ias(
1119                issuer_der.clone(),
1120                serial.clone(),
1121                encrypted_key,
1122            )],
1123            &plaintext,
1124            &cek,
1125        );
1126        let credential = PubSecCredential::from_parsed(fake_cert(&issuer_der, &serial), priv_key);
1127        let encrypt = make_encrypt_dict("adbe.pkcs7.s4", 2, &[envelope_der]);
1128        let handler = open_with_certificate(&encrypt, &credential)
1129            .expect("open ok")
1130            .expect("matched recipient");
1131        assert_eq!(handler.method, CryptMethod::Rc4);
1132        assert_eq!(handler.revision, 3);
1133        assert_eq!(handler.key.len(), 16);
1134    }
1135
1136    #[test]
1137    fn s5_v5_aes256_round_trip() {
1138        // adbe.pkcs7.s5 V=5 → AES-256, SHA-256 hash.
1139        let (priv_key, pub_key) = rsa_keypair();
1140        let issuer_der = super::der::write_sequence(b"O=Test");
1141        let serial = vec![0x42, 0x01, 0x00];
1142        let cek = [0xC1u8; 32];
1143        let iv = [0xCAu8; 16];
1144        // Plaintext = 20-byte seed + 4-byte permissions MSB.
1145        let mut plaintext = vec![0u8; 24];
1146        plaintext[..20].copy_from_slice(&[0xCD; 20]);
1147        plaintext[20..24].copy_from_slice(&[0xFF, 0xFF, 0xFF, 0xFC]);
1148        let encrypted_key = rsa_pkcs1_encrypt(&pub_key, &cek).unwrap();
1149        let envelope_der = build_envelope_aes256(
1150            &[RecipientPlain::ias(
1151                issuer_der.clone(),
1152                serial.clone(),
1153                encrypted_key,
1154            )],
1155            &plaintext,
1156            &cek,
1157            &iv,
1158        );
1159        // s5 uses /CF /<StmF> /Recipients (Table 27). Build that
1160        // dictionary structure.
1161        let mut filter = Dict::default();
1162        filter.set("CFM", Object::Name("AESV3".into()));
1163        filter.set(
1164            "Recipients",
1165            Object::Array(vec![Object::LiteralString(envelope_der)]),
1166        );
1167        filter.set("Length", Object::Integer(32));
1168        let mut cf = Dict::default();
1169        cf.set("DefaultCryptFilter", Object::Dict(filter));
1170        let mut encrypt = Dict::default();
1171        encrypt.set("Filter", Object::Name("Adobe.PPKLite".into()));
1172        encrypt.set("SubFilter", Object::Name("adbe.pkcs7.s5".into()));
1173        encrypt.set("V", Object::Integer(5));
1174        encrypt.set("P", Object::Integer(-4));
1175        encrypt.set("StmF", Object::Name("DefaultCryptFilter".into()));
1176        encrypt.set("StrF", Object::Name("DefaultCryptFilter".into()));
1177        encrypt.set("CF", Object::Dict(cf));
1178
1179        let credential = PubSecCredential::from_parsed(fake_cert(&issuer_der, &serial), priv_key);
1180        let handler = open_with_certificate(&encrypt, &credential)
1181            .expect("open ok")
1182            .expect("matched recipient");
1183        assert_eq!(handler.method, CryptMethod::Aes256);
1184        assert_eq!(handler.revision, 6);
1185        assert_eq!(handler.key.len(), 32);
1186    }
1187
1188    #[test]
1189    fn open_returns_none_when_cert_does_not_match() {
1190        let (priv_key, pub_key) = rsa_keypair();
1191        let issuer_der = super::der::write_sequence(b"O=Other");
1192        let cek = [0u8; 32];
1193        let iv = [0u8; 16];
1194        let plaintext = vec![0xAA; 24];
1195        let encrypted_key = rsa_pkcs1_encrypt(&pub_key, &cek).unwrap();
1196        let envelope_der = build_envelope_aes256(
1197            &[RecipientPlain::ias(
1198                issuer_der.clone(),
1199                vec![0x01],
1200                encrypted_key,
1201            )],
1202            &plaintext,
1203            &cek,
1204            &iv,
1205        );
1206        // Caller's cert has a different serial — no match.
1207        let mut filter = Dict::default();
1208        filter.set("CFM", Object::Name("AESV3".into()));
1209        filter.set(
1210            "Recipients",
1211            Object::Array(vec![Object::LiteralString(envelope_der)]),
1212        );
1213        let mut cf = Dict::default();
1214        cf.set("F", Object::Dict(filter));
1215        let mut encrypt = Dict::default();
1216        encrypt.set("Filter", Object::Name("Adobe.PPKLite".into()));
1217        encrypt.set("SubFilter", Object::Name("adbe.pkcs7.s5".into()));
1218        encrypt.set("V", Object::Integer(5));
1219        encrypt.set("P", Object::Integer(-4));
1220        encrypt.set("StmF", Object::Name("F".into()));
1221        encrypt.set("StrF", Object::Name("F".into()));
1222        encrypt.set("CF", Object::Dict(cf));
1223        let credential = PubSecCredential::from_parsed(
1224            fake_cert(&issuer_der, &[0x99]), // different serial
1225            priv_key,
1226        );
1227        let handler = open_with_certificate(&encrypt, &credential).unwrap();
1228        assert!(handler.is_none(), "unexpected match: {handler:?}");
1229    }
1230
1231    #[test]
1232    fn s5_v4_aes128_round_trip() {
1233        // adbe.pkcs7.s5 V=4 + AESV2 → AES-128, SHA-1 hash.
1234        let (priv_key, pub_key) = rsa_keypair();
1235        let issuer_der = super::der::write_sequence(b"O=v4test");
1236        let serial = vec![0x05];
1237        let cek = [0x77u8; 16];
1238        let iv = [0x88u8; 16];
1239        let plaintext = vec![0u8; 24];
1240        let encrypted_key = rsa_pkcs1_encrypt(&pub_key, &cek).unwrap();
1241        let envelope_der = build_envelope_aes128(
1242            &[RecipientPlain::ias(
1243                issuer_der.clone(),
1244                serial.clone(),
1245                encrypted_key,
1246            )],
1247            &plaintext,
1248            &cek,
1249            &iv,
1250        );
1251        let mut filter = Dict::default();
1252        filter.set("CFM", Object::Name("AESV2".into()));
1253        filter.set(
1254            "Recipients",
1255            Object::Array(vec![Object::LiteralString(envelope_der)]),
1256        );
1257        let mut cf = Dict::default();
1258        cf.set("DefaultCryptFilter", Object::Dict(filter));
1259        let mut encrypt = Dict::default();
1260        encrypt.set("Filter", Object::Name("Adobe.PPKLite".into()));
1261        encrypt.set("SubFilter", Object::Name("adbe.pkcs7.s5".into()));
1262        encrypt.set("V", Object::Integer(4));
1263        encrypt.set("P", Object::Integer(-4));
1264        encrypt.set("StmF", Object::Name("DefaultCryptFilter".into()));
1265        encrypt.set("StrF", Object::Name("DefaultCryptFilter".into()));
1266        encrypt.set("CF", Object::Dict(cf));
1267        let credential = PubSecCredential::from_parsed(fake_cert(&issuer_der, &serial), priv_key);
1268        let handler = open_with_certificate(&encrypt, &credential)
1269            .expect("open ok")
1270            .expect("matched recipient");
1271        assert_eq!(handler.method, CryptMethod::Aes128);
1272        assert_eq!(handler.revision, 4);
1273        assert_eq!(handler.key.len(), 16);
1274    }
1275}