ppt_rs/generator/slide_content/
digital_signature.rs1#[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#[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#[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#[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 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 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 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('&', "&")
188 .replace('<', "<")
189 .replace('>', ">")
190 .replace('"', """)
191 .replace('\'', "'")
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 & 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}