asx-rs 0.9.0

AS2 and AS4 B2B messaging library for Rust โ€” signing, encryption, MDN, and ebMS3/AS4 profile support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# ๐Ÿ“ฆ 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

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

- [MIT License]./LICENSE-MIT
- [Apache License, Version 2.0]./LICENSE-APACHE

at your option.