AS2

Send AS2 with a verified synchronous or asynchronous MDN, and receive with MDN dispositions.

On this page

AS2 (RFC 4130) is S/MIME over HTTP with a Message Disposition Notification (MDN) as receipt. Profile::as2() fixes the security settings (profiles).

Sending with a synchronous MDN

An AS2 Message is one payload to one partner.

use asx::{as2::{As2, Message}, ErrorKind, Partner, Payload};

async fn send_x12(as2: &As2, partner: &Partner, outbox_id: &str, x12: Vec<u8>) -> asx::Result<()> {
    let msg = Message::to(partner, Payload::new(x12, "application/edi-x12"))
        .message_id(format!("{outbox_id}@sender.example"));
    match as2.send(msg).await {
        Ok(mdn) => {
            println!("processed; MDN record {}", mdn.evidence().record_id());
            Ok(())
        }
        Err(e) if e.kind() == ErrorKind::PartnerError => {
            println!("partner refused: {:?}", e.mdn_disposition());
            Err(e)
        }
        Err(e) => Err(e),
    }
}

send returns Ok(Verified<Receipt>) only for an MDN that is signed by a pinned partner certificate (unsigned only with Quirk::UnsignedMdn), names the sent id in Original-Message-ID, echoes the MIC over the signed entity (RFC 4130 §7.3.1) and reports processed (warnings allowed). A processed/error or failed MDN is ErrorKind::PartnerError with mdn_disposition(). An MDN that fails any check is ErrorKind::Indeterminate: resend with the same message id.

message_id takes an RFC 2822 id-left@id-right value (sent as <id>); without it a fresh one is generated. Payload::named is refused.

Asynchronous MDNs

send_async_mdn(msg, receipt_url) sends with Receipt-Delivery-Option: receipt_url and returns a PendingMdn, which holds no secret; persist its serde form (versioned JSON) with the outbox entry. When the MDN arrives on your receipt URL, look it up and verify:

use asx::{as2::{As2, PendingMdn}, ErrorKind};
use bytes::Bytes;

async fn on_mdn(as2: &As2, request: http::Request<Bytes>, load: impl Fn(&str) -> Option<PendingMdn>) -> asx::Result<bool> {
    // Unauthenticated: only a lookup key. verify_async_mdn checks it.
    let Some(key) = PendingMdn::lookup_key(&request) else { return Ok(false) };
    let Some(pending) = load(&key) else { return Ok(false) };
    match as2.verify_async_mdn(request, &pending).await {
        Ok(_mdn) => Ok(true),
        Err(e) if e.kind() == ErrorKind::Rejected => Ok(false), // not an MDN for this message
        Err(e) => Err(e),
    }
}

A request that is not an MDN for pending is ErrorKind::Rejected; one that fails verification is ErrorKind::Indeterminate. The audit record is written when the MDN is verified.

Receiving

As2::inbound(dedup, handler) returns the same Inbound tower Service as AS4, with a handler over Received.

use asx::{as2::{As2, Received}, MemoryDedup, Reject, Verified};

async fn deliver(msg: Verified<Received>) -> Result<(), Reject> {
    let p = msg.payload();
    println!("{} from {}: {} bytes of {}", msg.message_id(), msg.from(), p.bytes().len(), p.mime_type());
    Ok(())
}

fn router(as2: &As2) -> axum::Router {
    axum::Router::new().route_service("/as2", as2.inbound(MemoryDedup::default(), deliver))
}

The payload is decrypted, verified and, if compressed (RFC 5402), decompressed. mime_type() is the signed entity's Content-Type; for a compressed entity, that of the MIME entity inside the CompressedData. AS2-From must be a configured partner whose pinned certificate signed.

MDN dispositions

SituationAnswerHandlerDedup
Body above max_message_bytesHTTP 413no—
UnparseableHTTP 400, with an MDN processed/error: unexpected-processing-error when one can be addressedno—
Unknown sender, bad signature or unpinned signerMDN processed/error: authentication-failedno—
AS2-To is not the local partyMDN processed/error: unexpected-processing-errorno—
Unsigned or unencryptedMDN processed/error: insufficient-message-securityno—
Decryption failed (one answer for every cause)MDN processed/error: decryption-failedno—
Decompression failed, or the compressed content is not a MIME entityMDN processed/error: decompression-failedno—
Duplicate (AS2-From, Message-ID)MDN processed/warning: duplicate-documentnounchanged
In flightHTTP 503nounchanged
claim failedHTTP 500, no MDNno—
Handler Ok, audit record and accept succeedsigned MDN processed with the MICyesaccepted
Handler Err, or a panicMDN processed/error: unexpected-processing-erroryesabandoned
Handler Ok, then the audit record or accept failsHTTP 500, no MDNyesabandoned

Every MDN is signed and returned synchronously, even when the sender asked for an asynchronous one. An unknown sender never causes a private-key operation.

Not supported

Sending asynchronous MDNs as receiver, streaming receive, outbound compression. See the migration page for 0.15 users.