# ๐ฆ asx-rs
**Async-native, memory-safe AS2 + AS4 EDI transport library for Rust.**
`asx-rs` implements the [AS2 (RFC 4130)](https://www.rfc-editor.org/rfc/rfc4130) and
[AS4 (OASIS ebMS3 + eDelivery)](https://docs.oasis-open.org/ebxml-msg/ebms/v3.0/profiles/AS4-profile/v1.0/)
protocols โ the wire formats used by PEPPOL, CEF eDelivery, BDEW, and tens of thousands of
EDI trading partner connections worldwide.
---
## โจ Features
### ๐จ AS2 (RFC 4130 / RFC 5751)
- 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
- `CmsSmimeTrustVerifier::requiring_signature()` โ reject encrypted-only (unsigned)
inbound messages so sender authentication can be enforced
- Signed-receipt enforcement: an unsigned MDN is rejected when a signed receipt was
requested (`As2ReceiveMdnRequest::require_signed_mdn`) โ no silent non-repudiation downgrade
- Payload compression (RFC 5402 / zlib, `compression` feature)
- Configurable interop mode: strict vs. relaxed for legacy partners
- Retry classification (`SuccessConfirmed` / `Indeterminate` / `AcceptedPendingVerification`)
### ๐ฌ AS4 (ebMS3 + OASIS eDelivery AS4 v1.15)
- One-Way/Push send and receive (SOAP 1.1 and 1.2)
- One-Way/Pull with bounded in-memory pull store per MPC partition
- **WS-Security XMLDSig signing** โ RSA-SHA256 *and* **ECDSA-SHA256**; algorithm is
auto-detected from the signing certificate key type. Works with NIST P-256/P-384/P-521
and BSI BrainpoolP256r1/P384r1 (required by BDEW AS4-Profil ยง2.2.6.2.1 / BSI TR-03116-3 ยง9.1).
The signature covers the **entire `eb:Messaging` header block** (all UserMessage
routing/authorization metadata), the SOAP Body, and each payload attachment. On
receive, the consumed `eb:Messaging` block is bound to the verified signature
(XML-Signature-Wrapping defence). Signing certificates weaker than RSA-2048 are rejected.
- **XML Encryption (XMLenc11)** โ auto-dispatches on recipient certificate key type:
- **RSA recipient** โ RSA-OAEP (SHA-256/MGF1-SHA-256) โ PEPPOL / CEF eDelivery
- **EC recipient** โ ECDH-ES + ConcatKDF (NIST SP 800-56A ยง5.8.1) + AES-128 Key Wrap
(RFC 3394) โ BDEW AS4-Profil ยง2.2.6.2.2 / BSI TR-03116-3 ยง9.2
- `X509PKIPathv1` BST token type (`WsSecOutboundKeyInfoProfile::X509PKIPathv1`)
- `require_encrypted_inbound` policy field โ rejects unencrypted push messages at the policy
layer (symmetric to `require_signed_push`)
- Streaming receive path with bounded memory; conversation-ordered delivery gate
- P-Mode registry for per-partner MEP/security configuration
- SBDH 1.3 envelope wrap/unwrap for PEPPOL and CEF eDelivery
### ๐ Security & Reliability
- **Type-state lifecycle machine** โ compiler enforces `UntrustedBytes โ StructurallyParsed
โ CryptographicallyVerified โ ContentDecrypted โ DomainReady`; payloads cannot reach
application code without every gate passing
- OCSP stapling + responder-based revocation checking (`async-ocsp` feature)
- PKIX certificate chain validation (fail-closed on empty trust store)
- `TtlDedupStorage` / `BoundedFifoDedupStorage` prevents replay attacks
- Reconciliation hooks for async delivery confirmation
### ๐ HTTP Transport
- Axum server integration (`server` feature) โ drop-in `Router` for AS2 and AS4 ingress
- Async HTTP egress via `reqwest` (`client` feature)
- Inbound endpoint governance against unexpected sources
### ๐ Observability
- `EventBus` with fan-out broadcast and ordered mpsc audit channel
- `DurableAuditSink` trait for pluggable persistent audit backends
- `EventBusMetrics` with lock-free `AtomicU64` counters
- Optional Prometheus/OpenMetrics sink (`prometheus` feature)
### ๐งฉ Interop
- Profile stacking with regional packs and per-partner overlays
- `interop-strict` (default) and `interop-relaxed` feature-gated modes
- Exception policies (`InteropExceptionPolicy`) for well-known deviations
### ๐งช Testing (`testing` feature โ never enable in production)
- **`InsecureBypassAs4Verifier`** โ skips all WS-Security verification; parity with
`InsecureBypassTrustVerifier` on the AS2 side
- **`MockAs4Endpoint`** (`testing + server`) โ local HTTP AS4 server that accepts any push
(signed, unsigned, encrypted, or plain), records messages in an async channel, and
returns synchronous AS4 receipts. Configure decryption via
`MockAs4Endpoint::builder().with_decryption_key_pem(key_pem).bind(addr).await?`
- **`EventBus::new_for_testing()`** โ zero-config `BestEffort` bus; never fails on emit
when no subscriber is active โ no more `new_with_config_and_mode(...)` boilerplate
- **`DurableInMemoryDedupBackend`** โ in-memory dedup with `is_durable() = true`; passes
the strict durable-backend guard without a real persistent store
- **`As4HttpTransport::new_for_localhost_testing()`** โ plain-HTTP transport for
integration tests against `MockAs4Endpoint` on `http://127.0.0.1:โฆ`; SSRF guards
disabled for the test binary only
- **`generate_self_signed_ec_keypair(cn, curve)`** โ self-signed EC cert+key (P-256 through
BrainpoolP512r1); no openssl/rcgen dev-dependency needed in downstream crates
- **`generate_self_signed_rsa_keypair(cn, bits)`** โ same for RSA
- **`verifier_seal`** โ re-export of the `As4Verifier` sealed trait; allows downstream
crates to write custom verifiers (recording, fault-injection, etc.)
---
## ๐ Quick Start
```toml
[dependencies]
# AS4 โ PEPPOL / CEF eDelivery (RSA) or BDEW (EC/ECDH-ES) โ same code, key type decides
asx-rs = { version = "0.9", features = ["as4", "client", "server", "async-ocsp"] }
# AS2 + AS4 with compression
asx-rs = { version = "0.9", features = ["as2", "as4", "compression", "client", "server", "async-ocsp"] }
[dev-dependencies]
# Testing without PKI certificates (MockAs4Endpoint, bypass verifier, keypair generators)
asx-rs = { version = "0.9", features = ["as4", "testing", "server"] }
```
> `as2` and `as4` are **not** in the default feature set โ add them explicitly.
> The `testing` feature is **compile-error-guarded** against release profile builds.
---
## ๐ Examples
### AS4 โ Send a signed push message
The signing algorithm is chosen automatically from the key type โ no configuration needed:
- RSA certificate โ RSA-SHA256 (PEPPOL / CEF eDelivery)
- EC certificate (P-256, BrainpoolP256r1, โฆ) โ ECDSA-SHA256 (BDEW, BSI TR-03116-3 ยง9.1)
```rust
use asx_rs::as4::{send_async, As4SendPolicyBuilder, As4SendRequest};
use asx_rs::core::SessionContextBuilder;
use asx_rs::observability::EventBus;
// Session with trust anchor + signing material โ one fluent chain:
let session = SessionContextBuilder::new("sess-001", "partner-gln")
.with_signing_material(my_signing_cert_pem, my_signing_key_pem)
.with_trust_anchor_pem(partner_root_ca_pem)
.with_fingerprint_sha256(partner_cert_sha256_hex) // optional hard pin
.build()?;
// RSA or EC signing key โ auto-detected:
let (policy, creds) = As4SendPolicyBuilder::new()
.action("urn:bdew:as4:service:UTILMD")
.service("urn:bdew:as4:service", "")
.signing_cert_pem(my_signing_cert_pem) // RSA or EC
.signing_key_pem(my_signing_key_pem)
.recipient_cert_pem(partner_enc_cert_pem) // RSA โ RSA-OAEP; EC โ ECDH-ES
.encrypt(true)
.build()?;
let bus = EventBus::new(1024)?;
let output = send_async(
&session, &bus,
As4SendRequest {
message_id: "urn:uuid:abc123@my-host".into(),
payload: edifact_bytes,
policy,
credentials: Some(creds),
payload_filename: None,
},
).await?;
// output.soap_envelope.body โ multipart/related bytes to POST
// output.http_content_type โ HTTP Content-Type header value
```
### AS4 โ Receive push with encryption enforcement
```rust
use asx_rs::as4::{receive_push_with_dedup_async, As4PushPolicyBuilder, As4ReceivePushRequest};
use std::sync::Arc;
let policy = As4PushPolicyBuilder::new()
.inbound_decryption_key_pem(my_ec_or_rsa_private_key_pem)
.require_encrypted_inbound(true) // fail-closed: reject any unencrypted message
.build()?;
let outcome = receive_push_with_dedup_async(
&session, &bus,
As4ReceivePushRequest {
http_content_type,
payload,
receipt_payload: None,
policy,
authenticated_sender_scope: None,
},
dedup_backend,
).await?;
```
### AS4 โ Testing without PKI certificates
```rust
use asx_rs::as4::mock_endpoint::MockAs4Endpoint;
use asx_rs::observability::EventBus;
use tokio::time::{timeout, Duration};
// Plain receive: bind to a random port โ no cert, no WIRK, no PEPPOL test PKI needed
let endpoint = MockAs4Endpoint::bind("127.0.0.1:0").await?;
// Receive encrypted messages: configure decryption key via builder
let endpoint = MockAs4Endpoint::builder()
.with_decryption_key_pem(my_ec_or_rsa_private_key_pem)
.bind("127.0.0.1:0")
.await?;
let url = endpoint.local_url(); // "http://127.0.0.1:PORT/as4/inbox"
// Test EventBus: zero-config, never fails on emit (BestEffort)
let bus = EventBus::new_for_testing(); // requires `testing` feature
// Wait for the first message (returns None if the endpoint is dropped).
let msg = timeout(Duration::from_secs(5), endpoint.next_message())
.await??;
assert_eq!(msg.action, "urn:bdew:as4:service:UTILMD");
assert!(!msg.payload.is_empty());
// from_party_ids contains the sender GLN; to_party_ids the receiver GLN
```
### AS4 โ Generate EC or RSA test keypairs
```rust
use asx_rs::fixtures::{EcCurve, generate_self_signed_ec_keypair, generate_self_signed_rsa_keypair};
// BrainpoolP256r1 โ required by BDEW AS4-Profil ยง2.2.6.2.1
let (cert_pem, key_pem) = generate_self_signed_ec_keypair("test-ap", EcCurve::BrainpoolP256r1);
// P-256 โ standard for PEPPOL Access Points
let (cert_pem, key_pem) = generate_self_signed_ec_keypair("peppol-ap", EcCurve::P256);
// RSA-2048 โ classic PEPPOL style
let (cert_pem, key_pem) = generate_self_signed_rsa_keypair("rsa-ap", 2048);
```
### AS4 โ Custom verifier (testing feature)
```rust
use asx_rs::as4::{As4Verifier, verifier_seal, types::As4PushPolicy};
use asx_rs::core::{Result, SessionContext};
struct RecordingVerifier {
calls: std::sync::atomic::AtomicUsize,
}
impl verifier_seal::Sealed for RecordingVerifier {}
impl As4Verifier for RecordingVerifier {
fn verify_security(
&self, _session: &SessionContext, _policy: &As4PushPolicy,
_soap_xml: &str, _soap_doc: &roxmltree::Document<'_>,
_message_id: &str, _external_reference: Option<(&str, &[u8])>,
) -> Result<()> {
self.calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
}
```
### AS2 โ Send a signed message
```rust
use asx_rs::as2::{send_async, As2SendCredentials, As2SendPolicy, As2SendRequest};
use asx_rs::core::SessionContext;
use asx_rs::observability::EventBus;
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(1024)?;
let output = send_async(
&session, &bus,
As2SendRequest {
message_id: "msg-001@example.com".into(),
payload: b"<Invoice/>".to_vec(),
policy,
credentials: creds,
},
).await?;
```
### Strict production startup
```rust
use asx_rs::presets::{
DeploymentTopology, issue_strict_runtime_bootstrap_token_with_as4_topology,
strict_production_event_bus,
};
use asx_rs::as4::{As4ConversationOrderGate, As4PullStore};
use std::sync::Arc;
let bus = strict_production_event_bus(1024, durable_audit_sink)?;
let _token = issue_strict_runtime_bootstrap_token_with_as4_topology(
"startup", &bus,
reconciliation.as_ref(), dedup.as_ref(),
DeploymentTopology::Clustered,
Some(pull_store), Some(conversation_gate),
)?;
// _token must be passed to session_with_strict_runtime_bootstrap_token()
// before any protocol entry point is called
```
### SBDH โ PEPPOL / CEF eDelivery
```rust
use asx_rs::sbdh::{StandardBusinessDocument, SbdhHeader, SbdhParty, SbdhDocumentIdentification};
let wrapped = 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,
}.wrap()?;
```
---
## ๐๏ธ Feature Flags
| `as2` | AS2 send/receive (`as2::send_sync/async`, `as2::receive_sync/async`) | โ |
| `as4` | AS4 send/receive, pull store, WS-Security, ECDH-ES + ECDSA support | โ |
| `client` | HTTP egress via `reqwest` | โ |
| `server` | Axum router integration (`as2_router`, `as4_router`) | โ |
| `compression` | Zlib/GZIP payload compression (RFC 5402) | โ
|
| `async-ocsp` | Async OCSP responder fetching | โ
|
| `interop-strict` | Strict interop mode as compile-time default | โ
|
| `interop-relaxed` | Relaxed mode helpers for legacy partners | โ |
| `trace` | `tracing` instrumentation on hot paths | โ
|
| `prometheus` | Built-in Prometheus/OpenMetrics `MetricsSink` | โ |
| `opentelemetry` | OpenTelemetry metrics `MetricsSink` adapter | โ |
| `testing` | `InsecureBypassAs4Verifier`, `DurableInMemoryDedupBackend`, keypair generators, `verifier_seal`, interop matrix executor, **`EventBus::new_for_testing()`** | โ |
| `testing + server` | Also: **`MockAs4Endpoint`** (with `builder().with_decryption_key_pem()`) | โ |
> **Security:** The `testing` feature triggers a `compile_error!` in release profile builds.
> It must never appear in production binaries.
---
## ๐๏ธ Architecture
```
asx-rs
โโโ as2/ AS2 send, receive, MDN handling
โโโ as4/ AS4 push/pull, P-Mode registry, pull store, conversation gate
โ โโโ pmode.rs P-Mode registry + resolution
โ โโโ parser.rs ebMS3 UserMessage XML parser
โ โโโ pull_store/ Bounded in-memory pull queue (per-MPC)
โ โโโ mock_endpoint/ In-process test endpoint (testing + server)
โโโ crypto/
โ โโโ as2_smime CMS/S/MIME signing + verification
โ โโโ wssec/ WS-Security (XMLDSig, XMLenc11, OCSP, Exc-C14N)
โโโ transport/
โ โโโ ingress HTTP request normalisation + validation
โ โโโ 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
```
### ๐ Trust lifecycle
```
UntrustedBytes
โ structural parse (MIME / SOAP envelope)
โผ
StructurallyParsed
โ cryptographic verify (S/MIME or XMLDSig)
โผ
CryptographicallyVerified
โ decrypt (S/MIME EnvelopedData or XMLenc11)
โผ
ContentDecrypted
โ dedup check + domain validation
โผ
DomainReady โ your application code starts here
```
### ๐ XML Encryption: automatic key transport selection
`encrypt_payload_xmlenc` inspects the recipient certificate's key type at call time:
| RSA | RSA-OAEP (SHA-256 / MGF1-SHA-256) | `BinarySecurityToken` | PEPPOL, CEF eDelivery |
| EC (any named curve) | ECDH-ES ephemeral + ConcatKDF (NIST SP 800-56A) + AES-128-KW (RFC 3394) | `X509SKI` | BDEW AS4-Profil ยง2.2.6.2.2, BSI TR-03116-3 ยง9.2 |
No configuration knob is needed โ pass the recipient's certificate and the right algorithm
follows from the key type.
---
## ๐ Security Notes
- **`InsecureBypassAs4Verifier` / `InsecureBypassTrustVerifier`** bypass all cryptographic
checks. They are strictly limited to the `testing` feature, which is blocked in release
builds via `compile_error!`.
- PKIX chain validation is **fail-closed**: an empty `trust_anchor_pems` store with
`require_chain_validation = true` rejects every certificate.
- Minimum signing-key strength is enforced: RSA moduli below 2048 bits are rejected.
- OCSP is **disabled by default** (`OcspMode::Disabled`). Set
`OcspMode::ResponderOnly` or `OcspMode::StapledAndResponder` in production.
- **SSRF-hardened egress**: URL scheme and private/loopback/link-local/CGNAT/IPv4-mapped-IPv6
ranges are blocked, DNS resolution is pinned against rebinding, and **HTTP redirects are
never followed** (a redirect cannot escape the validated target). Applies to AS2/AS4
egress, OCSP, and SMP clients.
- **AS4 message integrity**: the WS-Security signature covers the whole `eb:Messaging`
metadata block; the receive path binds the consumed block to the verified signature
(XML-Signature-Wrapping defence).
---
## ๐ Status
`asx-rs` is **beta quality**. Core AS2 and AS4 push/pull flows are implemented, tested
(913+ tests across unit + integration suites), and exercised in production integrations.
- โ
AS2 send/receive (signed, encrypted, compressed, sync + async)
- โ
AS4 push send/receive (signed, encrypted, dedup, fragment reassembly)
- โ
AS4 pull (with reliability classification)
- โ
WS-Security (RSA-SHA256, ECDSA-SHA256, ECDH-ES + ConcatKDF + AES-KW, XMLenc11)
- โ
OCSP + PKIX chain validation
- โ
Strict-mode production validation gate
- โ
Comprehensive testing infrastructure (bypass verifier, mock endpoint, keypair generators)
- โ ๏ธ Persistent storage backends (dedup/reconciliation) are trait-defined but not shipped
in-tree; deployers provide their own Redis/PostgreSQL/DynamoDB implementations.
---
## ๐ License
Licensed under either of:
- [MIT License](./LICENSE-MIT)
- [Apache License, Version 2.0](./LICENSE-APACHE)
at your option.