use std::sync::Arc;
use zeroize::Zeroize;
use crate::core::{AsxError, ErrorCode, ErrorContext, InteropMode, Result};
#[cfg(feature = "as2")]
use crate::crypto::as2_smime::SmimeCipher;
use crate::http::HttpHeaders;
use crate::interop::InteropExceptionPolicy;
use crate::lifecycle::DomainReady;
use crate::reliability::{DeliveryOutcome, RetryDecision};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MimeEnvelope {
pub content_type: String,
pub body: Arc<[u8]>,
}
impl MimeEnvelope {
#[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())
}
}
#[allow(unused)]
pub mod payload_content_type {
pub const EDIFACT: &str = "application/EDIFACT";
pub const X12: &str = "application/EDI-X12";
pub const EDI_CONSENT: &str = "application/edi-consent";
pub const XML: &str = "application/xml";
pub const JSON: &str = "application/json";
pub const OCTET_STREAM: &str = "application/octet-stream";
pub const TEXT: &str = "text/plain";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum As2MicAlgorithm {
#[default]
Sha256,
Sha384,
Sha512,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As2SendPolicy {
pub interop_mode: InteropMode,
pub fail_closed_audit_events: bool,
pub sign: bool,
pub encrypt: bool,
pub compress: bool,
pub payload_content_type: Option<&'static str>,
pub mic_algorithm: As2MicAlgorithm,
#[cfg(feature = "as2")]
pub encryption_cipher: SmimeCipher,
pub as2_from_id: String,
}
impl Default for As2SendPolicy {
fn default() -> Self {
Self {
interop_mode: InteropMode::Strict,
fail_closed_audit_events: true,
sign: true,
encrypt: true,
compress: false,
payload_content_type: None,
mic_algorithm: As2MicAlgorithm::Sha256,
#[cfg(feature = "as2")]
encryption_cipher: SmimeCipher::Aes256Cbc,
as2_from_id: String::new(),
}
}
}
impl As2SendPolicy {
pub fn strict() -> Self {
Self::default()
}
pub fn regulated() -> Self {
Self::default()
}
}
#[derive(Clone, Default)]
pub struct As2SendCredentials {
pub signing_cert_pem: Option<Arc<[u8]>>,
pub signing_key_pem: Option<Vec<u8>>,
pub recipient_cert_pem: Option<Arc<[u8]>>,
}
impl std::fmt::Debug for As2SendCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("As2SendCredentials")
.field("signing_cert_pem", &self.signing_cert_pem)
.field(
"signing_key_pem",
&crate::core::redact_present(self.signing_key_pem.is_some()),
)
.field("recipient_cert_pem", &self.recipient_cert_pem)
.finish()
}
}
impl Drop for As2SendCredentials {
fn drop(&mut self) {
if let Some(key) = self.signing_key_pem.as_mut() {
key.zeroize();
}
}
}
#[cfg(feature = "as2")]
#[derive(Debug, Clone)]
pub struct As2PreparedSendCredentials {
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 As2SendCredentials {
#[cfg(feature = "as2")]
pub fn prepare_for_policy(
&self,
policy: &As2SendPolicy,
stage: &'static str,
error_code: ErrorCode,
) -> Result<As2PreparedSendCredentials> {
let ctx = || ErrorContext::new(stage);
let mut prepared = As2PreparedSendCredentials {
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, "AS2 signing certificate is missing", ctx())
})?;
let key_pem = self
.signing_key_pem
.as_ref()
.ok_or_else(|| AsxError::new(error_code, "AS2 signing key is missing", ctx()))?;
let signing_cert = openssl::x509::X509::from_pem(cert_pem).map_err(|_| {
AsxError::new(
error_code,
"AS2 signing certificate is not a valid PEM X.509 certificate",
ctx(),
)
})?;
let signing_key = openssl::pkey::PKey::private_key_from_pem(key_pem).map_err(|_| {
AsxError::new(
error_code,
"AS2 signing key is not a valid PEM private key",
ctx(),
)
})?;
let signing_cert_public = signing_cert.public_key().map_err(|_| {
AsxError::new(
error_code,
"AS2 signing certificate does not contain a usable public key",
ctx(),
)
})?;
if !signing_key.public_eq(&signing_cert_public) {
return Err(AsxError::new(
error_code,
"AS2 signing certificate does not match signing key",
ctx(),
));
}
prepared.signing_cert = Some(signing_cert);
prepared.signing_key = Some(signing_key);
}
if policy.encrypt {
let recipient_cert_pem = self.recipient_cert_pem.as_ref().ok_or_else(|| {
AsxError::new(error_code, "AS2 recipient certificate is missing", ctx())
})?;
let recipient_cert =
openssl::x509::X509::from_pem(recipient_cert_pem).map_err(|_| {
AsxError::new(
error_code,
"AS2 recipient certificate is not a valid PEM X.509 certificate",
ctx(),
)
})?;
prepared.recipient_cert = Some(recipient_cert);
} else if let Some(recipient_cert_pem) = self.recipient_cert_pem.as_ref() {
let recipient_cert =
openssl::x509::X509::from_pem(recipient_cert_pem).map_err(|_| {
AsxError::new(
error_code,
"AS2 recipient certificate is not a valid PEM X.509 certificate",
ctx(),
)
})?;
prepared.recipient_cert = Some(recipient_cert);
}
Ok(prepared)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As2SendOutput {
pub message_id: String,
pub mime: MimeEnvelope,
pub mic_base64: String,
pub digest_alg: &'static str,
pub traceparent: Option<String>,
pub http_headers: HttpHeaders,
}
impl As2SendOutput {
pub fn as_received_content_mic(&self) -> String {
format!("{}, {}", self.mic_base64, self.digest_alg)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum As2MdnMode {
Synchronous,
Asynchronous,
None,
}
#[derive(Debug, Clone)]
pub struct As2ReceivePolicy {
pub interop_mode: InteropMode,
pub interop_exceptions: InteropExceptionPolicy,
pub fail_closed_audit_events: bool,
pub spool_key_provider: Option<std::sync::Arc<dyn super::SpoolEncryptionKeyProvider>>,
pub enforce_as2_version: bool,
}
impl Default for As2ReceivePolicy {
fn default() -> Self {
Self {
interop_mode: InteropMode::Strict,
interop_exceptions: InteropExceptionPolicy::default(),
fail_closed_audit_events: true,
spool_key_provider: None,
enforce_as2_version: true,
}
}
}
impl As2ReceivePolicy {
pub fn strict() -> Self {
Self::default()
}
pub fn regulated() -> Self {
Self::default()
}
}
#[derive(Debug, Clone)]
pub struct As2ReceivePolicyBuilder {
interop_mode: InteropMode,
interop_exceptions: InteropExceptionPolicy,
fail_closed_audit_events: bool,
spool_key_provider: Option<std::sync::Arc<dyn super::SpoolEncryptionKeyProvider>>,
enforce_as2_version: bool,
}
impl Default for As2ReceivePolicyBuilder {
fn default() -> Self {
Self::new()
}
}
impl As2ReceivePolicyBuilder {
pub fn new() -> Self {
let defaults = As2ReceivePolicy::default();
Self {
interop_mode: defaults.interop_mode,
interop_exceptions: defaults.interop_exceptions,
fail_closed_audit_events: defaults.fail_closed_audit_events,
spool_key_provider: defaults.spool_key_provider,
enforce_as2_version: defaults.enforce_as2_version,
}
}
pub fn interop(mut self, mode: InteropMode) -> Self {
self.interop_mode = mode;
self
}
pub fn interop_exceptions(mut self, exceptions: InteropExceptionPolicy) -> Self {
self.interop_exceptions = exceptions;
self
}
pub fn fail_closed_audit_events(mut self, fail_closed: bool) -> Self {
self.fail_closed_audit_events = fail_closed;
self
}
pub fn spool_key_provider(
mut self,
provider: std::sync::Arc<dyn super::SpoolEncryptionKeyProvider>,
) -> Self {
self.spool_key_provider = Some(provider);
self
}
pub fn enforce_as2_version(mut self, enforce: bool) -> Self {
self.enforce_as2_version = enforce;
self
}
pub fn build(self) -> As2ReceivePolicy {
As2ReceivePolicy {
interop_mode: self.interop_mode,
interop_exceptions: self.interop_exceptions,
fail_closed_audit_events: self.fail_closed_audit_events,
spool_key_provider: self.spool_key_provider,
enforce_as2_version: self.enforce_as2_version,
}
}
}
#[cfg(feature = "as2")]
pub fn validate_as2_version_header(
headers: &[(String, String)],
policy: &As2ReceivePolicy,
) -> Result<()> {
if !policy.enforce_as2_version {
return Ok(());
}
let version = headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("AS2-Version"))
.map(|(_, v)| v.trim().to_owned());
match version.as_deref() {
Some("1.0") | Some("1.1") | Some("1.2") => Ok(()),
Some(v) => Err(AsxError::new(
ErrorCode::InteropViolation,
format!("unrecognised AS2-Version '{v}'; expected 1.0, 1.1, or 1.2"),
ErrorContext::new("as2_receive"),
)),
None if policy.interop_mode == InteropMode::Strict => Err(AsxError::new(
ErrorCode::InteropViolation,
"AS2-Version header is missing",
ErrorContext::new("as2_receive"),
)),
None => Ok(()),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedMdn {
pub final_recipient: Option<String>,
pub original_message_id: Option<String>,
pub disposition: String,
pub received_content_mic: Option<String>,
pub is_signed: bool,
}
#[derive(Debug, Clone)]
pub struct As2InboundResult {
pub content: DomainReady<Arc<[u8]>>,
pub sync_mdn: Option<As2GeneratedMdn>,
pub received_content_mic: Option<String>,
pub mic_algorithm: As2MicAlgorithm,
pub async_mdn_address: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As2ReceiveMdnOutput {
pub payload: DomainReady<Arc<[u8]>>,
pub mdn: ParsedMdn,
pub outcome: DeliveryOutcome,
pub retry_decision: RetryDecision,
pub interop_reason_codes: Vec<&'static str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct As2GeneratedMdn {
pub bytes: Arc<[u8]>,
pub content_type: String,
pub is_signed: bool,
}
#[derive(Clone, PartialEq, Eq)]
pub struct As2MdnSigningCredentials {
pub signing_cert_pem: Vec<u8>,
pub signing_key_pem: Vec<u8>,
}
impl std::fmt::Debug for As2MdnSigningCredentials {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("As2MdnSigningCredentials")
.field("signing_cert_pem", &self.signing_cert_pem)
.field(
"signing_key_pem",
&crate::core::redact_present(!self.signing_key_pem.is_empty()),
)
.finish()
}
}
impl Drop for As2MdnSigningCredentials {
fn drop(&mut self) {
self.signing_key_pem.zeroize();
}
}
#[derive(Debug, Clone)]
pub struct As2ReceiveMdnRequest {
pub payload: Arc<[u8]>,
pub mdn_payload: Arc<[u8]>,
pub mdn_mode: As2MdnMode,
pub require_signed_mdn: bool,
pub expected_mic: Option<String>,
pub policy: As2ReceivePolicy,
pub original_message_id: Option<String>,
}