asx-rs 0.14.0

AS2 and AS4 B2B messaging library for Rust — signing, encryption, MDN, and ebMS3/AS4 profile support
Documentation

asx-rs

AS2 and AS4 B2B messaging for Rust — async-native, memory-safe, and fail-closed by default.

Crates.io Docs.rs License

asx-rs implements AS2 (RFC 4130) and AS4 (OASIS ebMS3 / eDelivery) — the wire protocols behind PEPPOL, CEF eDelivery, BDEW, and a large share of the world's EDI trading-partner connections.

It is an embeddable library, not a gateway. There is no container to run, no database to provision, and no JVM. You get the protocol and the cryptography; storage, key management and deployment stay yours.

📖 Documentation · 🦀 API reference · 📋 Changelog


Install

cargo add asx-rs --features as4,client,server

as2 and as4 are not on by default — enable the protocols you need.

Flag Enables Default
crypto Shared OpenSSL gate (RSA/ECDSA, X.509, CMS, OCSP) — pulled in by as2/as4 via protocol
as2 AS2 send/receive, S/MIME, MDN
as4 AS4 send/receive, pull store, WS-Security, XML encryption
client HTTP egress via reqwest
server Axum router integration for inbound
compression AS2 (RFC 5402 CMS) and AS4 (gzip) payload compression
async-ocsp Async OCSP responder fetching
interop-strict Strict interop mode as the compile-time default
interop-relaxed Relaxed-mode helpers for legacy partners
trace tracing instrumentation on hot paths
prometheus / opentelemetry Metrics sink adapters
dns Built-in BDXL/NAPTR resolver for Peppol participant discovery
testing Bypass verifiers, mock endpoint, keypair generators

The testing feature raises a compile_error! in release builds. It cannot reach a production binary by accident.

Example

Send a signed AS4 message and verify the counterparty's receipt:

use asx_rs::as4::{send_async, As4ReceiptPolicy, As4SendPolicyBuilder, As4SendRequest};
use asx_rs::transport::egress::{As4HttpTransport, TransportConfig};

let (policy, credentials) = As4SendPolicyBuilder::new()
    .action("urn:example:action")
    .service("urn:example:service", "")
    .signing_cert_pem(cert_pem)
    .signing_key_pem(key_pem)
    .build()?;

let sent = send_async(&session, &bus, As4SendRequest {
    message_id: message_id.clone(),
    payload,
    policy,
    credentials: Some(credentials),
    payload_filename: None,
}).await?;

let outcome = As4HttpTransport::new(TransportConfig::default())?
    .send_and_verify(&url, &session, &bus, &sent, &As4ReceiptPolicy::regulated())
    .await?;

A receipt is only evidence of delivery once its signature and non-repudiation digests are checked against the message you sent. send_and_verify does both in one call, so that step cannot be skipped by accident.

More examples — AS2 send, inbound routers, encryption enforcement, testing without a PKI — are in the getting-started guide.

What makes it different

The type system enforces the trust boundary. Payloads move through UntrustedBytes → StructurallyParsed → CryptographicallyVerified → ContentDecrypted → DomainReady. Application code cannot receive bytes that skipped a gate, because no value exists in that state.

Insecure configurations are unreachable by omission. Signatures are required, AS4 pull is denied, SMP lookup results are not trusted, and encrypted-spool profiles refuse to run without a key — until you explicitly choose otherwise. Every relaxation is a named variant you select, never a field you forgot.

Integration points are traits, not built-ins. Dedup, reconciliation, durable audit, DNS resolution and spool-encryption keys are interfaces you implement against your own PostgreSQL, Redis, resolver, KMS or HSM. The crate does not guess at their protocols — it ships a conformance suite instead, so "durable" and "cluster-safe" are things your backend demonstrates rather than declares. And it does not ship what it cannot know: there is no retry scheduler, no storage backend and no alerting engine, because a retry budget and a failure-rate threshold are properties of your partners and your topology, not of a protocol.

See the security model for trust boundaries, defaults, and what the library does not protect against.

Status

Beta. Core AS2 and AS4 flows are implemented and covered by ~1,000 unit and integration tests, plus property, fuzz, golden-vector and cross-implementation gates. The library contains no unsafe#![forbid(unsafe_code)] keeps it that way — and is clippy-clean at --all-features --all-targets. MSRV is Rust 1.88, and CI builds on that exact floor.

Implemented:

  • AS2 send/receive — signed, encrypted, compressed, sync and async MDN
  • AS4 push send/receive — signed, encrypted, dedup, fragment reassembly
  • AS4 pull with reliability classification and MPC authorization
  • WS-Security: RSA-SHA256, ECDSA-SHA256, RSA-OAEP, ECDH-ES + ConcatKDF + AES-KW
  • Cross-implementation interop, tested live: a containerized Holodeck B2B counterparty exchanges a signed AS4 push for a signed NRR receipt; C14N/digests are verified against xmlsec1; AS2 S/MIME is exchanged in both directions with the openssl CLI
  • Reception Awareness / Non-Repudiation of Receipt, verified end to end
  • OCSP and PKIX chain validation
  • Multi-payload AS4, both directions — every eb:PartInfo attachment signed, verified, and independently compressed/encrypted
  • ebMS3 Test Service (§5.2.2): a connectivity ping is acknowledged and reported as one, never delivered as a business document
  • Peppol/CEF dynamic discovery — BDXL U-NAPTR participant lookup against the OpenPeppol SML zones, plus SMP 1.0 and 2.0 ServiceMetadata, with the DNS query behind a trait so DNSSEC validation stays your choice

Known gaps:

  • No official conformance certificate yet. The self-hosted counterparty loop passes; the free EC eDelivery conformance service is the next step — see the conformance guide.
  • No storage backends ship in-tree. DedupStorage, ReconciliationStorage and DurableAuditSink are trait-defined; you supply the implementation — and asx_rs::storage::conformance is the suite that proves yours is durable and its first_seen atomic, rather than taking your word for it.

Development

just ci             # fast pre-push subset: lint, check, test, feature matrix
just release-gate   # every mandatory gate (slow)
just --list         # all recipes

Recipes mirror .github/workflows/ci.yml and RELEASING.md; when a gate changes in one, change it in the other.

The documentation site is built with Zola:

zola --root site serve   # http://127.0.0.1:1111
zola --root site build   # also fails on broken links and dangling anchors

Versioning

Pre-1.0, breaking changes ship in minor versions. The changelog separates wire/crypto behaviour — which can break interoperability with a counterparty even when your code compiles unchanged — from API changes, which break the build instead.

License

Dual-licensed under MIT or Apache-2.0, at your option.