1use std::fs;
2use std::path::Path;
3
4use crate::core::{ErrorKind, XmlError, XmlResult};
5
6use super::{digest_bytes, DigestAlgorithm};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct SignaturePolicy {
11 pub id: SignaturePolicyId,
12 pub description: Option<String>,
13 pub digest_algorithm: DigestAlgorithm,
14 pub digest_value: Vec<u8>,
15 pub qualifiers: Vec<SignaturePolicyQualifier>,
16}
17
18impl SignaturePolicy {
19 pub fn new(
20 id: SignaturePolicyId,
21 digest_algorithm: DigestAlgorithm,
22 digest_value: impl Into<Vec<u8>>,
23 ) -> Self {
24 Self {
25 id,
26 description: None,
27 digest_algorithm,
28 digest_value: digest_value.into(),
29 qualifiers: Vec::new(),
30 }
31 }
32
33 pub fn from_bytes(
36 id: SignaturePolicyId,
37 digest_algorithm: DigestAlgorithm,
38 policy_document: impl AsRef<[u8]>,
39 ) -> XmlResult<Self> {
40 let digest_value = digest_bytes(digest_algorithm, policy_document)?;
41
42 Ok(Self::new(id, digest_algorithm, digest_value))
43 }
44
45 pub fn from_file(
49 id: SignaturePolicyId,
50 digest_algorithm: DigestAlgorithm,
51 path: impl AsRef<Path>,
52 ) -> XmlResult<Self> {
53 let path = path.as_ref();
54 let policy_document = fs::read(path).map_err(|error| {
55 XmlError::new(
56 ErrorKind::Signature,
57 format!(
58 "cannot read XAdES signature policy document `{}`: {error}",
59 path.display()
60 ),
61 )
62 })?;
63
64 Self::from_bytes(id, digest_algorithm, policy_document)
65 }
66
67 pub fn with_description(mut self, description: impl Into<String>) -> Self {
68 self.description = Some(description.into());
69 self
70 }
71
72 pub fn with_qualifier(mut self, qualifier: SignaturePolicyQualifier) -> Self {
73 self.qualifiers.push(qualifier);
74 self
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum SignaturePolicyId {
81 Uri(String),
82 Oid(String),
83}
84
85impl SignaturePolicyId {
86 pub fn value(&self) -> &str {
87 match self {
88 Self::Uri(value) | Self::Oid(value) => value,
89 }
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum SignaturePolicyQualifier {
96 SpUri(String),
97 SpUserNotice {
98 organization: Option<String>,
99 notice_numbers: Vec<i64>,
100 explicit_text: Option<String>,
101 },
102}
103
104#[derive(Debug, Clone, Default, PartialEq, Eq)]
106pub struct SignerRole {
107 claimed_roles: Vec<String>,
108}
109
110impl SignerRole {
111 pub fn new() -> Self {
112 Self::default()
113 }
114
115 pub fn claimed(role: impl Into<String>) -> Self {
116 Self::new().with_claimed_role(role)
117 }
118
119 pub fn with_claimed_role(mut self, role: impl Into<String>) -> Self {
120 self.claimed_roles.push(role.into());
121 self
122 }
123
124 pub fn claimed_roles(&self) -> &[String] {
125 &self.claimed_roles
126 }
127
128 pub fn is_empty(&self) -> bool {
129 self.claimed_roles.is_empty()
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn signature_policy_keeps_explicit_digest_and_qualifiers() {
139 let policy = SignaturePolicy::new(
140 SignaturePolicyId::Uri("urn:example:policy:v1".to_owned()),
141 DigestAlgorithm::Sha256,
142 [1, 2, 3],
143 )
144 .with_qualifier(SignaturePolicyQualifier::SpUri(
145 "https://example.test/policy.pdf".to_owned(),
146 ));
147
148 assert_eq!(policy.id.value(), "urn:example:policy:v1");
149 assert_eq!(policy.digest_algorithm, DigestAlgorithm::Sha256);
150 assert_eq!(policy.digest_value, vec![1, 2, 3]);
151 assert_eq!(policy.qualifiers.len(), 1);
152 }
153
154 #[test]
155 fn signature_policy_can_hash_document_bytes() -> XmlResult<()> {
156 let policy = SignaturePolicy::from_bytes(
157 SignaturePolicyId::Uri("https://example.test/policy.pdf".to_owned()),
158 DigestAlgorithm::Sha256,
159 b"policy bytes",
160 )?;
161
162 assert_eq!(policy.id.value(), "https://example.test/policy.pdf");
163 assert_eq!(policy.digest_algorithm, DigestAlgorithm::Sha256);
164 assert_eq!(
165 policy.digest_value,
166 digest_bytes(DigestAlgorithm::Sha256, b"policy bytes")?
167 );
168
169 Ok(())
170 }
171
172 #[test]
173 fn signature_policy_can_hash_document_file() -> XmlResult<()> {
174 let path = std::env::temp_dir().join(format!(
175 "xdoc-policy-{}-{}.pdf",
176 std::process::id(),
177 "from-file"
178 ));
179 fs::write(&path, b"policy file bytes").expect("write policy fixture");
180
181 let policy = SignaturePolicy::from_file(
182 SignaturePolicyId::Uri("https://example.test/policy.pdf".to_owned()),
183 DigestAlgorithm::Sha512,
184 &path,
185 )?;
186
187 fs::remove_file(&path).expect("remove policy fixture");
188
189 assert_eq!(policy.digest_algorithm, DigestAlgorithm::Sha512);
190 assert_eq!(
191 policy.digest_value,
192 digest_bytes(DigestAlgorithm::Sha512, b"policy file bytes")?
193 );
194
195 Ok(())
196 }
197
198 #[test]
199 fn signature_policy_file_reports_read_error() {
200 let path =
201 std::env::temp_dir().join(format!("xdoc-policy-{}-missing.pdf", std::process::id()));
202
203 let error = SignaturePolicy::from_file(
204 SignaturePolicyId::Uri("https://example.test/policy.pdf".to_owned()),
205 DigestAlgorithm::Sha256,
206 &path,
207 )
208 .expect_err("missing policy file must fail");
209
210 assert_eq!(error.kind(), &ErrorKind::Signature);
211 assert!(error
212 .message()
213 .contains("cannot read XAdES signature policy document"));
214 assert!(error.message().contains(&path.display().to_string()));
215 }
216
217 #[test]
218 fn signature_policy_supports_optional_description() {
219 let policy = SignaturePolicy::new(
220 SignaturePolicyId::Uri("urn:example:policy:v1".to_owned()),
221 DigestAlgorithm::Sha256,
222 [1, 2, 3],
223 )
224 .with_description("Example policy");
225
226 assert_eq!(policy.description.as_deref(), Some("Example policy"));
227 }
228
229 #[test]
230 fn signature_policy_id_supports_oid() {
231 let id = SignaturePolicyId::Oid("1.2.3.4".to_owned());
232
233 assert_eq!(id.value(), "1.2.3.4");
234 }
235
236 #[test]
237 fn signer_role_keeps_claimed_roles_in_order() {
238 let role = SignerRole::claimed("reviewer").with_claimed_role("approver");
239
240 assert_eq!(role.claimed_roles(), ["reviewer", "approver"]);
241 assert!(!role.is_empty());
242 }
243}