Skip to main content

xdoc/signature/
policy.rs

1use std::fs;
2use std::path::Path;
3
4use crate::core::{ErrorKind, XmlError, XmlResult};
5
6use super::{digest_bytes, DigestAlgorithm};
7
8/// Explicit XAdES signature policy expected by EPES.
9#[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    /// Builds an explicit XAdES policy by hashing the actual policy document
34    /// bytes with the declared digest algorithm.
35    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    /// Builds an explicit XAdES policy by reading and hashing a local policy
46    /// document. Network fetching and cache policy remain the caller's
47    /// responsibility.
48    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/// Identifier for a signature policy.
79#[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/// Optional qualifiers attached to a signature policy.
94#[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/// Typed XAdES signer role extension.
105#[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}