ribbit_client/
cms_parser.rs

1//! CMS/PKCS#7 parser for extracting signer certificates and public keys
2//!
3//! This module provides proper PKCS#7 parsing using the cms crate to:
4//! - Parse `SignedData` structures
5//! - Extract signer certificates
6//! - Extract public keys for signature verification
7
8use crate::error::{Error, Result};
9use cms::cert::CertificateChoices;
10use cms::content_info::ContentInfo;
11use cms::signed_data::SignerInfo;
12use der::{Decode, Encode};
13use rsa::RsaPublicKey;
14use rsa::pkcs1::DecodeRsaPublicKey;
15use rsa::signature::Verifier;
16use rsa::traits::PublicKeyParts;
17use sha2::{Sha256, Sha384, Sha512};
18use tracing::{debug, trace, warn};
19use x509_cert::certificate::Certificate;
20use x509_cert::spki::SubjectPublicKeyInfoRef;
21
22/// Information about a parsed CMS/PKCS#7 signature
23#[derive(Debug, Clone)]
24pub struct CmsSignatureInfo {
25    /// The `SignedData` structure
26    pub signed_data: SignedDataInfo,
27    /// Information about each signer
28    pub signers: Vec<SignerDetails>,
29    /// All certificates in the signature
30    pub certificates: Vec<CertificateDetails>,
31    /// Raw `SignedData` for verification
32    pub raw_signed_data: Vec<u8>,
33}
34
35/// Parsed `SignedData` information
36#[derive(Debug, Clone)]
37pub struct SignedDataInfo {
38    /// CMS version
39    pub version: u8,
40    /// Digest algorithms used
41    pub digest_algorithms: Vec<String>,
42    /// Whether this contains detached signature
43    pub is_detached: bool,
44}
45
46/// Details about a signer
47#[derive(Debug, Clone)]
48pub struct SignerDetails {
49    /// Signer identifier (issuer and serial)
50    pub identifier: SignerIdentifier,
51    /// Digest algorithm used
52    pub digest_algorithm: String,
53    /// Signature algorithm used
54    pub signature_algorithm: String,
55    /// The signature value
56    pub signature: Vec<u8>,
57    /// The signer's certificate (if found)
58    pub certificate: Option<CertificateDetails>,
59    /// The public key (if extracted)
60    pub public_key: Option<PublicKeyInfo>,
61    /// Whether signed attributes are present
62    pub has_signed_attributes: bool,
63    /// DER-encoded signed attributes (if present)
64    pub signed_attributes_der: Option<Vec<u8>>,
65}
66
67/// Signer identifier
68#[derive(Debug, Clone)]
69pub struct SignerIdentifier {
70    /// Issuer distinguished name
71    pub issuer: String,
72    /// Serial number (hex)
73    pub serial_number: String,
74}
75
76/// Certificate details
77#[derive(Debug, Clone)]
78pub struct CertificateDetails {
79    /// Subject DN
80    pub subject: String,
81    /// Issuer DN
82    pub issuer: String,
83    /// Serial number (hex)
84    pub serial_number: String,
85    /// Public key info
86    pub public_key: Option<PublicKeyInfo>,
87}
88
89/// Public key information
90#[derive(Debug, Clone)]
91pub struct PublicKeyInfo {
92    /// Algorithm (e.g., "RSA", "ECDSA")
93    pub algorithm: String,
94    /// Key size in bits
95    pub key_size: usize,
96    /// The actual public key bytes (DER encoded)
97    pub key_bytes: Vec<u8>,
98}
99
100/// Parse a CMS/PKCS#7 signature and extract signer information
101///
102/// # Errors
103/// Returns an error if:
104/// - The input is not a valid CMS/PKCS#7 structure
105/// - The `ContentInfo` cannot be parsed
106/// - The `SignedData` structure is malformed
107/// - Certificate parsing fails
108pub fn parse_cms_signature(signature_bytes: &[u8]) -> Result<CmsSignatureInfo> {
109    trace!("Parsing CMS signature: {} bytes", signature_bytes.len());
110
111    // Parse as ContentInfo
112    let content_info = ContentInfo::from_der(signature_bytes)
113        .map_err(|e| Error::Asn1Error(format!("Failed to parse ContentInfo: {e:?}")))?;
114
115    // Check content type - SignedData OID is 1.2.840.113549.1.7.2
116    let signed_data_oid = der::asn1::ObjectIdentifier::new("1.2.840.113549.1.7.2")
117        .map_err(|e| Error::Asn1Error(format!("Invalid OID: {e}")))?;
118
119    if content_info.content_type != signed_data_oid {
120        return Err(Error::Asn1Error(
121            "ContentInfo is not SignedData".to_string(),
122        ));
123    }
124
125    // Re-encode the AnyRef to get the SignedData bytes
126    let signed_data_bytes = content_info
127        .content
128        .to_der()
129        .map_err(|e| Error::Asn1Error(format!("Failed to encode content: {e:?}")))?;
130
131    // Parse as SignedData
132    let signed_data = cms::signed_data::SignedData::from_der(&signed_data_bytes)
133        .map_err(|e| Error::Asn1Error(format!("Failed to parse SignedData: {e:?}")))?;
134
135    debug!(
136        "Parsed SignedData: {} signers",
137        signed_data.signer_infos.0.len()
138    );
139
140    // Parse digest algorithms
141    let digest_algorithms: Vec<String> = signed_data
142        .digest_algorithms
143        .iter()
144        .map(|alg| oid_to_algorithm_name(&alg.oid))
145        .collect();
146
147    // Check if detached signature (no encapsulated content)
148    let is_detached = signed_data.encap_content_info.econtent.is_none();
149
150    // Parse all certificates
151    let mut certificates = Vec::new();
152    if let Some(cert_set) = &signed_data.certificates {
153        debug!("Certificate set present with {} entries", cert_set.0.len());
154        for (i, cert_choice) in cert_set.0.iter().enumerate() {
155            match cert_choice {
156                CertificateChoices::Certificate(cert) => {
157                    debug!("Entry {} is a Certificate", i);
158                    if let Ok(details) = extract_certificate_details(cert) {
159                        certificates.push(details);
160                    }
161                }
162                CertificateChoices::Other(_) => {
163                    debug!(
164                        "Entry {} is not a Certificate (different CertificateChoice variant)",
165                        i
166                    );
167                }
168            }
169        }
170    } else {
171        debug!("No certificate set in SignedData");
172    }
173
174    debug!("Found {} certificates", certificates.len());
175
176    // Parse each signer
177    let mut signers = Vec::new();
178    debug!("Processing {} signers", signed_data.signer_infos.0.len());
179    for (i, signer_info) in signed_data.signer_infos.0.iter().enumerate() {
180        debug!("Processing signer #{}", i);
181        match parse_signer_info(signer_info, &certificates) {
182            Ok(signer) => {
183                debug!("Successfully parsed signer #{}", i);
184                signers.push(signer);
185            }
186            Err(e) => {
187                warn!("Failed to parse signer #{}: {}", i, e);
188            }
189        }
190    }
191
192    Ok(CmsSignatureInfo {
193        signed_data: SignedDataInfo {
194            version: 1, // CMS version is usually 1 or 3
195            digest_algorithms,
196            is_detached,
197        },
198        signers,
199        certificates,
200        raw_signed_data: signed_data_bytes,
201    })
202}
203
204/// Parse a `SignerInfo` and match with certificate
205fn parse_signer_info(
206    signer_info: &SignerInfo,
207    certificates: &[CertificateDetails],
208) -> Result<SignerDetails> {
209    let identifier = match &signer_info.sid {
210        cms::signed_data::SignerIdentifier::IssuerAndSerialNumber(isn) => {
211            debug!("Signer uses IssuerAndSerialNumber");
212            SignerIdentifier {
213                issuer: isn.issuer.to_string(),
214                serial_number: hex::encode(isn.serial_number.as_bytes()),
215            }
216        }
217        cms::signed_data::SignerIdentifier::SubjectKeyIdentifier(ski) => {
218            debug!("Signer uses SubjectKeyIdentifier");
219            // Convert SKI to hex string for identification
220            let ski_hex = hex::encode(ski.0.as_bytes());
221            SignerIdentifier {
222                issuer: format!("SubjectKeyIdentifier: {ski_hex}"),
223                serial_number: ski_hex,
224            }
225        }
226    };
227
228    debug!(
229        "Looking for certificate matching issuer='{}', serial='{}'",
230        identifier.issuer, identifier.serial_number
231    );
232
233    // Find matching certificate
234    let certificate = certificates
235        .iter()
236        .find(|cert| {
237            let matches =
238                cert.issuer == identifier.issuer && cert.serial_number == identifier.serial_number;
239            if !matches {
240                trace!(
241                    "Certificate mismatch: cert.issuer='{}', cert.serial='{}'",
242                    cert.issuer, cert.serial_number
243                );
244            }
245            matches
246        })
247        .cloned();
248
249    // Extract public key if we have the certificate
250    let public_key = certificate
251        .as_ref()
252        .and_then(|cert| cert.public_key.clone());
253
254    if certificate.is_none() {
255        warn!(
256            "No certificate found for signer: issuer='{}', serial='{}'",
257            identifier.issuer, identifier.serial_number
258        );
259        debug!("Available certificates: {}", certificates.len());
260    } else {
261        debug!("Found certificate for signer");
262    }
263
264    // Check for signed attributes and encode them if present
265    let (has_signed_attributes, signed_attributes_der) = if let Some(signed_attrs) =
266        &signer_info.signed_attrs
267    {
268        debug!("Signer has {} signed attributes", signed_attrs.len());
269
270        // For CMS signature verification, we need to encode the signed attributes
271        // as a SET OF (implicit tag [0]) for signature verification
272        let mut attr_bytes = Vec::new();
273
274        // We need to re-encode as SET instead of implicit [0]
275        // First collect all attributes
276        let mut encoded_attrs = Vec::new();
277        for attr in signed_attrs.iter() {
278            encoded_attrs.push(
279                attr.to_der()
280                    .map_err(|e| Error::Asn1Error(format!("Failed to encode attribute: {e}")))?,
281            );
282        }
283
284        // Sort for SET encoding (DER canonical)
285        encoded_attrs.sort();
286
287        // Manually build SET OF
288        attr_bytes.push(0x31); // SET tag
289
290        // Calculate length
291        let content_len: usize = encoded_attrs.iter().map(std::vec::Vec::len).sum();
292        if content_len < 128 {
293            #[allow(clippy::cast_possible_truncation)]
294            {
295                attr_bytes.push(content_len as u8);
296            }
297        } else {
298            // Long form
299            let len_bytes = content_len.to_be_bytes();
300            let len_bytes = &len_bytes[len_bytes.iter().position(|&b| b != 0).unwrap_or(0)..];
301            #[allow(clippy::cast_possible_truncation)]
302            {
303                attr_bytes.push(0x80 | len_bytes.len() as u8);
304            }
305            attr_bytes.extend_from_slice(len_bytes);
306        }
307
308        // Add all attributes
309        for attr in encoded_attrs {
310            attr_bytes.extend_from_slice(&attr);
311        }
312
313        (true, Some(attr_bytes))
314    } else {
315        debug!("Signer has no signed attributes - signature is directly over content");
316        (false, None)
317    };
318
319    Ok(SignerDetails {
320        identifier,
321        digest_algorithm: oid_to_algorithm_name(&signer_info.digest_alg.oid),
322        signature_algorithm: oid_to_algorithm_name(&signer_info.signature_algorithm.oid),
323        signature: signer_info.signature.as_bytes().to_vec(),
324        certificate,
325        public_key,
326        has_signed_attributes,
327        signed_attributes_der,
328    })
329}
330
331/// Extract details from a certificate
332fn extract_certificate_details(cert: &Certificate) -> Result<CertificateDetails> {
333    let tbs = &cert.tbs_certificate;
334
335    // Extract public key info
336    // Convert SPKI to owned type for compatibility
337    let spki_der = tbs
338        .subject_public_key_info
339        .to_der()
340        .map_err(|e| Error::Asn1Error(format!("Failed to encode SPKI: {e}")))?;
341    let spki_ref = SubjectPublicKeyInfoRef::from_der(&spki_der)
342        .map_err(|e| Error::Asn1Error(format!("Failed to parse SPKI: {e}")))?;
343
344    let public_key = extract_public_key_info(&spki_ref);
345
346    Ok(CertificateDetails {
347        subject: tbs.subject.to_string(),
348        issuer: tbs.issuer.to_string(),
349        serial_number: hex::encode(tbs.serial_number.as_bytes()),
350        public_key: Some(public_key),
351    })
352}
353
354/// Extract public key information from `SubjectPublicKeyInfo`
355fn extract_public_key_info(spki: &SubjectPublicKeyInfoRef<'_>) -> PublicKeyInfo {
356    let algorithm = oid_to_algorithm_name(&spki.algorithm.oid);
357    let key_bytes = spki.subject_public_key.raw_bytes().to_vec();
358
359    // Try to determine key size
360    let key_size = match algorithm.as_str() {
361        "RSA" => {
362            // Try to decode as RSA public key to get modulus size
363            if let Ok(rsa_key) = RsaPublicKey::from_pkcs1_der(spki.subject_public_key.raw_bytes()) {
364                // Estimate key size from modulus length
365                rsa_key.size() * 8
366            } else {
367                // Fallback: estimate from key length
368                key_bytes.len() * 8
369            }
370        }
371        _ => key_bytes.len() * 8,
372    };
373
374    PublicKeyInfo {
375        algorithm,
376        key_size,
377        key_bytes,
378    }
379}
380
381/// Convert OID to human-readable algorithm name
382fn oid_to_algorithm_name(oid: &der::asn1::ObjectIdentifier) -> String {
383    match oid.to_string().as_str() {
384        // Digest algorithms
385        "2.16.840.1.101.3.4.2.1" => "SHA-256".to_string(),
386        "2.16.840.1.101.3.4.2.2" => "SHA-384".to_string(),
387        "2.16.840.1.101.3.4.2.3" => "SHA-512".to_string(),
388        "1.3.14.3.2.26" => "SHA-1".to_string(),
389
390        // Signature algorithms
391        "1.2.840.113549.1.1.11" => "RSA with SHA-256".to_string(),
392        "1.2.840.113549.1.1.12" => "RSA with SHA-384".to_string(),
393        "1.2.840.113549.1.1.13" => "RSA with SHA-512".to_string(),
394        "1.2.840.113549.1.1.5" => "RSA with SHA-1".to_string(),
395        "1.2.840.113549.1.1.1" => "RSA".to_string(),
396
397        // ECDSA
398        "1.2.840.10045.4.3.2" => "ECDSA with SHA-256".to_string(),
399        "1.2.840.10045.4.3.3" => "ECDSA with SHA-384".to_string(),
400        "1.2.840.10045.4.3.4" => "ECDSA with SHA-512".to_string(),
401
402        _ => format!("OID: {oid}"),
403    }
404}
405
406/// Verify a signature using the extracted public key
407///
408/// For CMS signatures with signed attributes, pass the DER-encoded attributes
409/// as `signed_data`. For direct signatures, pass the original content.
410///
411/// # Errors
412/// Returns an error if the public key algorithm is unsupported or signature verification fails.
413pub fn verify_with_public_key(
414    public_key: &PublicKeyInfo,
415    signed_data: &[u8],
416    signature: &[u8],
417    digest_algorithm: &str,
418) -> Result<bool> {
419    match public_key.algorithm.as_str() {
420        algo if algo == "RSA" || algo.starts_with("RSA with") => {
421            verify_rsa_signature(public_key, signed_data, signature, digest_algorithm)
422        }
423        _ => Err(Error::Asn1Error(format!(
424            "Unsupported algorithm for verification: {}",
425            public_key.algorithm
426        ))),
427    }
428}
429
430/// Verify RSA signature
431fn verify_rsa_signature(
432    public_key: &PublicKeyInfo,
433    signed_data: &[u8],
434    signature: &[u8],
435    digest_algorithm: &str,
436) -> Result<bool> {
437    // Parse the public key from DER format
438    // The key_bytes contain the SubjectPublicKeyInfo, we need to extract the actual RSA key
439    let rsa_key = if let Ok(key) = RsaPublicKey::from_pkcs1_der(&public_key.key_bytes) {
440        key
441    } else {
442        // Try parsing as SubjectPublicKeyInfo
443        let spki = x509_cert::spki::SubjectPublicKeyInfoOwned::from_der(&public_key.key_bytes)
444            .map_err(|e| Error::Asn1Error(format!("Failed to parse SubjectPublicKeyInfo: {e}")))?;
445
446        RsaPublicKey::from_pkcs1_der(spki.subject_public_key.raw_bytes())
447            .map_err(|e| Error::Asn1Error(format!("Failed to decode RSA key from SPKI: {e}")))?
448    };
449
450    // Create the appropriate verifying key based on the digest algorithm
451    let result = match digest_algorithm {
452        "SHA-256" => {
453            let verifying_key = rsa::pkcs1v15::VerifyingKey::<Sha256>::new(rsa_key);
454            let signature_obj = rsa::pkcs1v15::Signature::try_from(signature)
455                .map_err(|e| Error::Asn1Error(format!("Invalid signature format: {e}")))?;
456            verifying_key.verify(signed_data, &signature_obj).is_ok()
457        }
458        "SHA-384" => {
459            let verifying_key = rsa::pkcs1v15::VerifyingKey::<Sha384>::new(rsa_key);
460            let signature_obj = rsa::pkcs1v15::Signature::try_from(signature)
461                .map_err(|e| Error::Asn1Error(format!("Invalid signature format: {e}")))?;
462            verifying_key.verify(signed_data, &signature_obj).is_ok()
463        }
464        "SHA-512" => {
465            let verifying_key = rsa::pkcs1v15::VerifyingKey::<Sha512>::new(rsa_key);
466            let signature_obj = rsa::pkcs1v15::Signature::try_from(signature)
467                .map_err(|e| Error::Asn1Error(format!("Invalid signature format: {e}")))?;
468            verifying_key.verify(signed_data, &signature_obj).is_ok()
469        }
470        _ => {
471            return Err(Error::Asn1Error(format!(
472                "Unsupported digest algorithm: {digest_algorithm}"
473            )));
474        }
475    };
476
477    debug!(
478        "RSA signature verification with {}: {}",
479        digest_algorithm,
480        if result { "SUCCESS" } else { "FAILED" }
481    );
482
483    Ok(result)
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    #[test]
491    fn test_oid_to_algorithm_name() {
492        use der::asn1::ObjectIdentifier;
493
494        let sha256_oid = ObjectIdentifier::new("2.16.840.1.101.3.4.2.1").unwrap();
495        assert_eq!(oid_to_algorithm_name(&sha256_oid), "SHA-256");
496
497        let rsa_sha256_oid = ObjectIdentifier::new("1.2.840.113549.1.1.11").unwrap();
498        assert_eq!(oid_to_algorithm_name(&rsa_sha256_oid), "RSA with SHA-256");
499    }
500}