Skip to main content

ppt_rs/generator/slide_content/
digital_signature.rs

1//! Digital signature support for PPTX presentations
2//!
3//! Provides digital signature metadata and XML generation for the
4//! `_xmlsignatures/` package part per the OOXML digital signature spec.
5
6/// Hash algorithm used for signing
7#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]
8pub enum HashAlgorithm {
9    #[default]
10    Sha256,
11    Sha384,
12    Sha512,
13    Sha1,
14}
15
16impl HashAlgorithm {
17    pub fn uri(&self) -> &'static str {
18        match self {
19            HashAlgorithm::Sha256 => "http://www.w3.org/2001/04/xmlenc#sha256",
20            HashAlgorithm::Sha384 => "http://www.w3.org/2001/04/xmldsig-more#sha384",
21            HashAlgorithm::Sha512 => "http://www.w3.org/2001/04/xmlenc#sha512",
22            HashAlgorithm::Sha1 => "http://www.w3.org/2000/09/xmldsig#sha1",
23        }
24    }
25
26    pub fn name(&self) -> &'static str {
27        match self {
28            HashAlgorithm::Sha256 => "SHA-256",
29            HashAlgorithm::Sha384 => "SHA-384",
30            HashAlgorithm::Sha512 => "SHA-512",
31            HashAlgorithm::Sha1 => "SHA-1",
32        }
33    }
34}
35
36/// Signer identity information
37#[derive(Clone, Debug, Default)]
38pub struct SignerInfo {
39    pub name: String,
40    pub email: Option<String>,
41    pub organization: Option<String>,
42    pub title: Option<String>,
43}
44
45impl SignerInfo {
46    pub fn new(name: &str) -> Self {
47        Self {
48            name: name.to_string(),
49            ..Default::default()
50        }
51    }
52
53    pub fn email(mut self, email: &str) -> Self {
54        self.email = Some(email.to_string());
55        self
56    }
57
58    pub fn organization(mut self, org: &str) -> Self {
59        self.organization = Some(org.to_string());
60        self
61    }
62
63    pub fn title(mut self, title: &str) -> Self {
64        self.title = Some(title.to_string());
65        self
66    }
67}
68
69/// Digital signature configuration for a presentation
70#[derive(Clone, Debug, Default)]
71pub struct DigitalSignature {
72    pub signer: SignerInfo,
73    pub hash_algorithm: HashAlgorithm,
74    pub sign_date: Option<String>,
75    pub commitment_type: SignatureCommitment,
76    pub comments: Option<String>,
77}
78
79/// Commitment type for the signature
80#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]
81pub enum SignatureCommitment {
82    #[default]
83    Created,
84    Approved,
85    Reviewed,
86}
87
88impl SignatureCommitment {
89    pub fn uri(&self) -> &'static str {
90        match self {
91            SignatureCommitment::Created => "http://uri.etsi.org/01903/v1.2.2#ProofOfCreation",
92            SignatureCommitment::Approved => "http://uri.etsi.org/01903/v1.2.2#ProofOfApproval",
93            SignatureCommitment::Reviewed => "http://uri.etsi.org/01903/v1.2.2#ProofOfReview",
94        }
95    }
96
97    pub fn label(&self) -> &'static str {
98        match self {
99            SignatureCommitment::Created => "Created",
100            SignatureCommitment::Approved => "Approved",
101            SignatureCommitment::Reviewed => "Reviewed",
102        }
103    }
104}
105
106impl DigitalSignature {
107    pub fn new(signer: SignerInfo) -> Self {
108        Self {
109            signer,
110            hash_algorithm: HashAlgorithm::default(),
111            sign_date: None,
112            commitment_type: SignatureCommitment::default(),
113            comments: None,
114        }
115    }
116
117    pub fn hash_algorithm(mut self, algo: HashAlgorithm) -> Self {
118        self.hash_algorithm = algo;
119        self
120    }
121
122    pub fn sign_date(mut self, date: &str) -> Self {
123        self.sign_date = Some(date.to_string());
124        self
125    }
126
127    pub fn commitment(mut self, commitment: SignatureCommitment) -> Self {
128        self.commitment_type = commitment;
129        self
130    }
131
132    pub fn comments(mut self, comments: &str) -> Self {
133        self.comments = Some(comments.to_string());
134        self
135    }
136
137    /// Generate the `_xmlsignatures/origin.sigs` relationship XML
138    pub fn to_origin_xml(&self) -> String {
139        r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>"#.to_string()
140    }
141
142    /// Generate signature info XML for `_xmlsignatures/sig1.xml`
143    pub fn to_signature_xml(&self) -> String {
144        let date = self.sign_date.as_deref().unwrap_or("2025-01-01T00:00:00Z");
145        let comments_xml = self.comments.as_ref()
146            .map(|c| format!("<SignatureComments>{}</SignatureComments>", xml_escape(c)))
147            .unwrap_or_default();
148
149        let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#);
150        xml.push_str(r#"<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">"#);
151        xml.push_str(r#"<SignedInfo>"#);
152        xml.push_str(r#"<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>"#);
153        xml.push_str(&format!(
154            r#"<SignatureMethod Algorithm="{}"/>"#,
155            self.hash_algorithm.uri()
156        ));
157        xml.push_str(r#"</SignedInfo>"#);
158        xml.push_str(r#"<SignatureValue/>"#);
159        xml.push_str(r#"<KeyInfo>"#);
160        xml.push_str(&format!(
161            r#"<KeyName>{}</KeyName>"#,
162            xml_escape(&self.signer.name)
163        ));
164        xml.push_str(r#"</KeyInfo>"#);
165        xml.push_str("<Object>");
166        xml.push_str(&format!(
167            "<SignatureProperties><SignatureProperty Target=\"#SignatureInfo\"><SignatureInfoV1 xmlns=\"http://schemas.microsoft.com/office/2006/digsig\"><SetupID/><SignatureText>{}</SignatureText>{}<SignatureType>1</SignatureType><SignatureProviderUrl/><SignatureProviderDetails>9</SignatureProviderDetails><ManifestHashAlgorithm>{}</ManifestHashAlgorithm><SignatureProviderId>{{{{00000000-0000-0000-0000-000000000000}}}}</SignatureProviderId><CommitmentTypeId>{}</CommitmentTypeId><CommitmentTypeQualifier>{}</CommitmentTypeQualifier><SigningTime>{}</SigningTime></SignatureInfoV1></SignatureProperty></SignatureProperties>",
168            xml_escape(&self.signer.name),
169            comments_xml,
170            self.hash_algorithm.uri(),
171            self.commitment_type.uri(),
172            self.commitment_type.label(),
173            date,
174        ));
175        xml.push_str(r#"</Object>"#);
176        xml.push_str(r#"</Signature>"#);
177        xml
178    }
179
180    /// Generate content type entry for digital signatures
181    pub fn content_type_entry() -> &'static str {
182        r#"<Override PartName="/_xmlsignatures/sig1.xml" ContentType="application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"/>"#
183    }
184}
185
186fn xml_escape(s: &str) -> String {
187    s.replace('&', "&amp;")
188        .replace('<', "&lt;")
189        .replace('>', "&gt;")
190        .replace('"', "&quot;")
191        .replace('\'', "&apos;")
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_hash_algorithm_default() {
200        let algo = HashAlgorithm::default();
201        assert_eq!(algo, HashAlgorithm::Sha256);
202        assert!(algo.uri().contains("sha256"));
203        assert_eq!(algo.name(), "SHA-256");
204    }
205
206    #[test]
207    fn test_hash_algorithm_variants() {
208        assert!(HashAlgorithm::Sha384.uri().contains("sha384"));
209        assert!(HashAlgorithm::Sha512.uri().contains("sha512"));
210        assert!(HashAlgorithm::Sha1.uri().contains("sha1"));
211        assert_eq!(HashAlgorithm::Sha384.name(), "SHA-384");
212        assert_eq!(HashAlgorithm::Sha512.name(), "SHA-512");
213        assert_eq!(HashAlgorithm::Sha1.name(), "SHA-1");
214    }
215
216    #[test]
217    fn test_signer_info_new() {
218        let signer = SignerInfo::new("Alice");
219        assert_eq!(signer.name, "Alice");
220        assert!(signer.email.is_none());
221        assert!(signer.organization.is_none());
222    }
223
224    #[test]
225    fn test_signer_info_builder() {
226        let signer = SignerInfo::new("Bob")
227            .email("bob@example.com")
228            .organization("Acme Corp")
229            .title("Engineer");
230        assert_eq!(signer.name, "Bob");
231        assert_eq!(signer.email.as_deref(), Some("bob@example.com"));
232        assert_eq!(signer.organization.as_deref(), Some("Acme Corp"));
233        assert_eq!(signer.title.as_deref(), Some("Engineer"));
234    }
235
236    #[test]
237    fn test_signature_commitment_variants() {
238        assert!(SignatureCommitment::Created.uri().contains("Creation"));
239        assert!(SignatureCommitment::Approved.uri().contains("Approval"));
240        assert!(SignatureCommitment::Reviewed.uri().contains("Review"));
241        assert_eq!(SignatureCommitment::Created.label(), "Created");
242        assert_eq!(SignatureCommitment::Approved.label(), "Approved");
243        assert_eq!(SignatureCommitment::Reviewed.label(), "Reviewed");
244    }
245
246    #[test]
247    fn test_digital_signature_new() {
248        let sig = DigitalSignature::new(SignerInfo::new("Alice"));
249        assert_eq!(sig.signer.name, "Alice");
250        assert_eq!(sig.hash_algorithm, HashAlgorithm::Sha256);
251        assert_eq!(sig.commitment_type, SignatureCommitment::Created);
252    }
253
254    #[test]
255    fn test_digital_signature_builder() {
256        let sig = DigitalSignature::new(SignerInfo::new("Bob"))
257            .hash_algorithm(HashAlgorithm::Sha512)
258            .sign_date("2025-06-15T10:00:00Z")
259            .commitment(SignatureCommitment::Approved)
260            .comments("Looks good");
261        assert_eq!(sig.hash_algorithm, HashAlgorithm::Sha512);
262        assert_eq!(sig.sign_date.as_deref(), Some("2025-06-15T10:00:00Z"));
263        assert_eq!(sig.commitment_type, SignatureCommitment::Approved);
264        assert_eq!(sig.comments.as_deref(), Some("Looks good"));
265    }
266
267    #[test]
268    fn test_signature_xml() {
269        let sig = DigitalSignature::new(SignerInfo::new("Alice"))
270            .sign_date("2025-01-01T00:00:00Z");
271        let xml = sig.to_signature_xml();
272        assert!(xml.contains("<Signature"));
273        assert!(xml.contains("Alice"));
274        assert!(xml.contains("sha256"));
275        assert!(xml.contains("SigningTime"));
276    }
277
278    #[test]
279    fn test_signature_xml_with_comments() {
280        let sig = DigitalSignature::new(SignerInfo::new("Bob"))
281            .comments("Reviewed & approved");
282        let xml = sig.to_signature_xml();
283        assert!(xml.contains("Reviewed &amp; approved"));
284    }
285
286    #[test]
287    fn test_origin_xml() {
288        let sig = DigitalSignature::new(SignerInfo::new("X"));
289        let xml = sig.to_origin_xml();
290        assert!(xml.contains("Relationships"));
291    }
292
293    #[test]
294    fn test_content_type_entry() {
295        let ct = DigitalSignature::content_type_entry();
296        assert!(ct.contains("digital-signature"));
297        assert!(ct.contains("sig1.xml"));
298    }
299}