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}