use std::fs;
use std::path::Path;
use crate::core::{ErrorKind, XmlError, XmlResult};
use super::{digest_bytes, DigestAlgorithm};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignaturePolicy {
pub id: SignaturePolicyId,
pub description: Option<String>,
pub digest_algorithm: DigestAlgorithm,
pub digest_value: Vec<u8>,
pub qualifiers: Vec<SignaturePolicyQualifier>,
}
impl SignaturePolicy {
pub fn new(
id: SignaturePolicyId,
digest_algorithm: DigestAlgorithm,
digest_value: impl Into<Vec<u8>>,
) -> Self {
Self {
id,
description: None,
digest_algorithm,
digest_value: digest_value.into(),
qualifiers: Vec::new(),
}
}
pub fn from_bytes(
id: SignaturePolicyId,
digest_algorithm: DigestAlgorithm,
policy_document: impl AsRef<[u8]>,
) -> XmlResult<Self> {
let digest_value = digest_bytes(digest_algorithm, policy_document)?;
Ok(Self::new(id, digest_algorithm, digest_value))
}
pub fn from_file(
id: SignaturePolicyId,
digest_algorithm: DigestAlgorithm,
path: impl AsRef<Path>,
) -> XmlResult<Self> {
let path = path.as_ref();
let policy_document = fs::read(path).map_err(|error| {
XmlError::new(
ErrorKind::Signature,
format!(
"cannot read XAdES signature policy document `{}`: {error}",
path.display()
),
)
})?;
Self::from_bytes(id, digest_algorithm, policy_document)
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_qualifier(mut self, qualifier: SignaturePolicyQualifier) -> Self {
self.qualifiers.push(qualifier);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SignaturePolicyId {
Uri(String),
Oid(String),
}
impl SignaturePolicyId {
pub fn value(&self) -> &str {
match self {
Self::Uri(value) | Self::Oid(value) => value,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SignaturePolicyQualifier {
SpUri(String),
SpUserNotice {
organization: Option<String>,
notice_numbers: Vec<i64>,
explicit_text: Option<String>,
},
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SignerRole {
claimed_roles: Vec<String>,
}
impl SignerRole {
pub fn new() -> Self {
Self::default()
}
pub fn claimed(role: impl Into<String>) -> Self {
Self::new().with_claimed_role(role)
}
pub fn with_claimed_role(mut self, role: impl Into<String>) -> Self {
self.claimed_roles.push(role.into());
self
}
pub fn claimed_roles(&self) -> &[String] {
&self.claimed_roles
}
pub fn is_empty(&self) -> bool {
self.claimed_roles.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signature_policy_keeps_explicit_digest_and_qualifiers() {
let policy = SignaturePolicy::new(
SignaturePolicyId::Uri("urn:example:policy:v1".to_owned()),
DigestAlgorithm::Sha256,
[1, 2, 3],
)
.with_qualifier(SignaturePolicyQualifier::SpUri(
"https://example.test/policy.pdf".to_owned(),
));
assert_eq!(policy.id.value(), "urn:example:policy:v1");
assert_eq!(policy.digest_algorithm, DigestAlgorithm::Sha256);
assert_eq!(policy.digest_value, vec![1, 2, 3]);
assert_eq!(policy.qualifiers.len(), 1);
}
#[test]
fn signature_policy_can_hash_document_bytes() -> XmlResult<()> {
let policy = SignaturePolicy::from_bytes(
SignaturePolicyId::Uri("https://example.test/policy.pdf".to_owned()),
DigestAlgorithm::Sha256,
b"policy bytes",
)?;
assert_eq!(policy.id.value(), "https://example.test/policy.pdf");
assert_eq!(policy.digest_algorithm, DigestAlgorithm::Sha256);
assert_eq!(
policy.digest_value,
digest_bytes(DigestAlgorithm::Sha256, b"policy bytes")?
);
Ok(())
}
#[test]
fn signature_policy_can_hash_document_file() -> XmlResult<()> {
let path = std::env::temp_dir().join(format!(
"xdoc-policy-{}-{}.pdf",
std::process::id(),
"from-file"
));
fs::write(&path, b"policy file bytes").expect("write policy fixture");
let policy = SignaturePolicy::from_file(
SignaturePolicyId::Uri("https://example.test/policy.pdf".to_owned()),
DigestAlgorithm::Sha512,
&path,
)?;
fs::remove_file(&path).expect("remove policy fixture");
assert_eq!(policy.digest_algorithm, DigestAlgorithm::Sha512);
assert_eq!(
policy.digest_value,
digest_bytes(DigestAlgorithm::Sha512, b"policy file bytes")?
);
Ok(())
}
#[test]
fn signature_policy_file_reports_read_error() {
let path =
std::env::temp_dir().join(format!("xdoc-policy-{}-missing.pdf", std::process::id()));
let error = SignaturePolicy::from_file(
SignaturePolicyId::Uri("https://example.test/policy.pdf".to_owned()),
DigestAlgorithm::Sha256,
&path,
)
.expect_err("missing policy file must fail");
assert_eq!(error.kind(), &ErrorKind::Signature);
assert!(error
.message()
.contains("cannot read XAdES signature policy document"));
assert!(error.message().contains(&path.display().to_string()));
}
#[test]
fn signature_policy_supports_optional_description() {
let policy = SignaturePolicy::new(
SignaturePolicyId::Uri("urn:example:policy:v1".to_owned()),
DigestAlgorithm::Sha256,
[1, 2, 3],
)
.with_description("Example policy");
assert_eq!(policy.description.as_deref(), Some("Example policy"));
}
#[test]
fn signature_policy_id_supports_oid() {
let id = SignaturePolicyId::Oid("1.2.3.4".to_owned());
assert_eq!(id.value(), "1.2.3.4");
}
#[test]
fn signer_role_keeps_claimed_roles_in_order() {
let role = SignerRole::claimed("reviewer").with_claimed_role("approver");
assert_eq!(role.claimed_roles(), ["reviewer", "approver"]);
assert!(!role.is_empty());
}
}