pub use asx_rs::as4::As4PushPolicy;
pub use asx_rs::as4::FragmentScopePolicy;
use asx_rs::core::InteropMode;
use asx_rs::interop::{
As2ValidationPolicy, BaseProfile, CanonicalizationPolicy, ProfileStack,
ProfileValidationReport, SecurityPolicy, ValidationPolicy,
};
use crate::{
constants,
pmode::{BdewAction, PMode, PModeRegistry, bdew_pmode_with_endpoint},
};
type EncryptionCertPem = std::sync::Arc<[u8]>;
pub const PROFILE_NAME: &str = "bdew_mako_as4";
pub const PROFILE_VERSION: &str = "2.0.0";
pub fn bdew_push_policy(decryption_key_pem: Option<Vec<u8>>) -> As4PushPolicy {
match decryption_key_pem {
Some(key) => As4PushPolicy::regulated_with_decryption_key(key),
None => As4PushPolicy::regulated(),
}
}
pub fn bdew_mako_profile_stack() -> ProfileStack {
ProfileStack {
base: BaseProfile {
name: PROFILE_NAME.to_string(),
version: PROFILE_VERSION.to_string(),
mode: InteropMode::Strict,
canonicalization: CanonicalizationPolicy::default(),
security: SecurityPolicy {
require_signature: true,
require_encryption: true,
},
validation: ValidationPolicy {
reject_ambiguous_headers: true,
enforce_payload_limits: true,
},
security_floor: SecurityPolicy::SIGN_AND_ENCRYPT,
as2_validation: As2ValidationPolicy { require_mic: false },
},
extensions: Vec::new(),
overrides: Vec::new(),
partner_overrides: Vec::new(),
}
}
#[derive(Debug)]
pub struct BdewAs4Profile {
stack: ProfileStack,
registry: PModeRegistry,
encryption_certs: std::collections::HashMap<String, EncryptionCertPem>,
}
impl Default for BdewAs4Profile {
fn default() -> Self {
Self::new()
}
}
impl BdewAs4Profile {
pub fn new() -> Self {
Self {
stack: bdew_mako_profile_stack(),
registry: PModeRegistry::new(),
encryption_certs: std::collections::HashMap::new(),
}
}
pub fn profile_stack(&self) -> &ProfileStack {
&self.stack
}
pub fn registry(&self) -> &PModeRegistry {
&self.registry
}
pub fn register_pmode(&mut self, pmode: PMode) -> &mut Self {
self.registry.register(pmode);
self
}
pub fn register_partner_all_actions(
&mut self,
partner_mp_id: impl Into<String>,
endpoint_url: impl Into<String>,
) -> &mut Self {
let mp_id: String = partner_mp_id.into();
let url: String = endpoint_url.into();
for action in BdewAction::all_standard() {
let action_short = action
.as_uri()
.strip_prefix(constants::SERVICE)
.and_then(|s| s.strip_prefix(':'))
.unwrap_or("unknown")
.to_ascii_lowercase();
let id = format!("pm-{mp_id}-{action_short}");
self.registry
.register(bdew_pmode_with_endpoint(id, &mp_id, action, &url));
}
self
}
pub fn register_partner_encryption_cert(
&mut self,
partner_mp_id: impl Into<String>,
cert_pem: impl Into<Vec<u8>>,
) -> &mut Self {
self.encryption_certs
.insert(partner_mp_id.into(), cert_pem.into().into());
self
}
pub fn get_partner_encryption_cert(&self, partner_mp_id: &str) -> Option<&[u8]> {
self.encryption_certs
.get(partner_mp_id)
.map(|arc| arc.as_ref())
}
pub fn has_any_encryption_certs(&self) -> bool {
!self.encryption_certs.is_empty()
}
pub fn resolve_pmode_by_action(
&self,
partner_mp_id: &str,
action: &BdewAction,
) -> Option<&PMode> {
self.registry
.resolve_by_action(partner_mp_id, &action.as_uri())
}
pub fn all_pmodes(&self) -> &[PMode] {
self.registry.all()
}
pub fn resolve_endpoint(
&self,
partner_mp_id: &str,
service: &str,
action: &str,
) -> Option<&str> {
self.registry
.resolve(partner_mp_id, service, action)
.and_then(|pm| pm.endpoint_url.as_deref())
}
pub fn resolve_pmode(
&self,
partner_mp_id: &str,
service: &str,
action: &str,
) -> Option<&PMode> {
self.registry.resolve(partner_mp_id, service, action)
}
pub fn validate(&self) -> Result<ProfileValidationReport, BdewProfileError> {
Ok(self.stack.validate()?)
}
}
#[derive(Debug, thiserror::Error)]
pub enum BdewProfileError {
#[error(transparent)]
Validation(#[from] asx_rs::interop::ProfileValidationFailure),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants;
use crate::pmode::{BdewAction, bdew_pmode};
#[test]
fn profile_stack_validates_without_errors() {
let stack = bdew_mako_profile_stack();
let report = stack
.validate()
.expect("BDEW base profile must pass validation");
assert!(
report.lints.is_empty(),
"no redundant-override lints expected"
);
}
#[test]
fn profile_stack_name_and_version() {
let stack = bdew_mako_profile_stack();
assert_eq!(stack.base.name, PROFILE_NAME);
assert_eq!(stack.base.version, PROFILE_VERSION);
}
#[test]
fn profile_stack_security_policy() {
let stack = bdew_mako_profile_stack();
assert!(
stack.base.security.require_signature,
"signing must be required"
);
assert!(
stack.base.security.require_encryption,
"encryption must be required — BDEW AS4-Profil v1.2 §2.2.6.2.2"
);
}
#[test]
fn a_partner_overlay_cannot_turn_off_encryption() {
use asx_rs::interop::{
PartnerProfileOverlay, ProfilePolicyOverrides, ProfileValidationCode,
};
let mut profile = BdewAs4Profile::new();
profile.stack.partner_overrides.push(PartnerProfileOverlay {
name: "legacy-partner".to_owned(),
partner_id: "9900000000001".to_owned(),
overrides: ProfilePolicyOverrides {
security: Some(SecurityPolicy {
require_signature: true,
require_encryption: false,
}),
..Default::default()
},
});
let failure = profile
.stack
.validate()
.expect_err("the sign-and-encrypt floor must refuse the downgrade");
assert!(
failure.has_code(ProfileValidationCode::SecurityFloorViolation),
"expected a floor violation, got {failure}"
);
let err = profile
.validate()
.expect_err("and BdewAs4Profile::validate must surface it");
assert!(
err.to_string().contains("floor") || err.to_string().contains("encryption"),
"the refusal should name what was relaxed: {err}"
);
}
#[test]
fn a_layer_cannot_turn_off_signing() {
use asx_rs::interop::{ProfileOverride, ProfilePolicyOverrides, ProfileValidationCode};
let mut profile = BdewAs4Profile::new();
profile.stack.overrides.push(ProfileOverride {
name: "no-signing".to_owned(),
overrides: ProfilePolicyOverrides {
security: Some(SecurityPolicy {
require_signature: false,
require_encryption: true,
}),
..Default::default()
},
});
let failure = profile.stack.validate().expect_err("must refuse");
assert!(
failure.has_code(ProfileValidationCode::SecurityFloorViolation),
"expected a floor violation, got {failure}"
);
assert!(profile.validate().is_err());
}
#[test]
fn the_stack_declares_the_bdew_sign_and_encrypt_floor() {
let stack = bdew_mako_profile_stack();
assert_eq!(
stack.base.security_floor,
SecurityPolicy::SIGN_AND_ENCRYPT,
"BDEW AS4-Profil v1.2 §2.2.6.2.2 requires signing AND encryption"
);
}
#[test]
fn the_base_profile_passes_the_bdew_check() {
let profile = BdewAs4Profile::new();
assert!(profile.validate().is_ok());
}
#[test]
fn profile_stack_mode_is_strict() {
let stack = bdew_mako_profile_stack();
assert_eq!(stack.base.mode, InteropMode::Strict);
}
#[test]
fn profile_stack_no_as2_mic() {
let stack = bdew_mako_profile_stack();
assert!(
!stack.base.as2_validation.require_mic,
"AS2 MIC must not be required in an AS4 profile"
);
}
#[test]
fn bdew_as4_profile_register_and_resolve() {
let mut profile = BdewAs4Profile::new();
profile
.register_pmode(bdew_pmode("pm-u", "9900000000001", BdewAction::Utilmd))
.register_pmode(bdew_pmode("pm-a", "9900000000001", BdewAction::Aperak));
assert_eq!(profile.registry().len(), 2);
let pm = profile.resolve_pmode(
"9900000000001",
constants::SERVICE,
&BdewAction::Utilmd.as_uri(),
);
assert!(pm.is_some());
assert_eq!(pm.unwrap().id, "pm-u");
assert!(
profile
.resolve_pmode(
"9999999999999",
constants::SERVICE,
&BdewAction::Utilmd.as_uri()
)
.is_none()
);
}
#[test]
fn bdew_as4_profile_validates() {
let mut profile = BdewAs4Profile::new();
profile.register_pmode(bdew_pmode("pm-u", "9900000000001", BdewAction::Utilmd));
profile
.validate()
.expect("profile with registered P-Mode must validate");
}
#[test]
fn bdew_as4_profile_default_equals_new() {
let a = BdewAs4Profile::new();
let b = BdewAs4Profile::default();
assert_eq!(a.registry().len(), b.registry().len());
assert_eq!(a.profile_stack().base.name, b.profile_stack().base.name);
}
#[test]
fn resolve_endpoint_returns_url_when_baked_in() {
use crate::pmode::bdew_pmode_with_endpoint;
let mut profile = BdewAs4Profile::new();
profile.register_pmode(bdew_pmode_with_endpoint(
"pm-u",
"9900000000001",
BdewAction::Utilmd,
"https://partner.example/as4",
));
let url = profile.resolve_endpoint(
"9900000000001",
constants::SERVICE,
&BdewAction::Utilmd.as_uri(),
);
assert_eq!(url, Some("https://partner.example/as4"));
}
#[test]
fn resolve_endpoint_returns_none_when_not_set() {
let mut profile = BdewAs4Profile::new();
profile.register_pmode(bdew_pmode("pm-u", "9900000000001", BdewAction::Utilmd));
assert!(
profile
.resolve_endpoint(
"9900000000001",
constants::SERVICE,
&BdewAction::Utilmd.as_uri()
)
.is_none()
);
}
#[test]
fn register_partner_all_actions_creates_one_pmode_per_standard_action() {
use crate::pmode::BdewAction;
let mut profile = BdewAs4Profile::new();
profile.register_partner_all_actions("9900000000001", "https://partner.example/as4/inbox");
assert_eq!(profile.registry().len(), BdewAction::all_standard().len());
for pm in profile.all_pmodes() {
assert_eq!(
pm.endpoint_url.as_deref(),
Some("https://partner.example/as4/inbox"),
);
}
}
#[test]
fn register_partner_all_actions_chaining() {
let mut profile = BdewAs4Profile::new();
profile
.register_partner_all_actions("9900000000001", "https://a.example/as4")
.register_partner_all_actions("9900000000002", "https://b.example/as4");
use crate::pmode::BdewAction;
assert_eq!(
profile.registry().len(),
2 * BdewAction::all_standard().len()
);
}
#[test]
fn resolve_pmode_by_action_finds_registered_pmode() {
use crate::pmode::BdewAction;
let mut profile = BdewAs4Profile::new();
profile.register_partner_all_actions("9900000000001", "https://partner.example/as4/inbox");
let pm = profile.resolve_pmode_by_action("9900000000001", &BdewAction::Utilmd);
assert!(pm.is_some());
assert_eq!(pm.unwrap().partner_id, "9900000000001");
assert_eq!(pm.unwrap().action, BdewAction::Utilmd.as_uri());
}
#[test]
fn resolve_pmode_by_action_returns_none_for_unknown_partner() {
use crate::pmode::BdewAction;
let mut profile = BdewAs4Profile::new();
profile.register_partner_all_actions("9900000000001", "https://partner.example/as4");
assert!(
profile
.resolve_pmode_by_action("9999999999999", &BdewAction::Utilmd)
.is_none()
);
}
#[test]
fn all_pmodes_reflects_registered_pmode_count() {
use crate::pmode::BdewAction;
let mut profile = BdewAs4Profile::new();
assert!(profile.all_pmodes().is_empty());
profile.register_partner_all_actions("9900000000001", "https://a.example/as4");
assert_eq!(profile.all_pmodes().len(), BdewAction::all_standard().len());
}
}