lib3mf_core/crypto/
verification.rs

1use crate::error::{Lib3mfError, Result};
2use crate::model::crypto::*;
3use base64::prelude::*;
4use rsa::RsaPublicKey;
5use rsa::signature::Verifier;
6use sha1::Sha1;
7use sha2::{Digest, Sha256};
8use x509_parser::prelude::FromDer;
9
10/// Verifies a 3MF XML digital signature.
11///
12/// `public_key`: The RSA public key to use for verification.
13/// `content_resolver`: A closure that takes a URI and returns the content bytes.
14/// `signed_info_bytes`: The RAW canonicalized bytes of the `<SignedInfo>` element.
15///                      Note: This is critical. The parser must extract the exact bytes used for signing.
16///                      3MF usually requires C14N. If we don't have a C14N library,
17///                      we must rely on the raw bytes from the file matching the canonical form
18///                      (which is often true for generated files) or implement minimal C14N.
19pub fn verify_signature<F>(
20    signature: &Signature,
21    public_key: &RsaPublicKey,
22    content_resolver: F,
23    signed_info_bytes: &[u8],
24) -> Result<bool>
25where
26    F: Fn(&str) -> Result<Vec<u8>>,
27{
28    // 1. Verify References
29    for reference in &signature.signed_info.references {
30        verify_reference(reference, &content_resolver)?;
31    }
32
33    // 2. Verify SignatureValue
34    let sig_value = BASE64_STANDARD
35        .decode(&signature.signature_value.value)
36        .map_err(|e| Lib3mfError::Validation(format!("Invalid base64 signature: {}", e)))?;
37
38    // 3MF uses RSA-SHA256 usually for SignatureMethod.
39    // Check algo.
40    match signature.signed_info.signature_method.algorithm.as_str() {
41        "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => {
42            // RSA-SHA256
43            let verifying_key = rsa::pkcs1v15::VerifyingKey::<Sha256>::new(public_key.clone());
44            let rsa_signature =
45                rsa::pkcs1v15::Signature::try_from(sig_value.as_slice()).map_err(|e| {
46                    Lib3mfError::Validation(format!("Invalid RSA signature format: {}", e))
47                })?;
48
49            verifying_key
50                .verify(signed_info_bytes, &rsa_signature)
51                .map_err(|e| {
52                    Lib3mfError::Validation(format!("Signature verification failed: {}", e))
53                })?;
54        }
55        _ => {
56            return Err(Lib3mfError::Validation(format!(
57                "Unsupported signature method: {}",
58                signature.signed_info.signature_method.algorithm
59            )));
60        }
61    }
62
63    Ok(true)
64}
65
66fn verify_reference<F>(reference: &Reference, content_resolver: &F) -> Result<()>
67where
68    F: Fn(&str) -> Result<Vec<u8>>,
69{
70    // Resolve content
71    let content = content_resolver(&reference.uri)?;
72
73    // Calculate Digest
74    let calculated_digest = match reference.digest_method.algorithm.as_str() {
75        "http://www.w3.org/2001/04/xmlenc#sha256" => {
76            let mut hasher = Sha256::new();
77            hasher.update(&content);
78            hasher.finalize().to_vec()
79        }
80        "http://www.w3.org/2000/09/xmldsig#sha1" => {
81            let mut hasher = Sha1::new();
82            hasher.update(&content);
83            hasher.finalize().to_vec()
84        }
85        _ => {
86            return Err(Lib3mfError::Validation(format!(
87                "Unsupported digest method: {}",
88                reference.digest_method.algorithm
89            )));
90        }
91    };
92
93    // Decode stored digest
94    let stored_digest = BASE64_STANDARD
95        .decode(&reference.digest_value.value)
96        .map_err(|e| Lib3mfError::Validation(format!("Invalid base64 digest: {}", e)))?;
97
98    if calculated_digest != stored_digest {
99        return Err(Lib3mfError::Validation(format!(
100            "Digest mismatch for URI {}",
101            reference.uri
102        )));
103    }
104
105    Ok(())
106}
107
108/// Verify signature by extracting key from KeyInfo if possible.
109pub fn verify_signature_extended<F>(
110    signature: &Signature,
111    content_resolver: F,
112    signed_info_bytes: &[u8],
113) -> Result<bool>
114where
115    F: Fn(&str) -> Result<Vec<u8>>,
116{
117    let key = extract_key_from_signature(signature)?;
118    verify_signature(signature, &key, content_resolver, signed_info_bytes)
119}
120
121pub fn extract_key_from_signature(signature: &Signature) -> Result<RsaPublicKey> {
122    if let Some(info) = &signature.key_info {
123        // 1. Try KeyValue (RSA)
124        if let Some(kv) = &info.key_value
125            && let Some(rsa_val) = &kv.rsa_key_value
126        {
127            let n_bytes = BASE64_STANDARD
128                .decode(&rsa_val.modulus)
129                .map_err(|e| Lib3mfError::Validation(format!("Invalid modulus base64: {}", e)))?;
130            let e_bytes = BASE64_STANDARD
131                .decode(&rsa_val.exponent)
132                .map_err(|e| Lib3mfError::Validation(format!("Invalid exponent base64: {}", e)))?;
133
134            let n = rsa::BigUint::from_bytes_be(&n_bytes);
135            let e = rsa::BigUint::from_bytes_be(&e_bytes);
136
137            return RsaPublicKey::new(n, e).map_err(|e| {
138                Lib3mfError::Validation(format!("Invalid RSA key components: {}", e))
139            });
140        }
141
142        // 2. Try X509Data
143        if let Some(x509) = &info.x509_data
144            && let Some(cert_b64) = &x509.certificate
145        {
146            // Remove potential headers/whitespace
147            let clean_b64: String = cert_b64.chars().filter(|c| !c.is_whitespace()).collect();
148            let cert_der = BASE64_STANDARD
149                .decode(&clean_b64)
150                .map_err(|e| Lib3mfError::Validation(format!("Invalid X509 base64: {}", e)))?;
151
152            let (_, cert) = x509_parser::certificate::X509Certificate::from_der(&cert_der)
153                .map_err(|e| Lib3mfError::Validation(format!("Invalid X509 certificate: {}", e)))?;
154
155            // Extract SPKI
156            // rsa crate can parse PKCS#1 or PKCS#8. SPKI is usually PKCS#8 compatible (public key info).
157            // cert.tbs_certificate.subject_pki contains the SPKI
158            use rsa::pkcs8::DecodePublicKey;
159            return RsaPublicKey::from_public_key_der(cert.tbs_certificate.subject_pki.raw)
160                .map_err(|e| Lib3mfError::Validation(format!("Invalid RSA key in cert: {}", e)));
161        }
162    }
163    Err(Lib3mfError::Validation(
164        "No usable KeyValue or X509Certificate found in KeyInfo".into(),
165    ))
166}