Async-native, memory-safe AS2 + AS4 EDI transport library for Rust.
asx-rs implements the AS2 (RFC 4130) and AS4 (OASIS ebMS3 + eDelivery) protocols β the wire formats used by PEPPOL, CEF eDelivery, and tens of thousands of EDI trading partner connections worldwide.
β οΈ Alpha quality. Core AS2 send/receive and AS4 push/pull are working and tested. See Status for known gaps before using in production.
- Send and receive signed payloads (CMS/S/MIME, RSA-SHA256)
- Synchronous and asynchronous MDN (Message Disposition Notification)
- MIC computation for end-to-end integrity verification
- Payload compression (RFC 5402 / zlib, enabled by default via
compression) - Configurable interop mode: strict vs. relaxed for legacy partners
- Retry classification (
SuccessConfirmed/Indeterminate/AcceptedPendingVerification)
- One-Way/Push send and receive
- One-Way/Pull with bounded in-memory pull store per MPC partition
- WS-Security XMLDSig signing (RSA-SHA256, Exc-C14N) and verification
- XML encryption / decryption (XMLenc11 AES-GCM + RSA-OAEP)
- Streaming receive path with bounded memory
- P-Mode registry for per-partner MEP/security configuration
- SBDH 1.3 envelope wrap/unwrap for PEPPOL and CEF eDelivery
- Type-state lifecycle machine β compiler enforces the full trust chain:
UntrustedBytes β StructurallyParsed β CryptographicallyVerified β ContentDecrypted β DomainReady. Payload bytes cannot reach application code without every gate passing. - OCSP stapling + responder-based revocation checking (
async-ocspfeature) - PKIX certificate chain validation (fail-closed on empty trust store)
- Dedup storage (
InMemoryDedupStorage,TtlDedupStorage) prevents replay - Reconciliation hooks for async delivery confirmation
- Axum 0.7 server integration (
serverfeature) β drop-inRouterfor AS2 and AS4 ingress - Async HTTP egress via
reqwest(clientfeature) - Inbound endpoint governance (
HttpEndpointPolicy) against unexpected sources
EventBuswith fan-out broadcast and ordered mpsc audit channelDurableAuditSinktrait for pluggable audit backends- Configurable back-pressure policy (
BackpressurePolicy) EventBusMetricswith lock-freeAtomicU64counters- Optional built-in Prometheus/OpenMetrics text sink (
prometheusfeature)
- Profile stacking with regional packs and per-partner overlays
interop-strict(default) andinterop-relaxedfeature-gated modes- Exception policies (
InteropExceptionPolicy) for well-known deviations - Interop matrix executor (
testingfeature) β built-in fixture-based conformance runner
Add to Cargo.toml:
[dependencies]
# AS2 client + server with OCSP
asx-rs = { version = "0.3", features = ["as2", "client", "server", "async-ocsp"] }
# AS4 only
asx-rs = { version = "0.3", features = ["as4", "client", "server", "async-ocsp"] }
# Both protocols with compression (default)
asx-rs = { version = "0.3", features = ["as2", "as4", "compression", "client", "server", "async-ocsp"] }
as2andas4are not enabled by default β add them explicitly.
use asx_rs::as2::{send_sync, As2SendCredentials, As2SendPolicy, As2SendRequest};
use asx_rs::core::SessionContext;
use asx_rs::observability::{BackpressurePolicy, EventBus, EventEmissionMode};
let policy = As2SendPolicy {
sign: true,
encrypt: false,
compress: false,
as2_from_id: "my-company".into(),
..Default::default()
};
let creds = As2SendCredentials {
signing_cert_pem: Some(std::fs::read("my-cert.pem")?),
signing_key_pem: Some(std::fs::read("my-key.pem")?),
..Default::default()
};
let session = SessionContext::new("sess-001", "partner-a", "strict")?;
let bus = EventBus::new_with_config_and_mode(
1024,
None,
BackpressurePolicy::default(),
EventEmissionMode::BestEffort,
)?;
let output = send_sync(
&session,
&bus,
As2SendRequest {
message_id: "msg-001@example.com".to_string(),
payload: b"<Invoice/>".to_vec(),
policy,
credentials: creds,
},
)?;
// output.mime.body β body bytes to POST to partner's AS2 URL
// output.mime.content_type β HTTP Content-Type header value
// output.http_headers β required AS2 HTTP headers (AS2-From, AS2-To, etc.)use asx_rs::as2::{receive_sync, CmsSmimeTrustVerifier};
use asx_rs::core::SessionContext;
let session = SessionContext::new("sess-002", "partner-a", "strict")?;
let verifier = CmsSmimeTrustVerifier;
let trusted = receive_sync(&session, raw_http_body.to_vec(), &verifier)?;
println!("payload bytes: {}", trusted.as_ref().len());use asx_rs::as4::{send_sync, As4SendPolicyBuilder, As4SendRequest};
use asx_rs::core::SessionContext;
use asx_rs::observability::{BackpressurePolicy, EventBus, EventEmissionMode};
let (policy, creds) = As4SendPolicyBuilder::new()
.signing_cert_pem(signing_cert_pem)
.signing_key_pem(signing_key_pem)
.build()?;
let session = SessionContext::new("sess-003", "partner-a", "strict")?;
let bus = EventBus::new_with_config_and_mode(
1024,
None,
BackpressurePolicy::default(),
EventEmissionMode::BestEffort,
)?;
let output = send_sync(
&session,
&bus,
As4SendRequest {
message_id: "msg-001@example.com".to_string(),
payload: b"<Invoice/>".to_vec(),
policy,
credentials: creds,
},
)?;
// output.soap_envelope.body -> multipart/related bytes ready to POSTuse std::sync::Arc;
use axum::Router;
use async_trait::async_trait;
use asx_rs::transport::server::{As4AxumHandler, as4_router, HandlerOutcome};
use asx_rs::transport::As4HttpIngress;
struct MyAs4Handler;
#[async_trait]
impl As4AxumHandler for MyAs4Handler {
async fn handle(&self, ingress: As4HttpIngress) -> HandlerOutcome {
// Feed ingress.body into asx_rs::as4::receive_push_with_dedup_sync(β¦)
HandlerOutcome::ok()
}
}
#[tokio::main]
async fn main() {
let app: Router = as4_router(Arc::new(MyAs4Handler), "/as4/inbox");
let listener = tokio::net::TcpListener::bind("0.0.0.0:4080").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use std::sync::Arc;
use asx_rs::presets::{
DeploymentTopology,
StrictRuntimeBootstrapToken,
issue_strict_runtime_bootstrap_token_with_as4_topology,
strict_production_event_bus,
};
use asx_rs::as4::{As4ConversationOrderGate, As4PullStore};
use asx_rs::storage::{DedupStorage, ReconciliationStorage};
fn strict_runtime_bootstrap(
reconciliation: Arc<dyn ReconciliationStorage>,
dedup: Arc<dyn DedupStorage>,
audit_sink: Arc<dyn asx_rs::observability::audit_sink::DurableAuditSink>,
pull_store: &As4PullStore,
conversation_gate: &As4ConversationOrderGate,
) -> asx_rs::Result<asx_rs::observability::EventBus> {
let bus = strict_production_event_bus(1024, audit_sink)?;
// Fail closed before serving traffic if strict invariants are not met.
let _token: StrictRuntimeBootstrapToken = issue_strict_runtime_bootstrap_token_with_as4_topology(
"startup",
&bus,
reconciliation.as_ref(),
dedup.as_ref(),
DeploymentTopology::Clustered,
Some(pull_store),
Some(conversation_gate),
)?;
Ok(bus)
}In non-testing builds, strict interop protocol entry points fail closed unless
startup validation is bound to the session by explicitly applying
asx_rs::presets::session_with_strict_runtime_bootstrap_token(...).
For AS2 HTTP server flows, bind a strict session once with
session_with_strict_runtime_bootstrap_token(...) and then call
As2HttpIngress::receive_and_generate_mdn(...) or
As2HttpIngress::receive_and_generate_mdn_with_signing(...).
For AS4 HTTP server flows, bind a strict session once with
session_with_strict_runtime_bootstrap_token(...) and then call
As4HttpIngress::receive_push_with_dedup_sync(...) with an optional receipt payload.
use asx_rs::sbdh::{StandardBusinessDocument, SbdhHeader, SbdhParty, SbdhDocumentIdentification};
let doc = StandardBusinessDocument {
header: SbdhHeader {
header_version: "1.0".into(),
sender: SbdhParty { identifier: "0007:9876543210987".into(), authority: "iso6523-actorid-upis".into() },
receiver: SbdhParty { identifier: "0007:1234567890123".into(), authority: "iso6523-actorid-upis".into() },
document_identification: SbdhDocumentIdentification {
standard: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2".into(),
type_version: "2.1".into(),
instance_identifier: "urn:uuid:550e8400-e29b-41d4-a716-446655440000".into(),
r#type: "Invoice".into(),
multiple_type: false,
creation_date_and_time: "2026-01-01T12:00:00+00:00".into(),
},
},
payload: invoice_xml_bytes,
};
let wrapped = doc.wrap()?;
// wrapped β send via AS4 push to PEPPOL access point| Flag | Enables | Default |
|---|---|---|
as2 |
AS2 send/receive free functions (as2::send_sync, as2::receive_sync) |
β |
as4 |
AS4 send/receive free functions (as4::send_sync, as4::receive_push_with_dedup_sync) and As4PullStore |
β |
client |
HTTP egress via reqwest (As2HttpTransport, As4HttpTransport) |
β |
server |
Axum 0.7 router integration (as2_router, as4_router) |
β |
compression |
Zlib/GZIP compression via flate2 |
β |
async-ocsp |
Async OCSP responder fetching via reqwest |
β |
interop-strict |
Strict interop mode as default | β |
interop-relaxed |
Relaxed mode helpers for legacy partners | β |
trace |
tracing instrumentation stubs |
β |
prometheus |
Built-in PrometheusMetricsSink adapter for MetricsSink |
β |
postgres-storage |
PostgreSQL-backed durable, cluster-safe dedup/reconciliation storage | β |
testing |
Exposes fixture catalog and matrix executor | β |
asx-rs
βββ as2/ AS2 send, receive, MDN handling
βββ as4/ AS4 push/pull, P-Mode registry, pull store
β βββ pmode.rs P-Mode registry + resolution
β βββ parser.rs ebMS3 UserMessage XML parser
β βββ pull_store Bounded in-memory pull queue
βββ crypto/
β βββ as2_smime CMS/S/MIME signing + verification
β βββ wssec WS-Security (XMLDSig, XMLenc, OCSP, Exc-C14N)
β βββ soap_builder SOAP envelope construction
βββ transport/
β βββ ingress HTTP request normalisation
β βββ egress HTTP send with endpoint governance
β βββ server Axum router builders (server feature)
βββ lifecycle Type-state trust transition machine
βββ reliability Retry classification, dedup, reconciliation
βββ storage/ DedupStorage + ReconciliationStorage traits + in-memory impls
βββ observability/ EventBus, audit sink, back-pressure policy
βββ interop Profile stacks, regional packs, exception policies
βββ sbdh UN/CEFACT SBDH 1.3 wrap/unwrap
βββ wire Bounded stream reading, MIME utilities
βββ core Error types, SessionContext, shared utilities
Every inbound byte travels a compiler-enforced path before reaching your application:
UntrustedBytes
β structural parse (MIME / SOAP envelope)
βΌ
StructurallyParsed
β cryptographic verify (S/MIME or XMLDSig)
βΌ
CryptographicallyVerified
β decrypt (S/MIME EnvelopedData or XMLenc)
βΌ
ContentDecrypted
β dedup check + domain validation
βΌ
DomainReady β your application code starts here
InsecureBypassTrustVerifierskips all cryptographic verification. It is intended exclusively for testing. Never use it in production.- PKIX chain validation is fail-closed: an empty
trust_anchor_pemsstore rejects every certificate. - OCSP checking is opt-in via
OcspModeinCertHandle. The default isOcspMode::Disabledβ setOcspMode::ResponderOnlyorOcspMode::StapledAndResponderin production. - Outbound HTTP egress validates URL scheme and blocks private/loopback/link-local targets (including DNS-rebinding to private addresses).
asx-rs is alpha quality. Core AS2 send/receive and AS4 push/pull flows are implemented and tested, but the crate is not yet production-hardened.
Current constraints to evaluate before production rollout:
- Core send/receive entry points are synchronous, but async-safe wrappers are now available (
as2::send_async,as2::receive_async,as4::send_async,as4::receive_push_with_dedup_async) to isolate blocking work on Tokio blocking threads. - Production persistence adapters (Redis/PostgreSQL/DynamoDB) are trait-based and not yet shipped in-tree; deployers must provide backend implementations.
Licensed under either of:
at your option.