Skip to main content

xml_sec/policy/
profiles.rs

1//! Trusted profile selection and compilation; no configuration discovery or I/O.
2
3use super::*;
4use crate::provider::{CryptoProvider, ProviderCapability};
5
6mod composition;
7mod constraints;
8
9/// XML Security presets, selected only by trusted application code.
10/// These names do not certify full SAML, SOAP or XAdES conformance.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum PolicyProfile {
13    /// Implemented libxmlsec1 mechanisms, with explicit compatibility permission.
14    XmlSecCompatible,
15    /// Modern signatures, SHA-2/SHA-3 digests and authenticated encryption.
16    Modern,
17    /// Restricted SAML signatures and explicit authenticated target coverage.
18    SecureSaml,
19    /// SOAP Security-header signatures and explicit authenticated target coverage.
20    WsSecurity,
21    /// XML signatures with authenticated, typed SignedProperties references.
22    Xades,
23    /// Compatibility verification only, requiring expiring authorization.
24    LegacyVerify,
25}
26
27/// Auditable, caller-authorized exception with an exclusive expiration boundary.
28/// The core does not decide who is entitled to grant exceptions; the trusted
29/// application authenticates that authority before constructing this value.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct PolicyValidity {
32    /// Application's audit record identifier, unrelated to document content.
33    pub authorization_id: String,
34    /// Authenticated principal granting the exception.
35    pub authority: String,
36    /// Non-secret justification retained for audit.
37    pub reason: String,
38    /// Inclusive beginning of validity.
39    pub not_before: SystemTime,
40    /// Exclusive expiration; cached policies cannot outlive this boundary.
41    pub expires_at: SystemTime,
42}
43
44impl PolicyValidity {
45    /// Check metadata and the half-open interval against an explicit time.
46    pub fn validate_at(&self, now: SystemTime) -> Result<(), PolicyViolation> {
47        if self.authorization_id.trim().is_empty()
48            || self.authority.trim().is_empty()
49            || self.reason.trim().is_empty()
50            || self.not_before >= self.expires_at
51        {
52            return Err(configuration(
53                "authorization needs identity, authority, reason and a nonempty validity interval",
54            ));
55        }
56        if now < self.not_before || now >= self.expires_at {
57            return Err(PolicyViolation::AuthorizationExpired);
58        }
59        Ok(())
60    }
61}
62
63/// Signature-level XML Security constraints, not application protocol semantics.
64#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
65pub enum SignatureConstraints {
66    /// Generic XMLDSig processing with caller-selected request requirements.
67    #[default]
68    General,
69    /// SAML Core §5.4 signature format and coverage requirements.
70    Saml,
71    /// WS-Security §8.2 Security-header signature placement and target coverage.
72    WsSecurity,
73    /// ETSI EN 319 132-1 §4.4.2 SignedProperties protection.
74    Xades,
75}
76
77/// Editable compilation input. `None` disables an operation; a restrictive
78/// layer can disable but never re-enable an operation inherited as disabled.
79#[derive(Debug, Clone, Default)]
80pub struct PolicySet {
81    /// XMLDSig verification policy.
82    pub verification: Option<VerificationPolicy>,
83    /// XMLDSig signing policy.
84    pub signing: Option<SigningPolicy>,
85    /// XMLEnc encryption policy.
86    #[cfg(feature = "xmlenc")]
87    pub encryption: Option<EncryptionPolicy>,
88    /// XMLEnc decryption policy.
89    #[cfg(feature = "xmlenc")]
90    pub decryption: Option<DecryptionPolicy>,
91}
92
93/// Caller-owned capabilities promised to operations using the compiled policy.
94/// This is a contract, not permission to discover resources. Actual resource
95/// maps and target identities must still be supplied to each operation.
96pub struct PolicyCompileContext {
97    /// Caller guarantees an explicit external-resource map on applicable calls.
98    pub external_resources: bool,
99    /// Caller uses `verify_request` with nonempty expected target identities.
100    pub target_evidence: bool,
101}
102
103/// Immutable compiled output. Operation snapshots retain all constraints and
104/// authorization metadata when cloned into existing processing contexts.
105#[derive(Debug, Clone)]
106pub struct CompiledPolicies {
107    profile: PolicyProfile,
108    policies: PolicySet,
109}
110
111impl CompiledPolicies {
112    /// Explicit trusted profile identity, not inferred from XML.
113    pub const fn profile(&self) -> PolicyProfile {
114        self.profile
115    }
116    /// Borrow the compiled verification snapshot, if enabled.
117    pub fn verification(&self) -> Option<&VerificationPolicy> {
118        self.policies.verification.as_ref()
119    }
120    /// Borrow the compiled signing snapshot, if enabled.
121    pub fn signing(&self) -> Option<&SigningPolicy> {
122        self.policies.signing.as_ref()
123    }
124    /// Borrow the compiled encryption snapshot, if enabled.
125    #[cfg(feature = "xmlenc")]
126    pub fn encryption(&self) -> Option<&EncryptionPolicy> {
127        self.policies.encryption.as_ref()
128    }
129    /// Borrow the compiled decryption snapshot, if enabled.
130    #[cfg(feature = "xmlenc")]
131    pub fn decryption(&self) -> Option<&DecryptionPolicy> {
132        self.policies.decryption.as_ref()
133    }
134}
135
136/// Compose layers in system → application → tenant order. Every ordinary layer
137/// intersects the previous snapshot; only `authorize_replacement` can relax it.
138pub struct PolicyCompiler<'a> {
139    provider: &'a dyn CryptoProvider,
140    profile: PolicyProfile,
141    policies: PolicySet,
142}
143
144impl<'a> PolicyCompiler<'a> {
145    /// Construct a named profile using this engine's advertised capabilities.
146    /// LegacyVerify requires a valid authorization even before compilation.
147    pub fn new(
148        profile: PolicyProfile,
149        provider: &'a dyn CryptoProvider,
150        authorization: Option<PolicyValidity>,
151    ) -> Result<Self, PolicyViolation> {
152        if profile == PolicyProfile::LegacyVerify && authorization.is_none() {
153            return Err(configuration(
154                "LegacyVerify requires expiring authorization",
155            ));
156        }
157        if let Some(validity) = &authorization {
158            validity.validate_at(SystemTime::now())?;
159        }
160        let policies = profile_policies(profile, provider, authorization);
161        Ok(Self {
162            provider,
163            profile,
164            policies,
165        })
166    }
167
168    /// Intersect another complete layer without granting any new permission.
169    /// `None` operation fields mean disabled, not unspecified inheritance.
170    pub fn restrict(mut self, layer: &PolicySet) -> Result<Self, PolicyViolation> {
171        self.policies = self.policies.intersect(layer)?;
172        Ok(self)
173    }
174
175    /// Explicit relaxation path: replace the effective policy and retain the
176    /// authenticated grant on every enabled operation. Earlier authorization
177    /// lifetimes are intersected, so replacement cannot extend an existing grant.
178    pub fn authorize_replacement(
179        mut self,
180        mut replacement: PolicySet,
181        authorization: PolicyValidity,
182    ) -> Result<Self, PolicyViolation> {
183        authorization.validate_at(SystemTime::now())?;
184        replacement.bind_authorization(authorization, &self.policies)?;
185        self.policies = replacement;
186        Ok(self)
187    }
188
189    /// Compile contradictions before parsing, resolver calls or cryptography.
190    pub fn compile(
191        self,
192        context: &PolicyCompileContext,
193    ) -> Result<CompiledPolicies, PolicyViolation> {
194        self.policies.validate(context, self.provider)?;
195        Ok(CompiledPolicies {
196            profile: self.profile,
197            policies: self.policies,
198        })
199    }
200
201    /// Inspect a layer input for customization before restrictive composition.
202    pub fn policies(&self) -> &PolicySet {
203        &self.policies
204    }
205}
206
207fn configuration(reason: &'static str) -> PolicyViolation {
208    PolicyViolation::Configuration { reason }
209}
210
211fn profile_policies(
212    profile: PolicyProfile,
213    provider: &dyn CryptoProvider,
214    validity: Option<PolicyValidity>,
215) -> PolicySet {
216    let compatibility = matches!(
217        profile,
218        PolicyProfile::XmlSecCompatible | PolicyProfile::LegacyVerify
219    );
220    let constraints = match profile {
221        PolicyProfile::SecureSaml => SignatureConstraints::Saml,
222        PolicyProfile::WsSecurity => SignatureConstraints::WsSecurity,
223        PolicyProfile::Xades => SignatureConstraints::Xades,
224        _ => SignatureConstraints::General,
225    };
226    let signatures: HashSet<_> = SignatureAlgorithm::ALL
227        .into_iter()
228        .filter(|a| provider.supports(ProviderCapability::Verify(*a)))
229        .filter(|a| {
230            compatibility
231                || (a.signing_allowed() && a.hmac_output_bits().is_none_or(|bits| bits >= 256))
232        })
233        .filter(|a| !matches!(a, SignatureAlgorithm::PostQuantum(_)))
234        .collect();
235    let digests: HashSet<_> = DigestAlgorithm::ALL
236        .into_iter()
237        .filter(|a| provider.supports(ProviderCapability::Digest(*a)))
238        .filter(|a| compatibility || (a.signing_allowed() && a.output_len() >= 32))
239        .collect();
240    let transforms = if compatibility {
241        // Signers and verifiers in this profile must agree with libxmlsec1 on the
242        // digested output of XSLT transforms.
243        TransformPolicy {
244            xslt_stylesheet_semantics: XsltStylesheetSemantics::Libxmlsec1,
245            ..TransformPolicy::default()
246        }
247    } else {
248        TransformPolicy {
249            allowed_algorithms: Some(
250                [
251                    "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
252                    "http://www.w3.org/2001/10/xml-exc-c14n#",
253                    "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
254                    "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
255                    "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments",
256                    "http://www.w3.org/2006/12/xml-c14n11",
257                    "http://www.w3.org/2006/12/xml-c14n11#WithComments",
258                ]
259                .into_iter()
260                .map(str::to_owned)
261                .collect(),
262            ),
263            ..TransformPolicy::default()
264        }
265    };
266    let mut verification = VerificationPolicy {
267        validity: validity.clone(),
268        signature_constraints: constraints,
269        signature_algorithms: Some(signatures),
270        parameterized_rsa_pss: if compatibility {
271            RsaPssPermission::AllSupported
272        } else {
273            RsaPssPermission::SecureSupported
274        },
275        digest_algorithms: Some(digests),
276        transforms: transforms.clone(),
277        ..VerificationPolicy::default()
278    };
279    verification.key_trust.parameterized_rsa_pss = verification.parameterized_rsa_pss;
280    if compatibility {
281        verification.key_trust.allowed_legacy_signature_algorithms = verification
282            .signature_algorithms
283            .clone()
284            .unwrap_or_default();
285        verification.key_trust.rsa_keys.minimum_modulus_bits = 1024;
286        verification.key_trust.dsa_keys.minimum_modulus_bits = 1024;
287        verification.hmac.minimum_output_bits = 80;
288        verification.transforms.same_document_id_semantics =
289            SameDocumentIdSemantics::XmlSecBarename;
290    }
291    if constraints != SignatureConstraints::General {
292        verification.key_sources = KeySourcePolicy {
293            preset_key: true,
294            key_name: true,
295            x509_data: true,
296            key_value: false,
297            der_encoded_key_value: false,
298            key_info_reference: false,
299        };
300    }
301    if constraints == SignatureConstraints::Saml {
302        verification.uris.references = UriTypeSet::new(false, true, false);
303        verification.transforms.allowed_algorithms = Some(
304            [
305                "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
306                "http://www.w3.org/2001/10/xml-exc-c14n#",
307                "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
308            ]
309            .into_iter()
310            .map(str::to_owned)
311            .collect(),
312        );
313    }
314    let signing = SigningPolicy {
315        validity: validity.clone(),
316        signature_constraints: constraints,
317        parameterized_rsa_pss: RsaPssPermission::SecureSupported,
318        signature_algorithms: Some(
319            SignatureAlgorithm::ALL
320                .into_iter()
321                .filter(|a| a.signing_allowed() && !matches!(a, SignatureAlgorithm::PostQuantum(_)))
322                .filter(|a| a.hmac_output_bits().is_none_or(|bits| bits >= 256))
323                .filter(|a| provider.supports(ProviderCapability::Sign(*a)))
324                .collect(),
325        ),
326        digest_algorithms: Some(
327            DigestAlgorithm::ALL
328                .into_iter()
329                .filter(|a| a.signing_allowed() && a.output_len() >= 32)
330                .filter(|a| provider.supports(ProviderCapability::Digest(*a)))
331                .collect(),
332        ),
333        transforms: if constraints == SignatureConstraints::Saml {
334            verification.transforms.clone()
335        } else {
336            transforms
337        },
338        uris: if constraints == SignatureConstraints::Saml {
339            verification.uris
340        } else {
341            UriPolicy::default()
342        },
343        ..SigningPolicy::default()
344    };
345    let legacy = profile == PolicyProfile::LegacyVerify;
346    PolicySet {
347        verification: Some(verification),
348        signing: (!legacy).then_some(signing),
349        #[cfg(feature = "xmlenc")]
350        encryption: (!legacy).then(|| encryption_profile(provider, validity.clone())),
351        #[cfg(feature = "xmlenc")]
352        decryption: (!legacy).then(|| decryption_profile(provider, compatibility, validity)),
353    }
354}
355
356#[cfg(feature = "xmlenc")]
357const MODERN_CONTENT_METHODS: [DataEncryptionAlgorithm; 3] = [
358    DataEncryptionAlgorithm::Aes128Gcm,
359    DataEncryptionAlgorithm::Aes256Gcm,
360    DataEncryptionAlgorithm::ChaCha20Poly1305,
361];
362#[cfg(feature = "xmlenc")]
363const MODERN_WRAP_METHODS: [KeyWrapAlgorithm; 2] =
364    [KeyWrapAlgorithm::AesKw128, KeyWrapAlgorithm::AesKw256];
365#[cfg(feature = "xmlenc")]
366const MODERN_OAEP_DIGESTS: [OaepDigestAlgorithm; 3] = [
367    OaepDigestAlgorithm::Sha256,
368    OaepDigestAlgorithm::Sha384,
369    OaepDigestAlgorithm::Sha512,
370];
371
372#[cfg(feature = "xmlenc")]
373const COMPATIBLE_CONTENT_METHODS: &[DataEncryptionAlgorithm] = &[
374    DataEncryptionAlgorithm::Aes128Cbc,
375    DataEncryptionAlgorithm::Aes256Cbc,
376    DataEncryptionAlgorithm::Aes128Gcm,
377    DataEncryptionAlgorithm::Aes256Gcm,
378    DataEncryptionAlgorithm::Camellia128Cbc,
379    DataEncryptionAlgorithm::Camellia192Cbc,
380    DataEncryptionAlgorithm::Camellia256Cbc,
381    DataEncryptionAlgorithm::ChaCha20,
382    DataEncryptionAlgorithm::ChaCha20Poly1305,
383    #[cfg(feature = "legacy-algorithms")]
384    DataEncryptionAlgorithm::TripleDesCbc,
385    #[cfg(feature = "legacy-algorithms")]
386    DataEncryptionAlgorithm::Aes192Cbc,
387    #[cfg(feature = "legacy-algorithms")]
388    DataEncryptionAlgorithm::Aes192Gcm,
389];
390#[cfg(feature = "xmlenc")]
391const COMPATIBLE_WRAP_METHODS: &[KeyWrapAlgorithm] = &[
392    KeyWrapAlgorithm::AesKw128,
393    KeyWrapAlgorithm::AesKw256,
394    KeyWrapAlgorithm::CamelliaKw128,
395    KeyWrapAlgorithm::CamelliaKw192,
396    KeyWrapAlgorithm::CamelliaKw256,
397    #[cfg(feature = "legacy-algorithms")]
398    KeyWrapAlgorithm::TripleDes,
399    #[cfg(feature = "legacy-algorithms")]
400    KeyWrapAlgorithm::AesKw192,
401];
402#[cfg(feature = "xmlenc")]
403const COMPATIBLE_TRANSPORT_METHODS: &[KeyTransportAlgorithm] = &[
404    KeyTransportAlgorithm::RsaOaepMgf1p,
405    KeyTransportAlgorithm::RsaOaep11,
406    #[cfg(feature = "legacy-algorithms")]
407    KeyTransportAlgorithm::RsaPkcs1v15,
408];
409#[cfg(feature = "xmlenc")]
410const OAEP_MGF_DIGESTS: [OaepDigestAlgorithm; 5] = [
411    OaepDigestAlgorithm::Sha1,
412    OaepDigestAlgorithm::Sha224,
413    OaepDigestAlgorithm::Sha256,
414    OaepDigestAlgorithm::Sha384,
415    OaepDigestAlgorithm::Sha512,
416];
417
418#[cfg(feature = "xmlenc")]
419fn encryption_profile(
420    provider: &dyn CryptoProvider,
421    validity: Option<PolicyValidity>,
422) -> EncryptionPolicy {
423    EncryptionPolicy {
424        validity,
425        data_algorithms: Some(
426            MODERN_CONTENT_METHODS
427                .into_iter()
428                .filter(|a| provider.supports(ProviderCapability::Encrypt(*a)))
429                .collect(),
430        ),
431        key_transport_algorithms: Some(HashSet::from([KeyTransportAlgorithm::RsaOaep11])),
432        key_wrap_algorithms: Some(
433            MODERN_WRAP_METHODS
434                .into_iter()
435                .filter(|a| provider.supports(ProviderCapability::KeyWrap(*a)))
436                .collect(),
437        ),
438        oaep_digests: Some(HashSet::from(MODERN_OAEP_DIGESTS)),
439        ..EncryptionPolicy::default()
440    }
441}
442
443#[cfg(feature = "xmlenc")]
444fn decryption_profile(
445    provider: &dyn CryptoProvider,
446    compatibility: bool,
447    validity: Option<PolicyValidity>,
448) -> DecryptionPolicy {
449    let content = if compatibility {
450        COMPATIBLE_CONTENT_METHODS
451    } else {
452        &MODERN_CONTENT_METHODS
453    };
454    let wraps = if compatibility {
455        COMPATIBLE_WRAP_METHODS
456    } else {
457        &MODERN_WRAP_METHODS
458    };
459    let cbc_methods = if compatibility {
460        COMPATIBLE_CONTENT_METHODS
461    } else {
462        &[]
463    };
464    let oaep_digests = if compatibility {
465        DigestAlgorithm::ALL
466            .into_iter()
467            .filter(|digest| provider.supports(ProviderCapability::Digest(*digest)))
468            .filter_map(|digest| OaepDigestAlgorithm::from_uri(digest.uri()))
469            .collect()
470    } else {
471        HashSet::from(MODERN_OAEP_DIGESTS)
472    };
473    let transports = if compatibility {
474        COMPATIBLE_TRANSPORT_METHODS
475            .iter()
476            .copied()
477            .filter(|method| recovery_method_available(provider, *method, &oaep_digests))
478            .collect()
479    } else {
480        HashSet::from([KeyTransportAlgorithm::RsaOaep11])
481    };
482    DecryptionPolicy {
483        validity,
484        data_algorithms: Some(
485            content
486                .iter()
487                .copied()
488                .filter(|algorithm| provider.supports(ProviderCapability::Decrypt(*algorithm)))
489                .collect(),
490        ),
491        key_transport_algorithms: Some(transports),
492        key_wrap_algorithms: Some(
493            wraps
494                .iter()
495                .copied()
496                .chain(
497                    cbc_methods
498                        .iter()
499                        .copied()
500                        .filter(|algorithm| algorithm.cbc_block_len().is_some())
501                        .map(KeyWrapAlgorithm::Cbc),
502                )
503                .filter(|algorithm| provider.supports(ProviderCapability::KeyUnwrap(*algorithm)))
504                .collect(),
505        ),
506        oaep_digests: Some(oaep_digests),
507        ..DecryptionPolicy::default()
508    }
509}
510
511#[cfg(feature = "xmlenc")]
512fn recovery_method_available(
513    provider: &dyn CryptoProvider,
514    method: KeyTransportAlgorithm,
515    digests: &HashSet<OaepDigestAlgorithm>,
516) -> bool {
517    #[cfg(feature = "legacy-algorithms")]
518    if method == KeyTransportAlgorithm::RsaPkcs1v15 {
519        return provider.supports(ProviderCapability::Pkcs1v15Recovery);
520    }
521    // Admission here identifies an available mechanism; compilation still
522    // checks every permitted digest/MGF tuple, never silently dropping a tuple.
523    digests.iter().any(|&digest| {
524        OAEP_MGF_DIGESTS.into_iter().any(|mgf| {
525            digests.contains(&mgf)
526                && provider.supports(ProviderCapability::KeyRecovery(
527                    &crate::xmlenc::RsaOaepParameters {
528                        algorithm: method,
529                        digest,
530                        mgf_digest: mgf,
531                        label: Vec::new(),
532                    },
533                ))
534        })
535    })
536}