AS2 reference
AS2 (RFC 4130) in asx-rs: send and receive, S/MIME signing and encryption, MDN generation, and how the RFC 4130 section 7.3.1 MIC is computed.
Requires feature flag: as2
Overview
AS2 (RFC 4130) support in asx-rs is exposed through free functions in asx_rs::as2.
Primary flows:
- Outbound send with optional compression, signing, and encryption.
- Inbound receive with trust verification/decryption.
- MDN generation and parse/classification.
- Reliability and dedup integration for MDN-linked receive paths.
Public Entry Points
Send
pub fn send_sync(
session: &SessionContext,
event_bus: &EventBus,
request: As2SendRequest,
) -> Result<As2SendOutput>
Async-safe wrapper for Tokio services:
pub async fn send_async(
session: &SessionContext,
event_bus: &EventBus,
request: As2SendRequest,
) -> Result<As2SendOutput>
Behavior notes:
- RFC 5402 order is enforced for compression-enabled messages:
compress -> sign -> encrypt. - Compression produces a CMS
CompressedDatastructure (RFC 5402 / RFC 3274, ZLIB), carried asapplication/pkcs7-mime; smime-type=compressed-data— not a bare gzip stream. - Policy-controlled protocol events are emitted via
EventBus.
MIC computation
The Received-Content-MIC a receiver returns in the MDN must digest exactly the octets the sender digested, and RFC 4130 §7.3.1 makes that input depend on which protection was applied:
| Message | MIC input |
|---|---|
| Signed (with or without encryption) | the signed MIME entity — headers, blank line, content |
| Encrypted but unsigned | the decrypted MIME entity — headers and content |
| Neither signed nor encrypted | the content only, with no MIME headers |
The last row is deliberate: unprotected messages pass through intermediaries that reorder and rewrite headers, so including them would make the MIC unreproducible.
Consequences worth knowing:
As2SendPolicy::payload_content_typeappears on the wire as the signed entity'sContent-Type, so it is part of the MIC. Setting it to the payload's real media type (application/edi-x12,application/edifact, …) is required for the partner to compute the same value.- Binary content (compressed or encrypted payloads) is transfer-encoded with
base64before signing, per RFC 5751 §3.1.1 — MIME line-ending canonicalization would otherwise rewrite bytes inside the blob and break the signature. Text payloads keepbinarytransfer encoding. - Nothing is trimmed, re-folded, or normalized before hashing; the entity is digested as transmitted.
Receive (owned payload)
pub fn receive_sync(
session: &SessionContext,
payload: Vec<u8>,
verifier: &dyn As2TrustVerifier,
) -> Result<DomainReady<Arc<[u8]>>>
Async-safe wrapper for Tokio services:
pub async fn receive_async(
session: &SessionContext,
payload: Vec<u8>,
verifier: Arc<dyn As2TrustVerifier + Send + Sync>,
) -> Result<DomainReady<Arc<[u8]>>>
Token-enforced strict runtime sessions
For regulated deployments that require explicit startup proof, bind validated session context once and then use standard AS2 entry points:
let strict_session = bootstrap_token.bind(&session);
let received = asx_rs::as2::receive_sync(&strict_session, payload, verifier)?;
bootstrap_token comes from StrictRuntimeBootstrap::validate. In non-testing builds, strict-interop AS2 entry points refuse a session with no token binding, and there is no other way to set the marker.
Receive (streaming ingress)
pub async fn receive_stream<R: tokio::io::AsyncRead + Unpin>(
session: &SessionContext,
policy: &As2ReceivePolicy,
reader: R,
verifier: &dyn AsyncAs2TrustVerifier,
limits: StreamLimits,
) -> Result<DomainReady<Arc<[u8]>>>
pub async fn receive_stream_with_metrics<R: tokio::io::AsyncRead + Unpin>(
session: &SessionContext,
policy: &As2ReceivePolicy,
reader: R,
verifier: &dyn AsyncAs2TrustVerifier,
limits: StreamLimits,
) -> Result<(DomainReady<Arc<[u8]>>, StreamReadMetrics)>
Receive + MDN + reliability
pub fn receive_with_mdn_with_reliability(
session: &SessionContext,
event_bus: &EventBus,
request: As2ReceiveMdnRequest,
reconciliation_hook: &dyn ReconciliationStorage,
dedup_backend: &dyn DedupStorage,
verifier: &dyn As2TrustVerifier,
) -> Result<As2ReceiveMdnOutput>
Borrowed variant:
pub fn receive_with_mdn_with_reliability_mdn_ref(
session: &SessionContext,
event_bus: &EventBus,
request: As2ReceiveMdnRefRequest<'_>,
reconciliation_hook: &dyn ReconciliationStorage,
dedup_backend: &dyn DedupStorage,
verifier: &dyn As2TrustVerifier,
) -> Result<As2ReceiveMdnOutput>
Which MDN part carries the verdict
The machine-readable fields (Disposition, Received-Content-MIC, Original-Message-ID) live in a message/disposition-notification part, and that part is found by media type — RFC 3798 §3 places it second in a multipart/report, but position is a convention, not the identifier.
The same rules apply signed and unsigned, and the signed case is the one that matters because it is the non-repudiation evidence:
| Shape | Result |
|---|---|
multipart/report with one message/disposition-notification part | that part |
Signed content that is a message/disposition-notification | that part |
| Report with no machine-readable part | strict: rejected · relaxed: no verdict |
| Signed content of any other media type | strict: rejected · relaxed: no verdict |
| Report with two machine-readable parts | rejected in both modes |
Relaxed mode widens only the absent case: it never produces a verdict read out of a human-readable part, which is where a counterparty would put a forged Disposition: line.
MDN generation
pub fn generate_mdn(
session: &SessionContext,
event_bus: &EventBus,
request: As2GenerateMdnRequest,
) -> Result<As2MdnOutput>
Core Types
As2SendPolicy: outbound cryptographic/interop behavior.As2SendCredentials: PEM signing/encryption material.As2SendOutput: transport headers/body/MIC for HTTP send.As2ReceiveMdnRequest: payload + MDN + policy input for reliability path.ParsedAs2Mdn: parsed disposition/MIC/signature metadata.
Interop Notes
- Strict mode is the default profile.
- Relaxed mode remains feature-gated (
interop-relaxed) and should be used only with scoped, audit-visible exception policies. - Production deployments should keep strict mode unless exceptions are explicitly governed.
Audit Events
AS2 flows emit protocol events through EventBus (for example: outbound prepared, MIC computed, message signed/encrypted, MDN received, duplicate detected).
EventBus is strict by default. Use EventBus::new_regulated(..) in regulated deployments; opt into best-effort explicitly via EventBus::builder().emission_mode(EventEmissionMode::BestEffort) only where transport progress must not depend on subscriber liveness.