ribbit_client/
signature_verify.rs

1//! Enhanced signature verification for Ribbit V1 responses
2//!
3//! This module provides more comprehensive signature parsing and validation.
4
5use crate::error::Result;
6use der::{Decode, Encode};
7use digest::Digest;
8use sha2::{Sha256, Sha384, Sha512};
9use tracing::{debug, trace, warn};
10use x509_cert::{certificate::Certificate, der::asn1::ObjectIdentifier, time::Validity};
11
12/// OID constants for common algorithms
13mod oids {
14
15    /// PKCS#7 signedData
16    #[allow(dead_code)]
17    pub const SIGNED_DATA: &str = "1.2.840.113549.1.7.2";
18
19    /// SHA-256
20    pub const SHA256: &str = "2.16.840.1.101.3.4.2.1";
21    /// SHA-384
22    pub const SHA384: &str = "2.16.840.1.101.3.4.2.2";
23    /// SHA-512
24    pub const SHA512: &str = "2.16.840.1.101.3.4.2.3";
25
26    /// RSA with SHA-256
27    pub const RSA_SHA256: &str = "1.2.840.113549.1.1.11";
28    /// RSA with SHA-384
29    pub const RSA_SHA384: &str = "1.2.840.113549.1.1.12";
30    /// RSA with SHA-512
31    pub const RSA_SHA512: &str = "1.2.840.113549.1.1.13";
32
33    /// `SigningTime` attribute
34    pub const SIGNING_TIME: &str = "1.2.840.113549.1.9.5";
35    /// `TimeStampToken` attribute
36    pub const TIMESTAMP_TOKEN: &str = "1.2.840.113549.1.9.16.2.14";
37}
38
39/// Enhanced signature information with verification details
40#[derive(Debug, Clone)]
41pub struct EnhancedSignatureInfo {
42    /// Basic signature info
43    pub format: String,
44    /// Size in bytes
45    pub size: usize,
46    /// Digest algorithm used (e.g., SHA-256)
47    pub digest_algorithm: String,
48    /// Signature algorithm used (e.g., RSA with SHA-256)
49    pub signature_algorithm: String,
50
51    /// Verification status
52    pub is_verified: bool,
53    /// List of verification errors (if any)
54    pub verification_errors: Vec<String>,
55
56    /// Certificate details
57    pub certificates: Vec<CertificateInfo>,
58    /// Number of signers
59    pub signer_count: usize,
60    /// Timestamp information if present
61    pub timestamp_info: Option<TimestampInfo>,
62}
63
64/// Information about a certificate in the chain
65#[derive(Debug, Clone)]
66pub struct CertificateInfo {
67    /// Subject distinguished name
68    pub subject: String,
69    /// Issuer distinguished name
70    pub issuer: String,
71    /// Serial number (hex)
72    pub serial_number: String,
73    /// Not valid before
74    pub not_before: String,
75    /// Not valid after
76    pub not_after: String,
77    /// Whether the certificate is currently valid
78    pub is_valid: bool,
79}
80
81/// Timestamp information from the signature
82#[derive(Debug, Clone)]
83pub struct TimestampInfo {
84    /// Signing time from `SignerInfo`
85    pub signing_time: Option<String>,
86    /// Whether the timestamp is verified
87    pub is_verified: bool,
88    /// Timestamp authority info if available
89    pub timestamp_authority: Option<String>,
90}
91
92/// Parse and verify a PKCS#7 signature
93///
94/// # Arguments
95/// * `signature_bytes` - The raw PKCS#7 signature bytes
96/// * `signed_data` - The data that was signed (optional for verification)
97///
98/// # Errors
99///
100/// Returns an error if parsing fails completely
101#[allow(clippy::too_many_lines)]
102pub fn parse_and_verify_signature(
103    signature_bytes: &[u8],
104    signed_data: Option<&[u8]>,
105) -> Result<EnhancedSignatureInfo> {
106    trace!("Parsing signature: {} bytes", signature_bytes.len());
107
108    let mut info = EnhancedSignatureInfo {
109        format: "PKCS#7/CMS".to_string(),
110        size: signature_bytes.len(),
111        digest_algorithm: "Unknown".to_string(),
112        signature_algorithm: "Unknown".to_string(),
113        is_verified: false,
114        verification_errors: Vec::new(),
115        certificates: Vec::new(),
116        signer_count: 0,
117        timestamp_info: None,
118    };
119
120    // Check minimum size
121    if signature_bytes.is_empty() {
122        info.verification_errors
123            .push("Empty signature data".to_string());
124        return Ok(info);
125    }
126
127    // Try to parse with CMS crate first
128    match crate::cms_parser::parse_cms_signature(signature_bytes) {
129        Ok(cms_info) => {
130            debug!("Successfully parsed CMS signature");
131
132            // Update basic info
133            info.signer_count = cms_info.signers.len();
134
135            // Get first signer's algorithms (most common case)
136            if let Some(first_signer) = cms_info.signers.first() {
137                info.digest_algorithm
138                    .clone_from(&first_signer.digest_algorithm);
139                info.signature_algorithm
140                    .clone_from(&first_signer.signature_algorithm);
141
142                // Log public key extraction
143                if let Some(ref pk) = first_signer.public_key {
144                    debug!(
145                        "Extracted {} public key: {} bits",
146                        pk.algorithm, pk.key_size
147                    );
148                }
149            }
150
151            // Convert certificates
152            for cert in &cms_info.certificates {
153                let cert_info = CertificateInfo {
154                    subject: cert.subject.clone(),
155                    issuer: cert.issuer.clone(),
156                    serial_number: cert.serial_number.clone(),
157                    not_before: "Unknown".to_string(), // CMS doesn't expose validity
158                    not_after: "Unknown".to_string(),
159                    is_valid: true, // Assume valid for now
160                };
161                info.certificates.push(cert_info);
162            }
163
164            // Attempt verification if we have signed data and public key
165            if let Some(data) = signed_data {
166                if let Some(first_signer) = cms_info.signers.first() {
167                    if let Some(ref public_key) = first_signer.public_key {
168                        match crate::cms_parser::verify_with_public_key(
169                            public_key,
170                            data,
171                            &first_signer.signature,
172                            &first_signer.digest_algorithm,
173                        ) {
174                            Ok(true) => {
175                                info.is_verified = true;
176                                debug!("Signature verification successful!");
177                            }
178                            Ok(false) => {
179                                info.verification_errors
180                                    .push("Signature verification failed".to_string());
181                            }
182                            Err(e) => {
183                                info.verification_errors
184                                    .push(format!("Verification error: {e}"));
185                            }
186                        }
187                    } else {
188                        info.verification_errors
189                            .push("No public key found for verification".to_string());
190                    }
191                }
192            }
193
194            // Still extract timestamp using the old method
195            let pkcs7_info = parse_pkcs7_structure(signature_bytes);
196            if let Some(timestamp) = extract_timestamp_info(&pkcs7_info) {
197                info.timestamp_info = Some(timestamp);
198            }
199        }
200        Err(e) => {
201            warn!("CMS parsing failed, falling back to manual parsing: {e}");
202
203            // Fall back to manual parsing
204            let pkcs7_info = parse_pkcs7_structure(signature_bytes);
205            info.digest_algorithm
206                .clone_from(&pkcs7_info.digest_algorithm);
207            info.signature_algorithm
208                .clone_from(&pkcs7_info.signature_algorithm);
209            info.signer_count = pkcs7_info.signer_count;
210
211            // Extract certificates
212            for cert_der in &pkcs7_info.certificates {
213                match Certificate::from_der(cert_der) {
214                    Ok(cert) => {
215                        let cert_info = extract_certificate_info(&cert);
216                        info.certificates.push(cert_info);
217                    }
218                    Err(e) => {
219                        warn!("Failed to parse certificate: {e}");
220                    }
221                }
222            }
223
224            // Extract timestamp information
225            if let Some(timestamp) = extract_timestamp_info(&pkcs7_info) {
226                info.timestamp_info = Some(timestamp);
227            }
228
229            // Attempt verification if we have the signed data
230            if let Some(data) = signed_data {
231                if verify_signature_data(&pkcs7_info, data) {
232                    info.is_verified = true;
233                } else {
234                    info.verification_errors
235                        .push("Signature verification failed".to_string());
236                }
237            }
238        }
239    }
240
241    Ok(info)
242}
243
244/// Internal PKCS#7 parsing result
245struct Pkcs7Info {
246    digest_algorithm: String,
247    signature_algorithm: String,
248    signer_count: usize,
249    certificates: Vec<Vec<u8>>,
250    #[allow(dead_code)]
251    signature_value: Vec<u8>,
252    /// Raw PKCS#7 data for timestamp extraction
253    raw_data: Vec<u8>,
254}
255
256/// Parse PKCS#7 structure manually
257fn parse_pkcs7_structure(data: &[u8]) -> Pkcs7Info {
258    let mut info = Pkcs7Info {
259        digest_algorithm: "Unknown".to_string(),
260        signature_algorithm: "Unknown".to_string(),
261        signer_count: 0,
262        certificates: Vec::new(),
263        signature_value: Vec::new(),
264        raw_data: data.to_vec(),
265    };
266
267    // This is a simplified parser that looks for known patterns
268    // In a production system, you'd use a proper PKCS#7 parser
269
270    // Look for algorithm OIDs
271    if let Some(pos) = find_oid_pattern(data, oids::SHA256) {
272        info.digest_algorithm = "SHA-256".to_string();
273        debug!("Found SHA-256 digest algorithm at position {pos}");
274    } else if let Some(pos) = find_oid_pattern(data, oids::SHA384) {
275        info.digest_algorithm = "SHA-384".to_string();
276        debug!("Found SHA-384 digest algorithm at position {pos}");
277    } else if let Some(pos) = find_oid_pattern(data, oids::SHA512) {
278        info.digest_algorithm = "SHA-512".to_string();
279        debug!("Found SHA-512 digest algorithm at position {pos}");
280    }
281
282    // Look for signature algorithms
283    if find_oid_pattern(data, oids::RSA_SHA256).is_some() {
284        info.signature_algorithm = "RSA with SHA-256".to_string();
285    } else if find_oid_pattern(data, oids::RSA_SHA384).is_some() {
286        info.signature_algorithm = "RSA with SHA-384".to_string();
287    } else if find_oid_pattern(data, oids::RSA_SHA512).is_some() {
288        info.signature_algorithm = "RSA with SHA-512".to_string();
289    }
290
291    // Extract certificates (look for certificate patterns)
292    let mut pos = 0;
293    while pos < data.len().saturating_sub(4) {
294        // Certificates typically start with SEQUENCE tag (0x30) followed by length
295        if data[pos] == 0x30 && data[pos + 1] == 0x82 {
296            let len = ((data[pos + 2] as usize) << 8) | (data[pos + 3] as usize);
297
298            // Certificates are typically 300-2000 bytes
299            if len > 300 && len < 2000 && pos + 4 + len <= data.len() {
300                let cert_data = data[pos..pos + 4 + len].to_vec();
301
302                // Quick validation - check if it might be a certificate
303                if cert_data.len() > 100 && cert_data[4] == 0x30 {
304                    info.certificates.push(cert_data);
305                    debug!("Found potential certificate at position {pos}, length {len}");
306                }
307
308                pos += 4 + len;
309            } else {
310                pos += 1;
311            }
312        } else {
313            pos += 1;
314        }
315    }
316
317    // Count signers (simplified - assume one signer if we found certificates)
318    if !info.certificates.is_empty() {
319        info.signer_count = 1;
320    }
321
322    debug!(
323        "Parsed PKCS#7: {} certificates, {} signers, digest: {}, signature: {}",
324        info.certificates.len(),
325        info.signer_count,
326        info.digest_algorithm,
327        info.signature_algorithm
328    );
329
330    info
331}
332
333/// Find an OID pattern in data
334fn find_oid_pattern(data: &[u8], oid_str: &str) -> Option<usize> {
335    // Convert OID string to DER encoding
336    let oid = ObjectIdentifier::new(oid_str).ok()?;
337    let oid_bytes = oid.to_der().ok()?;
338
339    data.windows(oid_bytes.len())
340        .position(|window| window == oid_bytes)
341}
342
343/// Extract information from a parsed certificate
344fn extract_certificate_info(cert: &Certificate) -> CertificateInfo {
345    let tbs = &cert.tbs_certificate;
346
347    CertificateInfo {
348        subject: tbs.subject.to_string(),
349        issuer: tbs.issuer.to_string(),
350        serial_number: tbs.serial_number.to_string(),
351        not_before: format!("{}", tbs.validity.not_before),
352        not_after: format!("{}", tbs.validity.not_after),
353        is_valid: is_certificate_valid(&tbs.validity),
354    }
355}
356
357/// Check if a certificate is currently valid
358fn is_certificate_valid(_validity: &Validity) -> bool {
359    // For now, we can't easily compare with current time without additional dependencies
360    // In a real implementation, you'd check against the current system time
361    true
362}
363
364/// Extract timestamp information from PKCS#7 structure
365fn extract_timestamp_info(pkcs7: &Pkcs7Info) -> Option<TimestampInfo> {
366    let mut timestamp_info = TimestampInfo {
367        signing_time: None,
368        is_verified: false,
369        timestamp_authority: None,
370    };
371
372    // Look for signing time attribute in the raw PKCS#7 data
373    // In a real implementation, this would parse SignerInfo attributes
374    if let Some(pos) = find_oid_pattern(&pkcs7.raw_data, oids::SIGNING_TIME) {
375        // For now, we just detect the presence
376        timestamp_info.signing_time = Some(format!("Present (position: {pos})"));
377        debug!("Found signing time attribute at position {pos}");
378
379        // Try to extract the actual time (simplified)
380        // SigningTime is typically followed by UTCTime or GeneralizedTime
381        if pos + 20 < pkcs7.raw_data.len() {
382            // Look for time encoding after the OID
383            let time_data = &pkcs7.raw_data[pos + 11..pos + 30];
384            trace!("Time data near signing time OID: {:02x?}", time_data);
385        }
386    }
387
388    // Look for timestamp token
389    if let Some(pos) = find_oid_pattern(&pkcs7.raw_data, oids::TIMESTAMP_TOKEN) {
390        timestamp_info.timestamp_authority = Some("TSA present".to_string());
391        debug!("Found timestamp token at position {pos}");
392    }
393
394    // Return None if no timestamp info found
395    if timestamp_info.signing_time.is_none() && timestamp_info.timestamp_authority.is_none() {
396        None
397    } else {
398        // Basic verification: timestamps should not be in the future
399        timestamp_info.is_verified = true; // For now, just mark as verified if present
400        Some(timestamp_info)
401    }
402}
403
404/// Verify signature data (placeholder implementation)
405fn verify_signature_data(pkcs7: &Pkcs7Info, signed_data: &[u8]) -> bool {
406    // This is a placeholder. Real implementation would:
407    // 1. Extract the signer's public key from the certificate
408    // 2. Compute the message digest
409    // 3. Verify the signature using the public key
410
411    debug!("Signature verification not yet implemented");
412
413    // Compute expected digest
414    let digest = match pkcs7.digest_algorithm.as_str() {
415        "SHA-256" => Sha256::digest(signed_data).to_vec(),
416        "SHA-384" => Sha384::digest(signed_data).to_vec(),
417        "SHA-512" => Sha512::digest(signed_data).to_vec(),
418        _ => return false,
419    };
420
421    debug!(
422        "Computed {} digest: {} bytes",
423        pkcs7.digest_algorithm,
424        digest.len()
425    );
426
427    // For now, we can't verify without proper PKCS#7 parsing
428    false
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    #[test]
436    fn test_oid_pattern_finding() {
437        // Create a buffer with SHA-256 OID
438        let oid = ObjectIdentifier::new(oids::SHA256).unwrap();
439        let oid_bytes = oid.to_der().unwrap();
440
441        let mut data = vec![0x00, 0x00];
442        data.extend_from_slice(&oid_bytes);
443        data.extend_from_slice(&[0x00, 0x00]);
444
445        let pos = find_oid_pattern(&data, oids::SHA256);
446        assert_eq!(pos, Some(2));
447    }
448
449    #[test]
450    fn test_empty_signature_parsing() {
451        let result = parse_and_verify_signature(&[], None);
452        assert!(result.is_ok());
453
454        let info = result.unwrap();
455        assert!(!info.is_verified);
456        assert!(!info.verification_errors.is_empty());
457    }
458}