use super::pmode::PayloadPackagingMode;
use crate::core::InteropMode;
use crate::core::{AsxError, ErrorCode, ErrorContext, Result};
use crate::crypto::soap_builder::WsAddressingHeaders;
use crate::crypto::wssec::{
WsSecOutboundKeyInfoProfile, WsSecSignatureReference, XmlEncPayloadAlgorithm,
};
use crate::interop::InteropExceptionPolicy;
use crate::lifecycle::DomainReady;
use crate::reliability::{DeliveryOutcome, RetryDecision};
use crate::sbdh::SbdhHeader;
use std::sync::Arc;
use zeroize::Zeroize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SoapEnvelope {
pub action: String,
pub body: Arc<[u8]>,
}
impl SoapEnvelope {
#[inline]
pub fn body_bytes(&self) -> bytes::Bytes {
bytes::Bytes::copy_from_slice(&self.body)
}
#[inline]
pub fn into_body_bytes(self) -> bytes::Bytes {
bytes::Bytes::from(self.body.to_vec())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum FragmentScopePolicy {
#[default]
RequireAuthenticatedScope,
UseSoapSenderId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4PushPolicy {
pub interop: InteropMode,
pub interop_exceptions: InteropExceptionPolicy,
pub require_signed_receipt: bool,
#[doc(hidden)]
pub(crate) require_signed_push: bool,
pub fail_closed_audit_events: bool,
pub inbound_decryption_key_pem: Option<Arc<[u8]>>,
pub timestamp_freshness_window: Option<std::time::Duration>,
pub fragment_scope_policy: FragmentScopePolicy,
}
impl Default for As4PushPolicy {
fn default() -> Self {
Self {
interop: InteropMode::Strict,
interop_exceptions: InteropExceptionPolicy::default(),
require_signed_receipt: true,
require_signed_push: true,
fail_closed_audit_events: true,
inbound_decryption_key_pem: None,
timestamp_freshness_window: Some(std::time::Duration::from_secs(300)),
fragment_scope_policy: FragmentScopePolicy::RequireAuthenticatedScope,
}
}
}
impl As4PushPolicy {
pub fn strict() -> Self {
Self::default()
}
pub fn regulated() -> Self {
Self {
interop: InteropMode::Strict,
interop_exceptions: InteropExceptionPolicy::default(),
require_signed_receipt: true,
require_signed_push: true,
fail_closed_audit_events: true,
inbound_decryption_key_pem: None,
timestamp_freshness_window: Some(std::time::Duration::from_secs(300)),
fragment_scope_policy: FragmentScopePolicy::RequireAuthenticatedScope,
}
}
pub fn require_signed_push(&self) -> bool {
self.require_signed_push
}
#[cfg(all(feature = "testing", feature = "interop-relaxed"))]
pub fn test_relaxed() -> Self {
Self {
interop: InteropMode::Relaxed,
require_signed_push: false,
require_signed_receipt: false,
fail_closed_audit_events: false,
timestamp_freshness_window: None,
fragment_scope_policy: FragmentScopePolicy::UseSoapSenderId,
..Self::default()
}
}
}
fn validate_strict_as4_policy_consistency(
stage: &'static str,
interop: InteropMode,
interop_exceptions: &InteropExceptionPolicy,
) -> Result<()> {
if interop == InteropMode::Strict
&& (interop_exceptions.scoped_profile_name.is_some()
|| !interop_exceptions.allowed.is_empty())
{
return Err(AsxError::new(
ErrorCode::InvalidInput,
"strict AS4 policy forbids configured interop exception overrides",
ErrorContext::new(stage),
));
}
Ok(())
}
fn validate_strict_as4_send_policy_consistency(
stage: &'static str,
interop: InteropMode,
sign: bool,
fail_closed_audit_events: bool,
payload_packaging_mode: PayloadPackagingMode,
) -> Result<()> {
if interop == InteropMode::Strict {
if !sign {
return Err(AsxError::new(
ErrorCode::InvalidInput,
"strict AS4 send policy forbids sign=false",
ErrorContext::new(stage),
));
}
if payload_packaging_mode != PayloadPackagingMode::MimeAttachment {
return Err(AsxError::new(
ErrorCode::InvalidInput,
"strict AS4 send policy requires MIME attachment payload packaging",
ErrorContext::new(stage),
));
}
}
#[cfg(not(feature = "testing"))]
if interop == InteropMode::Strict && !fail_closed_audit_events {
return Err(AsxError::new(
ErrorCode::InvalidInput,
"strict AS4 send policy requires fail_closed_audit_events=true in non-testing builds",
ErrorContext::new(stage),
));
}
#[cfg(feature = "testing")]
let _ = fail_closed_audit_events;
Ok(())
}
pub(crate) fn validate_as4_send_policy_and_credentials_consistency(
stage: &'static str,
policy: &As4SendPolicy,
credentials: &As4SendCredentials,
error_code: ErrorCode,
) -> Result<()> {
let ctx = || ErrorContext::new(stage);
if policy.action.trim().is_empty() {
return Err(AsxError::new(
error_code,
"As4SendPolicy.action must not be empty",
ctx(),
));
}
if policy.service.trim().is_empty() {
return Err(AsxError::new(
error_code,
"As4SendPolicy.service must not be empty",
ctx(),
));
}
if let Some(ref id) = policy.ref_to_message_id
&& id.trim().is_empty()
{
return Err(AsxError::new(
error_code,
"As4SendPolicy.ref_to_message_id must not be empty when set",
ctx(),
));
}
if let Some(ref conversation_id) = policy.conversation_id
&& conversation_id.trim().is_empty()
{
return Err(AsxError::new(
error_code,
"As4SendPolicy.conversation_id must not be empty when set",
ctx(),
));
}
if policy.sign {
if credentials.signing_cert_pem.is_none() {
return Err(AsxError::new(
error_code,
"sign = true requires signing_cert_pem",
ctx(),
));
}
if credentials.signing_key_pem.is_none() {
return Err(AsxError::new(
error_code,
"sign = true requires signing_key_pem",
ctx(),
));
}
}
if (policy.encrypt || policy.encrypt_soap_headers) && credentials.recipient_cert_pem.is_none() {
return Err(AsxError::new(
error_code,
"encrypt = true or encrypt_soap_headers = true requires recipient_cert_pem",
ctx(),
));
}
Ok(())
}
#[cfg(feature = "as4")]
#[derive(Debug, Clone)]
pub struct As4PreparedSendCredentials {
pub signing_cert: Option<openssl::x509::X509>,
pub signing_key: Option<openssl::pkey::PKey<openssl::pkey::Private>>,
pub recipient_cert: Option<openssl::x509::X509>,
}
impl As4SendCredentials {
#[cfg(feature = "as4")]
pub fn prepare_for_policy(
&self,
policy: &As4SendPolicy,
stage: &'static str,
error_code: ErrorCode,
) -> Result<As4PreparedSendCredentials> {
let ctx = || ErrorContext::new(stage);
let mut prepared = As4PreparedSendCredentials {
signing_cert: None,
signing_key: None,
recipient_cert: None,
};
if policy.sign {
let cert_pem = self.signing_cert_pem.as_ref().ok_or_else(|| {
AsxError::new(error_code, "sign = true requires signing_cert_pem", ctx())
})?;
let key_pem = self.signing_key_pem.as_ref().ok_or_else(|| {
AsxError::new(error_code, "sign = true requires signing_key_pem", ctx())
})?;
let signing_cert = openssl::x509::X509::from_pem(cert_pem).map_err(|_err| {
AsxError::new(
error_code,
"signing_cert_pem is not a valid PEM X.509 certificate",
ctx(),
)
})?;
let signing_key = openssl::pkey::PKey::private_key_from_pem(key_pem).map_err(|_err| {
AsxError::new(
error_code,
"signing_key_pem is not a valid PEM private key (check PEM format and key type)",
ctx(),
)
})?;
let signing_cert_public = signing_cert.public_key().map_err(|_err| {
AsxError::new(
error_code,
"signing_cert_pem does not contain a usable public key",
ctx(),
)
})?;
if !signing_key.public_eq(&signing_cert_public) {
return Err(AsxError::new(
error_code,
"signing_cert_pem does not match signing_key_pem",
ctx(),
));
}
prepared.signing_cert = Some(signing_cert);
prepared.signing_key = Some(signing_key);
}
if policy.encrypt {
let cert_pem = self.recipient_cert_pem.as_ref().ok_or_else(|| {
AsxError::new(
error_code,
"encrypt = true requires recipient_cert_pem",
ctx(),
)
})?;
let recipient_cert = openssl::x509::X509::from_pem(cert_pem).map_err(|_err| {
AsxError::new(
error_code,
"recipient_cert_pem is not a valid PEM X.509 certificate",
ctx(),
)
})?;
prepared.recipient_cert = Some(recipient_cert);
} else if let Some(cert_pem) = &self.recipient_cert_pem {
let recipient_cert = openssl::x509::X509::from_pem(cert_pem).map_err(|_err| {
AsxError::new(
error_code,
"recipient_cert_pem is not a valid PEM X.509 certificate",
ctx(),
)
})?;
prepared.recipient_cert = Some(recipient_cert);
}
Ok(prepared)
}
}
fn validate_strict_as4_receive_policy_consistency(
stage: &'static str,
interop: InteropMode,
require_signed_receipt: bool,
fail_closed_audit_events: bool,
) -> Result<()> {
if interop == InteropMode::Strict && !require_signed_receipt {
return Err(AsxError::new(
ErrorCode::InvalidInput,
"strict AS4 receive policy requires require_signed_receipt=true",
ErrorContext::new(stage),
));
}
#[cfg(not(feature = "testing"))]
if interop == InteropMode::Strict && !fail_closed_audit_events {
return Err(AsxError::new(
ErrorCode::InvalidInput,
"strict AS4 receive policy requires fail_closed_audit_events=true in non-testing builds",
ErrorContext::new(stage),
));
}
#[cfg(feature = "testing")]
let _ = fail_closed_audit_events;
Ok(())
}
#[derive(Default)]
pub struct As4PushPolicyBuilder(As4PushPolicy);
impl As4PushPolicyBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn interop(mut self, mode: InteropMode) -> Self {
self.0.interop = mode;
self
}
pub fn interop_exceptions(mut self, exc: InteropExceptionPolicy) -> Self {
self.0.interop_exceptions = exc;
self
}
pub fn require_signed_receipt(mut self, v: bool) -> Self {
self.0.require_signed_receipt = v;
self
}
#[cfg(feature = "testing")]
pub fn allow_unsigned_push(mut self, allow: bool) -> Self {
self.0.require_signed_push = !allow;
self
}
pub fn fail_closed_audit_events(mut self, v: bool) -> Self {
self.0.fail_closed_audit_events = v;
self
}
pub fn inbound_decryption_key_pem(mut self, pem: Vec<u8>) -> Self {
self.0.inbound_decryption_key_pem = Some(Arc::from(pem));
self
}
pub fn timestamp_freshness_window(mut self, window: Option<std::time::Duration>) -> Self {
self.0.timestamp_freshness_window = window;
self
}
pub fn fragment_scope_policy(mut self, policy: FragmentScopePolicy) -> Self {
self.0.fragment_scope_policy = policy;
self
}
pub fn build(self) -> Result<As4PushPolicy> {
let stage = "as4_push_policy_build";
validate_strict_as4_policy_consistency(stage, self.0.interop, &self.0.interop_exceptions)?;
validate_strict_as4_receive_policy_consistency(
stage,
self.0.interop,
self.0.require_signed_receipt,
self.0.fail_closed_audit_events,
)?;
if let Some(ref pem) = self.0.inbound_decryption_key_pem {
openssl::pkey::PKey::private_key_from_pem(pem.as_ref()).map_err(|_err| {
AsxError::new(
ErrorCode::InvalidInput,
"inbound_decryption_key_pem is not a valid PEM private key (check PEM format and key type)",
ErrorContext::new(stage),
)
})?;
}
Ok(self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4SendPolicy {
pub interop: InteropMode,
pub outbound_key_info_profile: WsSecOutboundKeyInfoProfile,
pub fail_closed_audit_events: bool,
pub sign: bool,
pub encrypt: bool,
pub outbound_xmlenc_payload_algorithm: XmlEncPayloadAlgorithm,
pub encrypt_soap_headers: bool,
pub compress: bool,
pub action: String,
pub service: String,
pub service_type: String,
pub ref_to_message_id: Option<String>,
pub original_sender: Option<String>,
pub final_recipient: Option<String>,
pub tracking_identifier: Option<String>,
pub conversation_id: Option<String>,
pub ws_addressing: Option<WsAddressingHeaders>,
pub sbdh_header: Option<SbdhHeader>,
pub payload_packaging_mode: PayloadPackagingMode,
}
impl Default for As4SendPolicy {
fn default() -> Self {
Self {
interop: InteropMode::Strict,
outbound_key_info_profile: WsSecOutboundKeyInfoProfile::X509DataAndRsaKeyValue,
fail_closed_audit_events: true,
sign: true,
encrypt: false,
outbound_xmlenc_payload_algorithm: XmlEncPayloadAlgorithm::Aes128Gcm,
encrypt_soap_headers: false,
compress: false,
action: "urn:example:action".into(),
service: "http://example.org/example".into(),
service_type: "example".into(),
ref_to_message_id: None,
original_sender: None,
final_recipient: None,
tracking_identifier: None,
conversation_id: None,
ws_addressing: None,
sbdh_header: None,
payload_packaging_mode: PayloadPackagingMode::default(),
}
}
}
impl As4SendPolicy {
pub fn strict() -> Self {
Self::default()
}
pub fn regulated() -> Self {
Self {
interop: InteropMode::Strict,
outbound_key_info_profile: WsSecOutboundKeyInfoProfile::X509DataAndRsaKeyValue,
fail_closed_audit_events: true,
sign: true,
encrypt: false,
outbound_xmlenc_payload_algorithm: XmlEncPayloadAlgorithm::Aes128Gcm,
encrypt_soap_headers: false,
compress: false,
action: "urn:example:action".into(),
service: "http://example.org/example".into(),
service_type: "example".into(),
ref_to_message_id: None,
original_sender: None,
final_recipient: None,
tracking_identifier: None,
conversation_id: None,
ws_addressing: None,
sbdh_header: None,
payload_packaging_mode: PayloadPackagingMode::default(),
}
}
#[cfg(all(feature = "testing", feature = "interop-relaxed"))]
pub fn test_relaxed() -> Self {
Self {
interop: InteropMode::Relaxed,
fail_closed_audit_events: false,
sign: false,
..Self::default()
}
}
}
#[derive(Default)]
pub struct As4SendPolicyBuilder {
policy: As4SendPolicy,
credentials: As4SendCredentials,
}
impl As4SendPolicyBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn interop(mut self, mode: InteropMode) -> Self {
self.policy.interop = mode;
self
}
pub fn outbound_key_info_profile(mut self, profile: WsSecOutboundKeyInfoProfile) -> Self {
self.policy.outbound_key_info_profile = profile;
self
}
pub fn fail_closed_audit_events(mut self, v: bool) -> Self {
self.policy.fail_closed_audit_events = v;
self
}
pub fn payload_packaging_mode(mut self, mode: PayloadPackagingMode) -> Self {
self.policy.payload_packaging_mode = mode;
self
}
pub fn sign(mut self, v: bool) -> Self {
self.policy.sign = v;
self
}
pub fn encrypt(mut self, v: bool) -> Self {
self.policy.encrypt = v;
self
}
pub fn outbound_xmlenc_payload_algorithm(mut self, v: XmlEncPayloadAlgorithm) -> Self {
self.policy.outbound_xmlenc_payload_algorithm = v;
self
}
pub fn encrypt_soap_headers(mut self, v: bool) -> Self {
self.policy.encrypt_soap_headers = v;
self
}
pub fn compress(mut self, v: bool) -> Self {
self.policy.compress = v;
self
}
pub fn action(mut self, action: impl Into<String>) -> Self {
self.policy.action = action.into();
self
}
pub fn service(mut self, service: impl Into<String>, service_type: impl Into<String>) -> Self {
self.policy.service = service.into();
self.policy.service_type = service_type.into();
self
}
pub fn ref_to_message_id(mut self, id: impl Into<String>) -> Self {
self.policy.ref_to_message_id = Some(id.into());
self
}
pub fn original_sender(mut self, value: impl Into<String>) -> Self {
self.policy.original_sender = Some(value.into());
self
}
pub fn final_recipient(mut self, value: impl Into<String>) -> Self {
self.policy.final_recipient = Some(value.into());
self
}
pub fn tracking_identifier(mut self, value: impl Into<String>) -> Self {
self.policy.tracking_identifier = Some(value.into());
self
}
pub fn conversation_id(mut self, value: impl Into<String>) -> Self {
self.policy.conversation_id = Some(value.into());
self
}
pub fn sbdh_header(mut self, header: SbdhHeader) -> Self {
self.policy.sbdh_header = Some(header);
self
}
pub fn ws_addressing(mut self, headers: WsAddressingHeaders) -> Self {
self.policy.ws_addressing = Some(headers);
self
}
pub fn signing_cert_pem(mut self, pem: Vec<u8>) -> Self {
self.credentials.signing_cert_pem = Some(pem);
self
}
pub fn signing_key_pem(mut self, pem: Vec<u8>) -> Self {
self.credentials.signing_key_pem = Some(pem);
self
}
pub fn recipient_cert_pem(mut self, pem: Vec<u8>) -> Self {
self.credentials.recipient_cert_pem = Some(pem);
self
}
pub fn build(self) -> Result<(As4SendPolicy, As4SendCredentials)> {
let stage = "as4_send_policy_build";
validate_strict_as4_send_policy_consistency(
stage,
self.policy.interop,
self.policy.sign,
self.policy.fail_closed_audit_events,
self.policy.payload_packaging_mode,
)?;
validate_as4_send_policy_and_credentials_consistency(
stage,
&self.policy,
&self.credentials,
ErrorCode::InvalidInput,
)?;
#[cfg(feature = "as4")]
self.credentials
.prepare_for_policy(&self.policy, stage, ErrorCode::InvalidInput)?;
Ok((self.policy, self.credentials))
}
}
#[derive(Debug, Clone, Default)]
pub struct As4SendCredentials {
pub signing_cert_pem: Option<Vec<u8>>,
pub signing_key_pem: Option<Vec<u8>>,
pub recipient_cert_pem: Option<Vec<u8>>,
}
impl Drop for As4SendCredentials {
fn drop(&mut self) {
if let Some(key) = self.signing_key_pem.as_mut() {
key.zeroize();
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4SendOutput {
pub message_id: String,
pub action: String,
pub traceparent: Option<String>,
pub http_content_type: String,
pub soap_envelope: SoapEnvelope,
pub ref_to_message_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4ReceivePushRequest {
pub http_content_type: String,
pub payload: Arc<[u8]>,
pub receipt_payload: Option<Vec<u8>>,
pub policy: As4PushPolicy,
pub authenticated_sender_scope: Option<Arc<str>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedWsAddressingHeaders {
pub message_id: Option<String>,
pub action: Option<String>,
pub to: Option<String>,
pub reply_to: Option<String>,
}
impl ParsedWsAddressingHeaders {
#[inline]
pub fn is_cef_conformant(&self) -> bool {
self.message_id.is_some() && self.action.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedAs4UserMessage {
pub message_id: String,
pub action: String,
pub from_party_ids: Vec<String>,
pub to_party_ids: Vec<String>,
pub mpc: Option<String>,
pub conversation_id: Option<String>,
pub has_ws_security_header: bool,
pub service: Option<String>,
pub ref_to_message_id: Option<String>,
pub original_sender: Option<String>,
pub final_recipient: Option<String>,
pub tracking_identifier: Option<String>,
pub timestamp: Option<String>,
pub wsa_headers: Option<ParsedWsAddressingHeaders>,
}
impl ParsedAs4UserMessage {
#[inline]
pub fn from_party_id(&self) -> &str {
&self.from_party_ids[0]
}
#[inline]
pub fn to_party_id(&self) -> &str {
&self.to_party_ids[0]
}
pub fn check_timestamp_freshness(
&self,
window: std::time::Duration,
) -> crate::core::Result<()> {
use crate::core::{AsxError, ErrorCode, ErrorContext};
let ts_str = match &self.timestamp {
Some(s) => s,
None => return Ok(()),
};
let ts = chrono::DateTime::parse_from_rfc3339(ts_str).map_err(|_| {
AsxError::new(
ErrorCode::ParseFailed,
format!("eb:Timestamp value '{ts_str}' is not a valid RFC 3339 timestamp"),
ErrorContext::new("as4_timestamp_freshness"),
)
})?;
let now = chrono::Utc::now();
let ts_utc: chrono::DateTime<chrono::Utc> = ts.into();
let delta = (now - ts_utc).abs();
let window_chrono =
chrono::Duration::from_std(window).unwrap_or(chrono::Duration::seconds(300));
if delta > window_chrono {
return Err(AsxError::new(
ErrorCode::SecurityVerificationFailed,
format!(
"eb:Timestamp is outside the freshness window (delta={:.0}s, allowed={}s); \
message rejected to prevent replay",
delta.num_milliseconds() as f64 / 1000.0,
window.as_secs(),
),
ErrorContext::new("as4_timestamp_freshness")
.with_message_id(self.message_id.clone()),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedAs4Receipt {
pub ref_to_message_id: String,
pub is_signed: bool,
pub has_non_repudiation_info: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4ReceivePushOutput {
pub payload: DomainReady<Arc<[u8]>>,
pub sbdh_header: Option<SbdhHeader>,
pub user_message: ParsedAs4UserMessage,
pub receipt: Option<ParsedAs4Receipt>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum As4ReceivePushProgress {
PendingFragment {
group_id: String,
received_fragments: usize,
expected_fragments: Option<usize>,
},
Complete(Box<As4ReceivePushOutput>),
Duplicate {
message_id: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum As4ReceiveOutcome {
FirstSeen(Box<As4ReceivePushOutput>),
Duplicate { message_id: String },
}
impl As4ReceiveOutcome {
#[inline]
pub fn is_first_seen(&self) -> bool {
matches!(self, Self::FirstSeen(_))
}
#[inline]
pub fn is_duplicate(&self) -> bool {
matches!(self, Self::Duplicate { .. })
}
#[inline]
pub fn into_output(self) -> Option<As4ReceivePushOutput> {
match self {
Self::FirstSeen(output) => Some(*output),
Self::Duplicate { .. } => None,
}
}
#[inline]
pub fn unwrap_output(self) -> As4ReceivePushOutput {
match self {
Self::FirstSeen(output) => *output,
Self::Duplicate { ref message_id } => {
panic!("called unwrap_output on a Duplicate (message_id={message_id})")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4QueuedPullMessage {
pub message_id: Arc<str>,
pub http_content_type: Arc<str>,
pub payload: Arc<[u8]>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4NriReference {
pub uri: String,
pub digest_method_uri: String,
pub digest_value_b64: String,
}
impl From<&WsSecSignatureReference> for As4NriReference {
fn from(r: &WsSecSignatureReference) -> Self {
Self {
uri: r.uri.clone(),
digest_method_uri: r.digest_method.algorithm_uri().to_string(),
digest_value_b64: r.digest_value_base64.clone(),
}
}
}
impl From<WsSecSignatureReference> for As4NriReference {
fn from(r: WsSecSignatureReference) -> Self {
Self {
uri: r.uri,
digest_method_uri: r.digest_method.algorithm_uri().to_string(),
digest_value_b64: r.digest_value_base64,
}
}
}
#[derive(Debug, Clone)]
pub struct As4PullRequestCredentials {
pub signing_key_pem: Vec<u8>,
pub signing_cert_pem: Vec<u8>,
pub key_info_profile: WsSecOutboundKeyInfoProfile,
}
impl Drop for As4PullRequestCredentials {
fn drop(&mut self) {
self.signing_key_pem.zeroize();
}
}
#[derive(Debug, Clone)]
pub struct As4GeneratePullRequestPolicy {
pub mpc: String,
pub message_id: String,
pub credentials: Option<As4PullRequestCredentials>,
pub authorization_info: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4PullPolicy {
pub interop: InteropMode,
pub mpc: String,
pub interop_exceptions: InteropExceptionPolicy,
pub require_signed_receipt: bool,
#[doc(hidden)]
pub(crate) require_signed_push: bool,
pub fail_closed_audit_events: bool,
pub expected_authorization_info: Option<String>,
}
const DEFAULT_MPC: &str =
"http://docs.oasis-open.org/ebxml-msg/ebms/v3.0/ns/core/200704/defaultMPC";
impl Default for As4PullPolicy {
fn default() -> Self {
Self {
interop: InteropMode::Strict,
mpc: String::from(DEFAULT_MPC),
interop_exceptions: InteropExceptionPolicy::default(),
require_signed_receipt: true,
require_signed_push: true,
fail_closed_audit_events: true,
expected_authorization_info: None,
}
}
}
impl As4PullPolicy {
pub fn strict() -> Self {
Self::default()
}
pub fn regulated() -> Self {
Self {
interop: InteropMode::Strict,
mpc: String::from(DEFAULT_MPC),
interop_exceptions: InteropExceptionPolicy::default(),
require_signed_receipt: true,
require_signed_push: true,
fail_closed_audit_events: true,
expected_authorization_info: None,
}
}
pub fn require_signed_push(&self) -> bool {
self.require_signed_push
}
}
#[derive(Default)]
pub struct As4PullPolicyBuilder(As4PullPolicy);
impl As4PullPolicyBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn interop(mut self, mode: InteropMode) -> Self {
self.0.interop = mode;
self
}
pub fn mpc(mut self, mpc: impl Into<String>) -> Self {
self.0.mpc = mpc.into();
self
}
pub fn interop_exceptions(mut self, exc: InteropExceptionPolicy) -> Self {
self.0.interop_exceptions = exc;
self
}
pub fn require_signed_receipt(mut self, v: bool) -> Self {
self.0.require_signed_receipt = v;
self
}
#[cfg(feature = "testing")]
pub fn allow_unsigned_push(mut self, allow: bool) -> Self {
self.0.require_signed_push = !allow;
self
}
pub fn fail_closed_audit_events(mut self, v: bool) -> Self {
self.0.fail_closed_audit_events = v;
self
}
pub fn expected_authorization_info(mut self, auth: Option<String>) -> Self {
self.0.expected_authorization_info = auth;
self
}
pub fn build(self) -> Result<As4PullPolicy> {
let stage = "as4_pull_policy_build";
validate_strict_as4_policy_consistency(stage, self.0.interop, &self.0.interop_exceptions)?;
validate_strict_as4_receive_policy_consistency(
stage,
self.0.interop,
self.0.require_signed_receipt,
self.0.fail_closed_audit_events,
)?;
if self.0.mpc.trim().is_empty() {
return Err(AsxError::new(
ErrorCode::InvalidInput,
"As4PullPolicy.mpc must not be empty",
ErrorContext::new(stage),
));
}
if let Some(ref auth) = self.0.expected_authorization_info
&& auth.trim().is_empty()
{
return Err(AsxError::new(
ErrorCode::InvalidInput,
"As4PullPolicy.expected_authorization_info must not be empty when set",
ErrorContext::new(stage),
));
}
Ok(self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4ReceivePullRequest {
pub pull_message_id: String,
pub policy: As4PullPolicy,
pub receipt_payload: Option<Vec<u8>>,
pub authorization_info: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As4ReceivePullOutput {
pub pull_message_id: Arc<str>,
pub correlation_message_id: Option<Arc<str>>,
pub mpc: Arc<str>,
pub duplicate_retrieval: bool,
pub pulled: Option<Arc<As4ReceivePushOutput>>,
pub outcome: DeliveryOutcome,
pub retry: RetryDecision,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum As4ErrorCode {
ValueNotRecognized,
FeatureNotSupported,
ValueInconsistent,
Other,
MissingReceipt,
InvalidReceipt,
DecompressionFailure,
}
impl As4ErrorCode {
pub fn ebms_code(self) -> &'static str {
match self {
As4ErrorCode::ValueNotRecognized => "EBMS:0001",
As4ErrorCode::FeatureNotSupported => "EBMS:0002",
As4ErrorCode::ValueInconsistent => "EBMS:0003",
As4ErrorCode::Other => "EBMS:0004",
As4ErrorCode::MissingReceipt => "EBMS:0301",
As4ErrorCode::InvalidReceipt => "EBMS:0302",
As4ErrorCode::DecompressionFailure => "EBMS:0303",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum As4ErrorSeverity {
Failure,
Warning,
}
impl As4ErrorSeverity {
pub(super) fn as_str(self) -> &'static str {
match self {
As4ErrorSeverity::Failure => "Failure",
As4ErrorSeverity::Warning => "Warning",
}
}
}