asx-rs 0.10.0

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

๐Ÿ“ฆ asx-rs

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, 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

[dependencies]
# AS4 โ€” PEPPOL / CEF eDelivery (RSA) or BDEW (EC/ECDH-ES) โ€” same code, key type decides
asx-rs = { version = "0.10", features = ["as4", "client", "server", "async-ocsp"] }

# AS2 + AS4 with compression
asx-rs = { version = "0.10", features = ["as2", "as4", "compression", "client", "server", "async-ocsp"] }

[dev-dependencies]
# Testing without PKI certificates (MockAs4Endpoint, bypass verifier, keypair generators)
asx-rs = { version = "0.10", 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)
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

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

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

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)

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

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

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

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

Flag Enables Default
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)
โ”‚   โ”‚   โ””โ”€โ”€ xmlenc    AES-GCM + (RSA-OAEP | ECDH-ES + ConcatKDF + AES-KW)
โ”‚   โ””โ”€โ”€ soap_builder  SOAP envelope construction
โ”œโ”€โ”€ 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:

Recipient cert key Key transport Key reference Profiles
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:

at your option.