Skip to main content

zerodds_security_permissions/
signature.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! S/MIME-Signatur-Trait + Dev-Helper fuer Permissions/Governance-XML.
5//!
6//! Spec §9.4.1.2.1 verlangt, dass Permissions- und Governance-XML
7//! mit der **Permissions-CA** signiert sind. Das Format ist S/MIME
8//! mit PKCS#7/CMS-Envelope — typischer Gebrauch: `openssl cms -sign`.
9//! Der produktive PKCS#7/CMS-Verifier lebt in [`crate::cms`].
10//!
11//! # Was hier definiert ist
12//!
13//! * Trait [`XmlSignatureVerifier`] als Abstraktion ueber den
14//!   Verify-Schritt.
15//! * [`NoOpVerifier`] — explizit dokumentierter Dev-Helper, der die
16//!   Signatur-Pruefung ueberspringt (`SignedPermissionsXml::open` mit
17//!   `NoOpVerifier` ist nur fuer Development und Tests gedacht — produktive
18//!   Anwendungen verwenden `cms::CmsVerifier`).
19//! * [`EnvelopeCheckVerifier`] — formaler Smoke-Verifier, der den
20//!   S/MIME-Envelope auf Plausibilitaet prueft, **ohne** die Signatur
21//!   wirklich zu pruefen. Auch Dev-Helper.
22//! * [`open_signed_permissions`] kapselt den "erst verifizieren, dann
23//!   parsen"-Flow.
24
25use alloc::string::String;
26use alloc::vec::Vec;
27
28use crate::xml::{Permissions, PermissionsError, parse_permissions_xml};
29
30/// Abstraktion fuer den S/MIME-Verify-Schritt.
31pub trait XmlSignatureVerifier {
32    /// Prueft die Signatur einer Permissions- oder Governance-XML.
33    ///
34    /// `signed_doc` ist der **rohe S/MIME-Container** (inklusive
35    /// PEM-Headers wie `-----BEGIN PKCS7-----`). Der Verifier
36    /// extrahiert den inneren XML-Content und verifiziert die
37    /// Signatur gegen die Permissions-CA.
38    ///
39    /// Rueckgabe: die **verifizierten** inneren XML-Bytes fuer
40    /// nachgelagertes Parsing.
41    ///
42    /// # Errors
43    /// Implementierung-spezifisch; tendiert nach
44    /// `PermissionsError::Malformed` bei Signatur- oder
45    /// Format-Problemen.
46    fn verify_and_extract(&self, signed_doc: &[u8]) -> Result<Vec<u8>, PermissionsError>;
47}
48
49/// No-op-Verifier fuer Development — akzeptiert **jedes** Input als
50/// gueltig und behandelt es als Klartext-XML. **NIE** in Produktion
51/// einsetzen.
52pub struct NoOpVerifier;
53
54impl XmlSignatureVerifier for NoOpVerifier {
55    fn verify_and_extract(&self, signed_doc: &[u8]) -> Result<Vec<u8>, PermissionsError> {
56        Ok(signed_doc.to_vec())
57    }
58}
59
60/// Simple-Envelope-Verifier fuer Tests und Pseudo-Signatur.
61///
62/// Erwartet ein Wrapper-Format `-----BEGIN SIGNED-XML-----\n<XML>\n-----END SIGNED-XML-----`
63/// und extrahiert den XML-Block. Der Signatur-Teil ist hier nur die
64/// Envelope-Praesenz (kein echter Crypto-Check) — der Zweck ist End-
65/// to-End-Tests der Verifier-Aufruf-Kette.
66pub struct EnvelopeCheckVerifier;
67
68impl XmlSignatureVerifier for EnvelopeCheckVerifier {
69    fn verify_and_extract(&self, signed_doc: &[u8]) -> Result<Vec<u8>, PermissionsError> {
70        const BEGIN: &str = "-----BEGIN SIGNED-XML-----\n";
71        const END: &str = "\n-----END SIGNED-XML-----";
72        let s = core::str::from_utf8(signed_doc)
73            .map_err(|_| PermissionsError::Malformed("signed-xml ist kein UTF-8".into()))?;
74        let body = s
75            .strip_prefix(BEGIN)
76            .and_then(|rest| rest.strip_suffix(END))
77            .ok_or_else(|| {
78                PermissionsError::Malformed(String::from(
79                    "signed-xml: envelope BEGIN/END fehlt oder ist fehlerhaft",
80                ))
81            })?;
82        Ok(body.as_bytes().to_vec())
83    }
84}
85
86/// High-Level-Wrapper: verifiziert Signatur, parst Permissions-XML.
87///
88/// # Errors
89/// Signatur- oder XML-Parse-Fehler.
90pub fn open_signed_permissions<V: XmlSignatureVerifier>(
91    signed_doc: &[u8],
92    verifier: &V,
93) -> Result<Permissions, PermissionsError> {
94    let inner = verifier.verify_and_extract(signed_doc)?;
95    let xml = core::str::from_utf8(&inner)
96        .map_err(|_| PermissionsError::Malformed("verified XML ist kein UTF-8".into()))?;
97    parse_permissions_xml(xml)
98}
99
100#[cfg(test)]
101#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
102mod tests {
103    use super::*;
104
105    const RAW_XML: &str = r#"
106<permissions>
107  <grant><subject_name>CN=alice</subject_name>
108    <allow_rule><publish><topic>T</topic></publish></allow_rule>
109  </grant>
110</permissions>
111"#;
112
113    #[test]
114    fn noop_verifier_passes_through() {
115        let perms = open_signed_permissions(RAW_XML.as_bytes(), &NoOpVerifier).unwrap();
116        assert_eq!(perms.grants.len(), 1);
117    }
118
119    #[test]
120    fn envelope_verifier_extracts_inner_xml() {
121        let wrapped =
122            alloc::format!("-----BEGIN SIGNED-XML-----\n{RAW_XML}\n-----END SIGNED-XML-----");
123        let perms = open_signed_permissions(wrapped.as_bytes(), &EnvelopeCheckVerifier).unwrap();
124        assert_eq!(perms.grants.len(), 1);
125        assert_eq!(perms.grants[0].subject_name, "CN=alice");
126    }
127
128    #[test]
129    fn envelope_verifier_rejects_missing_begin() {
130        let bad = b"no envelope here";
131        let err = open_signed_permissions(bad, &EnvelopeCheckVerifier).unwrap_err();
132        assert!(matches!(err, PermissionsError::Malformed(_)));
133    }
134
135    #[test]
136    fn envelope_verifier_rejects_missing_end() {
137        let bad = b"-----BEGIN SIGNED-XML-----\n<permissions/>\n";
138        let err = open_signed_permissions(bad, &EnvelopeCheckVerifier).unwrap_err();
139        assert!(matches!(err, PermissionsError::Malformed(_)));
140    }
141
142    #[test]
143    fn verifier_failure_propagates_malformed() {
144        struct AlwaysFail;
145        impl XmlSignatureVerifier for AlwaysFail {
146            fn verify_and_extract(&self, _doc: &[u8]) -> Result<Vec<u8>, PermissionsError> {
147                Err(PermissionsError::Malformed("signature mismatch".into()))
148            }
149        }
150        let err = open_signed_permissions(RAW_XML.as_bytes(), &AlwaysFail).unwrap_err();
151        assert!(matches!(err, PermissionsError::Malformed(m) if m.contains("mismatch")));
152    }
153
154    #[test]
155    fn non_utf8_inner_is_rejected() {
156        struct BinaryVerifier;
157        impl XmlSignatureVerifier for BinaryVerifier {
158            fn verify_and_extract(&self, _doc: &[u8]) -> Result<Vec<u8>, PermissionsError> {
159                Ok(vec![0xff, 0xfe, 0x00]) // not UTF-8
160            }
161        }
162        let err = open_signed_permissions(b"", &BinaryVerifier).unwrap_err();
163        assert!(matches!(err, PermissionsError::Malformed(_)));
164    }
165}