Skip to main content

oxideav_pdf/pubsec/
signed_data.rs

1//! CMS `SignedData` (RFC 5652 §5) parser scaffolding for PDF
2//! digital-signature payloads (ISO 32000-1 §12.8 + ETSI EN 319 142
3//! PAdES profiles).
4//!
5//! Round-19 ships the parser + typed accessors only — signature
6//! verification (hash-then-RSA / ECDSA dispatch per
7//! `digestAlgorithm` + `signatureAlgorithm`) is deferred to a follow-up
8//! round. Today's surface lets callers:
9//!
10//! * Pull a PDF signature blob (typically the bytes between the
11//!   `Contents <` and `>` of a `/Sig` annotation, hex-decoded) into a
12//!   structurally-parsed [`SignedData`] value.
13//! * Inspect the embedded certificate set (`certs[]`), the per-signer
14//!   identifier (IAS / SKI), and the signed/unsigned attribute lists.
15//! * Recover the encapsulated content body when present (detached
16//!   signatures — by far the most common shape in PAdES — leave
17//!   `encap_content_octets` empty; the bytes to verify are then the
18//!   PDF byte ranges named in the `/ByteRange` array).
19//!
20//! ```asn.1
21//! SignedData ::= SEQUENCE {
22//!   version              CMSVersion,
23//!   digestAlgorithms     SET OF DigestAlgorithmIdentifier,
24//!   encapContentInfo     EncapsulatedContentInfo,
25//!   certificates     [0] IMPLICIT CertificateSet OPTIONAL,
26//!   crls             [1] IMPLICIT RevocationInfoChoices OPTIONAL,
27//!   signerInfos          SET OF SignerInfo
28//! }
29//!
30//! EncapsulatedContentInfo ::= SEQUENCE {
31//!   eContentType   OBJECT IDENTIFIER,
32//!   eContent   [0] EXPLICIT OCTET STRING OPTIONAL
33//! }
34//!
35//! SignerInfo ::= SEQUENCE {
36//!   version              CMSVersion,
37//!   sid                  SignerIdentifier,
38//!   digestAlgorithm      DigestAlgorithmIdentifier,
39//!   signedAttrs      [0] IMPLICIT SignedAttributes OPTIONAL,
40//!   signatureAlgorithm   SignatureAlgorithmIdentifier,
41//!   signature            SignatureValue,
42//!   unsignedAttrs    [1] IMPLICIT UnsignedAttributes OPTIONAL
43//! }
44//! ```
45//!
46//! Provenance: RFC 5652 §5 (CMS SignedData) + RFC 5126 §5 (CAdES
47//! signed-attributes layout) + RFC 5280 §4 (CertificateChoices) +
48//! ISO 32000-1 §12.8.3.3 (PDF signature handler interaction).
49
50use crate::error::PdfError;
51
52use super::cms::{IssuerAndSerial, OID_SIGNED_DATA};
53use super::der::{
54    maybe_read_context, read_context, read_expected, read_integer_bytes, read_integer_u64,
55    read_octet_string, read_oid, read_sequence, read_set, read_tlv, tag, Class,
56};
57
58/// `SignerIdentifier` (RFC 5652 §5.3) — the CHOICE that picks between
59/// `IssuerAndSerialNumber` (CMS v1) and `[0] SubjectKeyIdentifier`
60/// (CMS v3). Same shape as the EnvelopedData KTRI's
61/// [`super::cms::RecipientId`] but distinguished by name to keep the
62/// type contracts on each side independent.
63#[derive(Debug, Clone)]
64pub enum SignerIdentifier {
65    /// `IssuerAndSerialNumber` — CMS SignerInfo v1.
66    IssuerAndSerial(IssuerAndSerial),
67    /// `[0] SubjectKeyIdentifier` — CMS SignerInfo v3. The 20-byte
68    /// SHA-1 of the signer cert's `SubjectPublicKeyInfo` BIT STRING
69    /// contents (RFC 5280 §4.2.1.2 method 1).
70    SubjectKeyIdentifier(Vec<u8>),
71}
72
73/// One `Attribute` (RFC 5652 §5.3) — `(attr_type, attr_values)`. The
74/// values are surfaced as raw DER bytes so a caller can re-parse per
75/// the OID's contract without the scaffolding having to know every
76/// signed-attribute schema in CAdES / PAdES / RFC 9216.
77#[derive(Debug, Clone)]
78pub struct Attribute {
79    /// `attrType` OID arcs.
80    pub oid: Vec<u64>,
81    /// Raw DER bytes of each `attrValues` SET element. Each entry is
82    /// the bytes of one full TLV (tag + length + body), so the caller
83    /// can re-parse with [`super::der::read_tlv`].
84    pub values: Vec<Vec<u8>>,
85}
86
87/// One `SignerInfo` slot inside the `SignedData.signerInfos` SET.
88///
89/// Round-19 keeps the surface inspection-only — the bytes that would
90/// feed signature verification are all surfaced (digest_algorithm OID,
91/// signature_algorithm OID, raw signature octet string), but no verify
92/// helper is implemented this round.
93#[derive(Debug, Clone)]
94pub struct SignerInfo {
95    /// CMS SignerInfo version — 1 (IAS) or 3 (SKI).
96    pub version: u64,
97    /// Signer identifier — IAS (v1) or SKI (v3).
98    pub sid: SignerIdentifier,
99    /// `digestAlgorithm` OID arcs (e.g. SHA-256 = 2.16.840.1.101.3.4.2.1).
100    pub digest_algorithm_oid: Vec<u64>,
101    /// `digestAlgorithm` raw parameter bytes (typically a NULL or
102    /// absent — we surface what was on the wire so a caller can
103    /// distinguish encodings).
104    pub digest_algorithm_params: Vec<u8>,
105    /// OPTIONAL signed attributes (`[0] IMPLICIT SET OF Attribute`).
106    /// Per RFC 5652 §5.3, when present the signature is computed over
107    /// the DER encoding of the SignedAttributes SET (with universal
108    /// SET tag, NOT the implicit `[0]` tag — RFC 5652 §5.4).
109    /// Empty vec means "absent".
110    pub signed_attrs: Vec<Attribute>,
111    /// **Round-19 verification helper** — when `signed_attrs` was
112    /// present on the wire, this carries the raw DER body of the
113    /// `[0] IMPLICIT` SET. The verifier needs to re-encode this with
114    /// the universal SET tag (0x31) before hashing per RFC 5652 §5.4
115    /// — to make that mechanical, we store the body bytes here and
116    /// the re-tagging happens in the verify dispatch (deferred).
117    /// `None` when `signed_attrs` was absent.
118    pub signed_attrs_der: Option<Vec<u8>>,
119    /// `signatureAlgorithm` OID arcs (e.g. RSAES-PKCS1-v1.5 =
120    /// 1.2.840.113549.1.1.1, ECDSA-with-SHA256 = 1.2.840.10045.4.3.2).
121    pub signature_algorithm_oid: Vec<u64>,
122    /// `signatureAlgorithm` raw parameter bytes.
123    pub signature_algorithm_params: Vec<u8>,
124    /// `signature` OCTET STRING — the actual signature octets the
125    /// verifier checks against the digest of the signed bytes.
126    pub signature: Vec<u8>,
127    /// OPTIONAL unsigned attributes (`[1] IMPLICIT SET OF Attribute`).
128    pub unsigned_attrs: Vec<Attribute>,
129}
130
131/// Parsed CMS `SignedData` reduced to the fields a PDF signature
132/// reader needs.
133#[derive(Debug, Clone)]
134pub struct SignedData {
135    /// CMS SignedData version — 1, 3, 4, or 5 (RFC 5652 §5.1).
136    pub version: u64,
137    /// `digestAlgorithms` SET — each entry is (oid_arcs, params_raw_bytes).
138    pub digest_algorithms: Vec<(Vec<u64>, Vec<u8>)>,
139    /// `encapContentInfo.eContentType` OID arcs. For attached PDF
140    /// signatures this is `id-data` (1.2.840.113549.1.7.1); for
141    /// detached PAdES signatures it's still `id-data` but the
142    /// `eContent` octets are absent — the bytes to verify are the
143    /// PDF byte ranges in `/ByteRange`.
144    pub encap_content_type: Vec<u64>,
145    /// `encapContentInfo.eContent` octets — `Some(bytes)` for
146    /// attached signatures, `None` when omitted (typical PAdES /
147    /// detached). The bytes here are the OCTET STRING body — no DER
148    /// header, no `[0]` wrapper.
149    pub encap_content_octets: Option<Vec<u8>>,
150    /// `certificates[0] IMPLICIT CertificateSet OPTIONAL` — each
151    /// entry is the raw DER bytes of one `CertificateChoices`
152    /// alternative (typically an X.509 v3 SEQUENCE; we surface every
153    /// alternative shape as opaque DER so the caller can dispatch on
154    /// the outer tag). Empty vec when the field was absent.
155    pub certs: Vec<Vec<u8>>,
156    /// `crls[1] IMPLICIT RevocationInfoChoices OPTIONAL` — each
157    /// entry is the raw DER bytes of one `RevocationInfoChoices`
158    /// alternative (typically an X.509 `CertificateList` SEQUENCE).
159    /// Empty vec when the field was absent.
160    pub crls: Vec<Vec<u8>>,
161    /// `signerInfos SET OF SignerInfo` — typically one entry for a
162    /// single-signer PDF, but the spec permits multiple.
163    pub signer_infos: Vec<SignerInfo>,
164}
165
166/// Parse a CMS `ContentInfo` whose `contentType` is `id-signedData`
167/// (`1.2.840.113549.1.7.2`), returning the inner [`SignedData`].
168///
169/// Round-19 entry point — symmetric to
170/// [`super::cms::parse_envelope`] for the EnvelopedData side.
171pub fn parse_signed_data(data: &[u8]) -> Result<SignedData, PdfError> {
172    let (body, rest) = read_sequence(data)?;
173    if !rest.is_empty() {
174        return Err(PdfError::other(
175            "CMS SignedData: trailing bytes after ContentInfo SEQUENCE",
176        ));
177    }
178    let (oid, rest) = read_oid(body)?;
179    if oid != OID_SIGNED_DATA {
180        return Err(PdfError::other(format!(
181            "CMS SignedData: ContentInfo contentType must be id-signedData (got {oid:?})"
182        )));
183    }
184    let (content, rest) = read_context(rest, 0)?;
185    if !rest.is_empty() {
186        return Err(PdfError::other(
187            "CMS SignedData: trailing bytes after [0] EXPLICIT content",
188        ));
189    }
190    parse_signed_data_inner(content)
191}
192
193/// Parse a bare `SignedData` SEQUENCE (no surrounding ContentInfo).
194pub fn parse_signed_data_inner(data: &[u8]) -> Result<SignedData, PdfError> {
195    let (body, rest) = read_sequence(data)?;
196    if !rest.is_empty() {
197        return Err(PdfError::other(
198            "CMS SignedData: trailing bytes after SignedData SEQUENCE",
199        ));
200    }
201    let (version, body) = read_integer_u64(body)?;
202    if version > 5 {
203        return Err(PdfError::other(format!(
204            "CMS SignedData: unsupported version {version}"
205        )));
206    }
207    // digestAlgorithms SET OF AlgorithmIdentifier.
208    let (da_set, body) = read_set(body)?;
209    let mut digest_algorithms: Vec<(Vec<u64>, Vec<u8>)> = Vec::new();
210    let mut cursor = da_set;
211    while !cursor.is_empty() {
212        let (alg_seq, after) = read_sequence(cursor)?;
213        let (alg_oid, alg_params) = read_oid(alg_seq)?;
214        digest_algorithms.push((alg_oid, alg_params.to_vec()));
215        cursor = after;
216    }
217
218    // EncapsulatedContentInfo ::= SEQUENCE {
219    //   eContentType OBJECT IDENTIFIER,
220    //   eContent [0] EXPLICIT OCTET STRING OPTIONAL
221    // }
222    let (eci, body) = read_sequence(body)?;
223    let (eci_oid, eci_rest) = read_oid(eci)?;
224    let (eci_econtent_opt, eci_rest) = maybe_read_context(eci_rest, 0)?;
225    if !eci_rest.is_empty() {
226        return Err(PdfError::other(
227            "CMS SignedData: trailing bytes after EncapsulatedContentInfo",
228        ));
229    }
230    let encap_content_octets = match eci_econtent_opt {
231        Some(b) => {
232            // `[0] EXPLICIT OCTET STRING` — body of the [0] wrapper is
233            // a universal OCTET STRING TLV. Some legacy PAdES emit the
234            // body as the raw octets directly (without the inner OCTET
235            // STRING wrapper); we accept either form.
236            if let Ok((tlv, rest_inner)) = read_tlv(b) {
237                if rest_inner.is_empty()
238                    && tlv.class == Class::Universal
239                    && tlv.tag_number == tag::OCTET_STRING
240                {
241                    Some(tlv.body.to_vec())
242                } else {
243                    Some(b.to_vec())
244                }
245            } else {
246                Some(b.to_vec())
247            }
248        }
249        None => None,
250    };
251
252    // certificates [0] IMPLICIT CertificateSet OPTIONAL
253    let mut cursor = body;
254    let mut certs: Vec<Vec<u8>> = Vec::new();
255    if !cursor.is_empty() {
256        let (peek, _) = read_tlv(cursor)?;
257        if peek.class == Class::ContextSpecific && peek.tag_number == 0 {
258            let (set_body, after) = read_tlv(cursor)?;
259            certs = split_set_into_raw_entries(set_body.body)?;
260            cursor = after;
261        }
262    }
263    // crls [1] IMPLICIT RevocationInfoChoices OPTIONAL
264    let mut crls: Vec<Vec<u8>> = Vec::new();
265    if !cursor.is_empty() {
266        let (peek, _) = read_tlv(cursor)?;
267        if peek.class == Class::ContextSpecific && peek.tag_number == 1 {
268            let (set_body, after) = read_tlv(cursor)?;
269            crls = split_set_into_raw_entries(set_body.body)?;
270            cursor = after;
271        }
272    }
273
274    // signerInfos SET OF SignerInfo.
275    let (si_set, after_si) = read_set(cursor)?;
276    if !after_si.is_empty() {
277        return Err(PdfError::other(
278            "CMS SignedData: trailing bytes after signerInfos SET",
279        ));
280    }
281    let mut signer_infos: Vec<SignerInfo> = Vec::new();
282    let mut si_cursor = si_set;
283    while !si_cursor.is_empty() {
284        let (info, tail) = parse_signer_info(si_cursor)?;
285        signer_infos.push(info);
286        si_cursor = tail;
287    }
288    if signer_infos.is_empty() {
289        return Err(PdfError::other(
290            "CMS SignedData: signerInfos SET must contain at least one SignerInfo",
291        ));
292    }
293
294    Ok(SignedData {
295        version,
296        digest_algorithms,
297        encap_content_type: eci_oid,
298        encap_content_octets,
299        certs,
300        crls,
301        signer_infos,
302    })
303}
304
305/// Parse one `SignerInfo` SEQUENCE per RFC 5652 §5.3.
306fn parse_signer_info(data: &[u8]) -> Result<(SignerInfo, &[u8]), PdfError> {
307    let (body, tail) = read_sequence(data)?;
308    let (version, body) = read_integer_u64(body)?;
309    if version != 1 && version != 3 {
310        return Err(PdfError::other(format!(
311            "CMS SignedData: unsupported SignerInfo version {version} (expected 1 or 3)"
312        )));
313    }
314    // SignerIdentifier — same CHOICE shape as RecipientIdentifier.
315    let (sid, body) = if version == 1 {
316        let (ias_body, rest) = read_sequence(body)?;
317        let (issuer_tlv, ias_after_issuer) = read_tlv(ias_body)?;
318        if issuer_tlv.class != Class::Universal || issuer_tlv.tag_number != tag::SEQUENCE {
319            return Err(PdfError::other(
320                "CMS SignedData: SignerInfo IAS issuer must be a SEQUENCE",
321            ));
322        }
323        let issuer_total = ias_body.len() - ias_after_issuer.len();
324        let issuer_der = ias_body[..issuer_total].to_vec();
325        let (serial_body, _) = read_integer_bytes(ias_after_issuer)?;
326        (
327            SignerIdentifier::IssuerAndSerial(IssuerAndSerial {
328                issuer_der,
329                serial: serial_body.to_vec(),
330            }),
331            rest,
332        )
333    } else {
334        // [0] IMPLICIT OCTET STRING (SubjectKeyIdentifier).
335        let (tlv, rest) = read_tlv(body)?;
336        if tlv.class != Class::ContextSpecific || tlv.tag_number != 0 {
337            return Err(PdfError::other(format!(
338                "CMS SignedData: SignerInfo[v=3] expects [0] SubjectKeyIdentifier (got class={:?} tag={})",
339                tlv.class, tlv.tag_number
340            )));
341        }
342        if tlv.constructed {
343            return Err(PdfError::other(
344                "CMS SignedData: SignerInfo SKI must be primitive [0] IMPLICIT OCTET STRING",
345            ));
346        }
347        (
348            SignerIdentifier::SubjectKeyIdentifier(tlv.body.to_vec()),
349            rest,
350        )
351    };
352
353    // digestAlgorithm AlgorithmIdentifier
354    let (da_seq, body) = read_sequence(body)?;
355    let (da_oid, da_params) = read_oid(da_seq)?;
356    let digest_algorithm_oid = da_oid;
357    let digest_algorithm_params = da_params.to_vec();
358
359    // [0] IMPLICIT signedAttrs SET OF Attribute OPTIONAL.
360    let mut cursor = body;
361    let mut signed_attrs: Vec<Attribute> = Vec::new();
362    let mut signed_attrs_der: Option<Vec<u8>> = None;
363    if !cursor.is_empty() {
364        let (peek, _) = read_tlv(cursor)?;
365        if peek.class == Class::ContextSpecific && peek.tag_number == 0 {
366            let (sa_tlv, after) = read_tlv(cursor)?;
367            signed_attrs_der = Some(sa_tlv.body.to_vec());
368            signed_attrs = split_attributes(sa_tlv.body)?;
369            cursor = after;
370        }
371    }
372
373    // signatureAlgorithm AlgorithmIdentifier
374    let (sa_seq, body) = read_sequence(cursor)?;
375    let (sa_oid, sa_params) = read_oid(sa_seq)?;
376    let signature_algorithm_oid = sa_oid;
377    let signature_algorithm_params = sa_params.to_vec();
378
379    // signature OCTET STRING
380    let (sig_bytes, body) = read_octet_string(body)?;
381    let signature = sig_bytes.to_vec();
382
383    // [1] IMPLICIT unsignedAttrs SET OF Attribute OPTIONAL
384    let mut cursor = body;
385    let mut unsigned_attrs: Vec<Attribute> = Vec::new();
386    if !cursor.is_empty() {
387        let (peek, _) = read_tlv(cursor)?;
388        if peek.class == Class::ContextSpecific && peek.tag_number == 1 {
389            let (ua_tlv, after) = read_tlv(cursor)?;
390            unsigned_attrs = split_attributes(ua_tlv.body)?;
391            cursor = after;
392        }
393    }
394    if !cursor.is_empty() {
395        return Err(PdfError::other(
396            "CMS SignedData: trailing bytes after SignerInfo body",
397        ));
398    }
399
400    Ok((
401        SignerInfo {
402            version,
403            sid,
404            digest_algorithm_oid,
405            digest_algorithm_params,
406            signed_attrs,
407            signed_attrs_der,
408            signature_algorithm_oid,
409            signature_algorithm_params,
410            signature,
411            unsigned_attrs,
412        },
413        tail,
414    ))
415}
416
417/// Decompose a SET-of-Attribute body into a Vec of typed attributes.
418fn split_attributes(body: &[u8]) -> Result<Vec<Attribute>, PdfError> {
419    let mut out = Vec::new();
420    let mut cursor = body;
421    while !cursor.is_empty() {
422        let (attr_seq, after) = read_sequence(cursor)?;
423        let (oid, attr_rest) = read_oid(attr_seq)?;
424        // attrValues SET OF AttributeValue.
425        let (set_tlv, after_set) = read_expected(attr_rest, Class::Universal, tag::SET)?;
426        if !after_set.is_empty() {
427            return Err(PdfError::other(
428                "CMS SignedData: Attribute has trailing bytes after attrValues SET",
429            ));
430        }
431        // Split the SET body into raw entries.
432        let mut values: Vec<Vec<u8>> = Vec::new();
433        let mut vcursor = set_tlv.body;
434        while !vcursor.is_empty() {
435            let before = vcursor.len();
436            let (_tlv, after_v) = read_tlv(vcursor)?;
437            let consumed = before - after_v.len();
438            values.push(vcursor[..consumed].to_vec());
439            vcursor = after_v;
440        }
441        out.push(Attribute { oid, values });
442        cursor = after;
443    }
444    Ok(out)
445}
446
447/// Split a SET body (or any concatenation of TLVs) into a Vec where
448/// each entry is the raw DER bytes (tag + length + body) of one TLV.
449/// Mirrors [`super::cms::parse_originator_info`]'s helper for the
450/// `certs[]` / `crls[]` arms.
451fn split_set_into_raw_entries(set_body: &[u8]) -> Result<Vec<Vec<u8>>, PdfError> {
452    let mut out = Vec::new();
453    let mut cursor = set_body;
454    while !cursor.is_empty() {
455        let before_len = cursor.len();
456        let (_tlv, after) = read_tlv(cursor)?;
457        let consumed = before_len - after.len();
458        out.push(cursor[..consumed].to_vec());
459        cursor = after;
460    }
461    Ok(out)
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use crate::pubsec::der;
468
469    /// Hand-build a minimal SignedData ContentInfo with one IAS signer
470    /// and an attached `eContent` octets payload, then re-parse it.
471    /// Exercises the "v=1 IAS, signed attrs absent, eContent attached"
472    /// branch — the simplest case the parser handles end-to-end.
473    #[test]
474    fn parse_minimal_signed_data_v1_ias() {
475        let issuer_der = der::write_sequence(b"O=Round-19 Signer");
476        let serial = vec![0x01, 0x42];
477        let digest_oid = vec![2u64, 16, 840, 1, 101, 3, 4, 2, 1]; // sha256
478        let signature_oid = vec![1u64, 2, 840, 113549, 1, 1, 1]; // rsaEncryption
479        let signature_bytes = vec![0xAAu8; 256];
480
481        // SignerInfo body
482        let mut si_body = der::write_integer_u64(1); // v1
483        let ias_body = {
484            let mut b = issuer_der.clone();
485            b.extend_from_slice(&der::write_integer_bytes(&serial));
486            b
487        };
488        si_body.extend_from_slice(&der::write_sequence(&ias_body));
489        // digestAlgorithm
490        let da_alg = {
491            let mut b = der::write_oid(&digest_oid);
492            b.extend_from_slice(&der::write_null());
493            der::write_sequence(&b)
494        };
495        si_body.extend_from_slice(&da_alg);
496        // signatureAlgorithm
497        let sig_alg = {
498            let mut b = der::write_oid(&signature_oid);
499            b.extend_from_slice(&der::write_null());
500            der::write_sequence(&b)
501        };
502        si_body.extend_from_slice(&sig_alg);
503        // signature OCTET STRING
504        si_body.extend_from_slice(&der::write_octet_string(&signature_bytes));
505        let signer_info = der::write_sequence(&si_body);
506
507        // digestAlgorithms SET
508        let da_set = der::write_set(&da_alg);
509
510        // EncapsulatedContentInfo
511        let payload = b"OXIDEAV-attached-econtent-bytes";
512        let eci_body = {
513            let mut b = der::write_oid(&[1u64, 2, 840, 113549, 1, 7, 1]); // id-data
514                                                                          // [0] EXPLICIT OCTET STRING
515            let octet = der::write_octet_string(payload);
516            b.extend_from_slice(&der::write_context_constructed(0, &octet));
517            b
518        };
519        let eci = der::write_sequence(&eci_body);
520
521        // signerInfos SET
522        let si_set = der::write_set(&signer_info);
523
524        // SignedData SEQUENCE
525        let mut sd_body = der::write_integer_u64(1);
526        sd_body.extend_from_slice(&da_set);
527        sd_body.extend_from_slice(&eci);
528        sd_body.extend_from_slice(&si_set);
529        let sd = der::write_sequence(&sd_body);
530
531        // Outer ContentInfo
532        let outer_body = {
533            let mut b = der::write_oid(&OID_SIGNED_DATA);
534            b.extend_from_slice(&der::write_context_constructed(0, &sd));
535            b
536        };
537        let envelope = der::write_sequence(&outer_body);
538
539        let parsed = parse_signed_data(&envelope).expect("parse SignedData");
540        assert_eq!(parsed.version, 1);
541        assert_eq!(parsed.digest_algorithms.len(), 1);
542        assert_eq!(parsed.digest_algorithms[0].0, digest_oid);
543        assert_eq!(parsed.encap_content_type, vec![1, 2, 840, 113549, 1, 7, 1]);
544        assert_eq!(parsed.encap_content_octets.as_deref(), Some(&payload[..]));
545        assert!(parsed.certs.is_empty());
546        assert!(parsed.crls.is_empty());
547        assert_eq!(parsed.signer_infos.len(), 1);
548        let si = &parsed.signer_infos[0];
549        assert_eq!(si.version, 1);
550        match &si.sid {
551            SignerIdentifier::IssuerAndSerial(ias) => {
552                assert_eq!(ias.issuer_der, issuer_der);
553                assert_eq!(ias.serial, serial);
554            }
555            other => panic!("expected IAS got {other:?}"),
556        }
557        assert_eq!(si.digest_algorithm_oid, digest_oid);
558        assert_eq!(si.signature_algorithm_oid, signature_oid);
559        assert_eq!(si.signature, signature_bytes);
560        assert!(si.signed_attrs.is_empty());
561        assert!(si.signed_attrs_der.is_none());
562        assert!(si.unsigned_attrs.is_empty());
563    }
564
565    /// SignedData with one v=3 SKI signer + signed attrs — exercises
566    /// the optional `[0] IMPLICIT SET OF Attribute` parse + the SKI
567    /// SignerIdentifier branch.
568    #[test]
569    fn parse_signed_data_v3_ski_with_signed_attrs() {
570        let signer_ski = vec![0xCDu8; 20];
571        let digest_oid = vec![2u64, 16, 840, 1, 101, 3, 4, 2, 1]; // sha256
572        let signature_oid = vec![1u64, 2, 840, 10045, 4, 3, 2]; // ecdsa-with-SHA256
573        let signature_bytes = vec![0xBBu8; 72];
574
575        // signedAttr: contentType = id-data
576        let attr_oid = vec![1u64, 2, 840, 113549, 1, 9, 3]; // contentType
577        let attr_value = der::write_oid(&[1u64, 2, 840, 113549, 1, 7, 1]); // id-data
578        let attr_seq_body = {
579            let mut b = der::write_oid(&attr_oid);
580            b.extend_from_slice(&der::write_set(&attr_value));
581            b
582        };
583        let attr_seq = der::write_sequence(&attr_seq_body);
584        // signedAttrs is `[0] IMPLICIT SET OF Attribute` — emit a
585        // context-specific constructed [0] wrapping the SET body
586        // (the IMPLICIT replaces the universal SET tag).
587        let signed_attrs_implicit_body = attr_seq.clone();
588        let signed_attrs_tlv = der::write_tlv(
589            der::Class::ContextSpecific,
590            true,
591            0,
592            &signed_attrs_implicit_body,
593        );
594
595        // SignerInfo
596        let mut si_body = der::write_integer_u64(3); // v3
597                                                     // [0] IMPLICIT SubjectKeyIdentifier (OCTET STRING) — primitive context-specific.
598        si_body.extend_from_slice(&der::write_context_primitive(0, &signer_ski));
599        let da_alg = {
600            let mut b = der::write_oid(&digest_oid);
601            b.extend_from_slice(&der::write_null());
602            der::write_sequence(&b)
603        };
604        si_body.extend_from_slice(&da_alg);
605        si_body.extend_from_slice(&signed_attrs_tlv);
606        let sig_alg = {
607            let mut b = der::write_oid(&signature_oid);
608            b.extend_from_slice(&der::write_null());
609            der::write_sequence(&b)
610        };
611        si_body.extend_from_slice(&sig_alg);
612        si_body.extend_from_slice(&der::write_octet_string(&signature_bytes));
613        let signer_info = der::write_sequence(&si_body);
614
615        let da_set = der::write_set(&da_alg);
616        // No eContent — detached signature shape.
617        let eci_body = der::write_oid(&[1u64, 2, 840, 113549, 1, 7, 1]);
618        let eci = der::write_sequence(&eci_body);
619        let si_set = der::write_set(&signer_info);
620
621        let mut sd_body = der::write_integer_u64(3); // v3 since signers use v3
622        sd_body.extend_from_slice(&da_set);
623        sd_body.extend_from_slice(&eci);
624        sd_body.extend_from_slice(&si_set);
625        let sd = der::write_sequence(&sd_body);
626
627        let outer_body = {
628            let mut b = der::write_oid(&OID_SIGNED_DATA);
629            b.extend_from_slice(&der::write_context_constructed(0, &sd));
630            b
631        };
632        let envelope = der::write_sequence(&outer_body);
633
634        let parsed = parse_signed_data(&envelope).expect("parse v3 SKI SignedData");
635        assert_eq!(parsed.version, 3);
636        assert!(parsed.encap_content_octets.is_none(), "detached signature");
637        let si = &parsed.signer_infos[0];
638        match &si.sid {
639            SignerIdentifier::SubjectKeyIdentifier(b) => assert_eq!(b, &signer_ski),
640            other => panic!("expected SKI got {other:?}"),
641        }
642        assert_eq!(si.signed_attrs.len(), 1);
643        assert_eq!(si.signed_attrs[0].oid, attr_oid);
644        assert_eq!(si.signed_attrs[0].values.len(), 1);
645        assert!(si.signed_attrs_der.is_some());
646        assert_eq!(si.signature, signature_bytes);
647    }
648
649    #[test]
650    fn rejects_envelope_with_wrong_oid() {
651        // A bare ContentInfo whose OID is id-envelopedData (1.2.840.113549.1.7.3)
652        // — parse_signed_data must refuse it.
653        let inner = der::write_sequence(&der::write_integer_u64(0));
654        let outer_body = {
655            let mut b = der::write_oid(&[1u64, 2, 840, 113549, 1, 7, 3]);
656            b.extend_from_slice(&der::write_context_constructed(0, &inner));
657            b
658        };
659        let envelope = der::write_sequence(&outer_body);
660        let err = parse_signed_data(&envelope).expect_err("must reject");
661        let msg = format!("{err}");
662        assert!(msg.contains("id-signedData"), "{msg}");
663    }
664}