asx-rs 0.6.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
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# AS4 Protocol Reference

Requires feature flag: `as4`

## Overview

AS4 (ebMS3 + eDelivery) support in `asx-rs` is exposed through free functions in `asx_rs::as4`.

Primary flows:

1. Outbound AS4 UserMessage send (signed, optionally encrypted).
2. Inbound push receive with dedup, WS-Security verification, and optional XML decryption.
3. Ordered push receive with conversation gate.
4. Pull receive with reliability integration.
5. Signal generation (receipt, error, pull request).

## Important Packaging Rule

Outbound payload packaging is MIME-only (`multipart/related`) with detached payload
attachments and cid references.  Embedded SOAP payload mode is unsupported for receive
and rejected on send.

---

## Signing: RSA-SHA256 and ECDSA-SHA256

`asx-rs` selects the XMLDSig signature algorithm automatically from the private key type:

| Key type | Algorithm URI | Profiles |
|---|---|---|
| RSA | `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` | PEPPOL, CEF eDelivery |
| EC (any curve) | `http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256` | BDEW AS4-Profil §2.2.6.2.1, BSI TR-03116-3 §9.1 |

No policy knob is required — pass the signing cert+key PEM and the library detects the
type.  BrainpoolP256r1 (BSI) and NIST P-256/P-384/P-521 are all supported.

### X509PKIPathv1 outbound token type

BDEW AS4-Profil §2.2.6.2.1 requires the `wsse:BinarySecurityToken` to use
`ValueType="...#X509PKIPathv1"` (a DER-encoded PKI path).  Enable it on the send policy:

```rust
use asx_rs::crypto::wssec::WsSecOutboundKeyInfoProfile;

let (policy, creds) = As4SendPolicyBuilder::new()
    .outbound_key_info_profile(WsSecOutboundKeyInfoProfile::X509PKIPathv1)
    // ... signing_cert_pem, signing_key_pem, action, service
    .build()?;
```

When `X509PKIPathv1` is set, the send path automatically:
1. Builds a `wsse:BinarySecurityToken` with `ValueType="...#X509PKIPathv1"` in the
   Security header (DER SEQUENCE { leaf certificate })
2. Emits `<wsse:SecurityTokenReference>` in `ds:KeyInfo` pointing to that token by
   `wsu:Id="X509PKIPathToken"`

---

## XML Encryption: automatic key transport selection

`asx-rs` selects the XML Encryption key transport algorithm from the recipient
certificate's public key type at call time.  No configuration is needed.

| Recipient cert key | Key transport | Key reference | Profiles |
|---|---|---|---|
| RSA | RSA-OAEP (SHA-256/MGF1-SHA-256) | `BinarySecurityToken` | PEPPOL, CEF eDelivery |
| EC (NIST P-256/P-384/P-521, BrainpoolP256r1/P384r1) | ECDH-ES ephemeral + ConcatKDF (NIST SP 800-56A §5.8.1) + AES-128 Key Wrap (RFC 3394) | `X509SKI` | BDEW AS4-Profil §2.2.6.2.2, BSI TR-03116-3 §9.2 |

### EC encryption XML structure

When the recipient has an EC key the outbound `<xenc:EncryptedKey>` uses:

```xml
<xenc:EncryptedKey>
  <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#kw-aes128"/>
  <ds:KeyInfo>
    <xenc:AgreementMethod Algorithm="http://www.w3.org/2009/xmlenc11#ECDH-ES">
      <xenc11:KeyDerivationMethod Algorithm="http://www.w3.org/2009/xmlenc11#ConcatKDF">
        <xenc11:ConcatKDFParams AlgorithmID="" PartyUInfo="" PartyVInfo="">
          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
        </xenc11:ConcatKDFParams>
      </xenc11:KeyDerivationMethod>
      <xenc:OriginatorKeyInfo>
        <ds:KeyValue>
          <dsig11:ECKeyValue>
            <dsig11:NamedCurve URI="urn:oid:1.3.36.3.3.2.8.1.1.7"/>  <!-- BrainpoolP256r1 -->
            <dsig11:PublicKey>BASE64_EPHEMERAL_PUBLIC_KEY</dsig11:PublicKey>
          </dsig11:ECKeyValue>
        </ds:KeyValue>
      </xenc:OriginatorKeyInfo>
      <xenc:RecipientKeyInfo>
        <ds:X509Data><ds:X509SKI>BASE64_SKI</ds:X509SKI></ds:X509Data>
      </xenc:RecipientKeyInfo>
    </xenc:AgreementMethod>
  </ds:KeyInfo>
  <xenc:CipherData><xenc:CipherValue>WRAPPED_CEK</xenc:CipherValue></xenc:CipherData>
</xenc:EncryptedKey>
```

`AlgorithmID`, `PartyUInfo`, and `PartyVInfo` are empty strings per BDEW AS4-Profil
(BSI TR-03116-3 §9.2 with the referenced default ConcatKDF parameters).

### ConcatKDF parameters

| Parameter | Value | Source |
|---|---|---|
| Hash | SHA-256 | XMLenc11 |
| Counter | 1 (single round, keydatalen ≤ 256 bits) | NIST SP 800-56A §5.8.1 |
| keydatalen | 128 bits (for kw-aes128) | Derived from key-wrap algorithm |
| AlgorithmID | `""` (empty) | BDEW AS4-Profil / BSI TR-03116-3 |
| PartyUInfo | `""` | BDEW AS4-Profil |
| PartyVInfo | `""` | BDEW AS4-Profil |

---

## `As4PushPolicy` — inbound receive policy

```rust
pub struct As4PushPolicy {
    /// Interop mode (Strict is default and required for production).
    pub interop: InteropMode,

    /// Reject inbound messages without a valid WS-Security signature.
    /// Default: `true` (fail-closed). Set `false` only for legacy partners.
    pub require_signed_push: bool,

    /// Reject inbound messages that are NOT XML-encrypted.
    ///
    /// Default: `false` (backward-compatible). Set `true` when encryption is
    /// mandatory (e.g. BDEW AS4-Profil §2.2.6.2.2).  The builder fails at
    /// construction if this is `true` but `inbound_decryption_key_pem` is unset.
    pub require_encrypted_inbound: bool,

    /// Private key PEM for decrypting inbound XML-encrypted payloads.
    /// Accepts both RSA and EC keys:
    ///   - RSA → RSA-OAEP (PEPPOL/CEF)
    ///   - EC  → ECDH-ES + ConcatKDF + AES-128 Key Wrap (BDEW)
    pub inbound_decryption_key_pem: Option<Arc<[u8]>>,

    /// Whether receipt verification failures close the operation.
    pub require_signed_receipt: bool,

    /// Timestamp freshness window (default: 5 minutes per eDelivery AS4 v1.15 §5.1.3).
    pub timestamp_freshness_window: Option<std::time::Duration>,

    /// Fail-closed audit event emission.
    pub fail_closed_audit_events: bool,

    /// Fragment group sender-scope policy.
    pub fragment_scope_policy: FragmentScopePolicy,
}
```

### Builder example — regulated deployment with mandatory encryption

```rust
let policy = As4PushPolicyBuilder::new()
    .inbound_decryption_key_pem(my_ec_private_key_pem)
    .require_encrypted_inbound(true)  // ← reject unencrypted messages fail-closed
    .build()?;
```

### Builder example — testing without PKI

```rust
// Only available with `testing` feature:
let policy = As4PushPolicyBuilder::new()
    .allow_unsigned_push(true)
    .fail_closed_audit_events(false)
    .timestamp_freshness_window(None)
    .build()?;
```

---

## `As4SendPolicy` — outbound send policy

Key fields:

| Field | Default | Notes |
|---|---|---|
| `sign` | `true` | Require signing in strict mode |
| `encrypt` | `false` | Set `true` + `recipient_cert_pem` for encrypted send |
| `outbound_key_info_profile` | `X509DataAndRsaKeyValue` | Use `X509PKIPathv1` for BDEW |
| `outbound_xmlenc_payload_algorithm` | `Aes128Gcm` | AES-128-GCM (eDelivery v1.15 default) |
| `payload_packaging_mode` | `MimeAttachment` | MIME-only (strict default) |

---

## Error Codes

| Code | Scenario | Notes |
|---|---|---|
| `ParseFailed` | Malformed SOAP/MIME/XML | Retry unlikely |
| `DecryptionFailed` | Wrong key, corrupt ciphertext, bad AES-KW integrity | Reject + audit |
| `SecurityVerificationFailed` | Bad signature, untrusted cert, timestamp out of window | Reject + audit |
| `PolicyViolation` | Unsigned but signing required; unencrypted but encryption required | Reject + signal error |
| `InteropViolation` | Missing required ebMS3 element (strict mode) | Reject |
| `ReliabilityFailure` | Non-durable dedup backend in strict mode | Fix configuration |

---



### Send

```rust
pub fn send_sync(
    session: &SessionContext,
    event_bus: &EventBus,
    request: As4SendRequest,
) -> Result<As4SendOutput>
```

Async-safe wrapper for Tokio services:

```rust
pub async fn send_async(
    session: &SessionContext,
    event_bus: &EventBus,
    request: As4SendRequest,
) -> Result<As4SendOutput>
```

Behavior notes:

1. SOAP envelope is generated and adapted for XOP/cid references.
2. MIME package is emitted as `multipart/related` output.
3. WS-Security signatures include detached payload reference for MIME attachment.
4. Optional XML encryption is applied before outbound packaging.

### Receive push (owned)

```rust
pub fn receive_push_with_dedup_sync(
    session: &SessionContext,
    event_bus: &EventBus,
    request: As4ReceivePushSyncRequest<'_>,
) -> Result<As4ReceivePushOutput>
```

Sync fragment-aware wrapper for large-message reassembly:

```rust
pub fn receive_push_with_dedup_sync_fragment_aware(
    session: &SessionContext,
    event_bus: &EventBus,
    request: As4ReceivePushSyncFragmentAwareRequest<'_>,
) -> Result<As4ReceivePushProgress>
```

Async-safe wrapper for Tokio services:

```rust
pub async fn receive_push_with_dedup_async(
    session: &SessionContext,
    event_bus: &EventBus,
    request: As4ReceivePushRequest,
    dedup_backend: Arc<dyn DedupStorage>,
) -> Result<As4ReceivePushOutput>
```

### Receive push (ordered)

```rust
pub async fn receive_push_ordered(
    session: &SessionContext,
    event_bus: &EventBus,
    request: As4ReceivePushOrderedRequest<'_>,
) -> Result<As4ReceivePushOutput>
```

### Receive pull with reliability

```rust
pub async fn receive_pull_with_reliability(
    session: &SessionContext,
    event_bus: &EventBus,
    request: As4ReceivePullWithReliabilityRequest<'_>,
) -> Result<As4ReceivePullOutput>
```

### Token-enforced strict runtime sessions

For regulated deployments that require explicit startup proof, bind validated
session context once and then use standard AS4 entry points:

```rust
let strict_session = asx_rs::presets::session_with_strict_runtime_bootstrap_token(
    "as4_receive_push_sync",
    &bootstrap_token,
    &session,
)?;

let out = asx_rs::as4::receive_push_with_dedup_sync(
    &strict_session,
    &event_bus,
    asx_rs::as4::As4ReceivePushSyncRequest {
        request,
        dedup_backend,
    },
)?;
```

In non-testing builds, strict interop AS4 entry points fail closed unless the
session is startup-validated with
`asx_rs::presets::session_with_strict_runtime_bootstrap_token(...)`.

### Strict production clustered topology gate

`As4PullStore` and `As4ConversationOrderGate` are process-local components.
Before accepting traffic in clustered deployments, fail closed unless you have
distributed replacements:

```rust
use asx_rs::presets::{
    DeploymentTopology,
    validate_strict_production_as4_topology_readiness,
};

validate_strict_production_as4_topology_readiness(
    "startup",
    DeploymentTopology::Clustered,
    Some(pull_store),
    Some(conversation_gate),
)?;
```

### Queue pull payload with reliability

```rust
pub async fn enqueue_pull_with_reliability(
    session: &SessionContext,
    event_bus: &EventBus,
    request: As4EnqueuePullWithReliabilityRequest<'_>,
) -> Result<As4PullEnqueueOutcome>
```

Use this API for production integrations. It emits overflow audit events and
queues reconciliation for dropped/rejected messages under configured overflow
policy.

### Signal generation

`generate_receipt`, `generate_receipt_with_nri`, `generate_error_signal`, and
`generate_pull_request` are available in `asx_rs::as4`.

## Core Types

1. `As4SendPolicy` / `As4SendPolicyBuilder`
2. `As4PushPolicy` / `As4PushPolicyBuilder`
3. `As4PullPolicy`
4. `As4ReceivePushRequest`
5. `As4ReceivePushOrderedRequest`
6. `As4ReceivePushOrderedFragmentAwareRequest`
7. `As4ReceivePushAsyncFragmentAwareRequest`
8. `As4ReceivePushSyncFragmentAwareRequest`
9. `As4ReceivePushSyncRequest`
10. `As4SendOutput` and `As4ReceivePushOutput`
11. `As4EnqueuePullWithReliabilityRequest`
12. `As4ReceivePullWithReliabilityRequest`
13. `PMode` / `PModeRegistry`

## Interop Notes

1. Strict mode is default and recommended.
2. Relaxed interop remains available by feature/profile policy for scoped exceptions.
3. Inbound payloads must be multipart/related with detached attachment bytes.
4. Signed inbound messages are validated in pinned-sender mode and require `SessionContext.cert_handle.fingerprint_sha256` to be configured.

## Test Service and P-Mode

`asx_rs::as4::test_service` and `asx_rs::as4::pmode` provide profile/test-service helpers for standards-aligned partner agreements and conformance workflows.

## SMP Integration: Dynamic Partner Discovery (PEPPOL / CEF)

In PEPPOL and CEF eDelivery networks, Access Points (APs) discover each other
dynamically via the **Service Metadata Publisher (SMP)** protocol
([OASIS BDX SMP 1.0](https://docs.peppol.eu/edelivery/smp/)).  Before sending
an AS4 message, the sender resolves the recipient's endpoint URL and signing
certificate from the SMP.

Enable the `smp` module with the `client` feature:

```toml
asx-rs = { version = "0.5", features = ["as4", "client", "async-ocsp"] }
```

### Lookup and Register a Runtime P-Mode

```rust
use asx_rs::smp::{SmpClient, SmpLookupRequest};
use asx_rs::as4::pmode::{PMode, PModeRegistry, MepType, PModeSecurity};
use std::sync::Arc;

async fn build_registry_from_smp() -> asx_rs::Result<Arc<PModeRegistry>> {
    // 1. Look up the recipient endpoint via PEPPOL SMP.
    let client = SmpClient::new("acc.edelivery.tech.ec.europa.eu");
    let endpoint = client.lookup_endpoint(SmpLookupRequest::peppol(
        "0088:1234567890123",       // recipient participant ID
        "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
        "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
    )).await?;

    // 2. Validate the SMP-provided certificate against your trust anchors before use.
    //    The certificate_der_b64 field holds a base64-encoded DER X.509 certificate.
    let partner_cert_pem: String = if let Some(cert_b64) = &endpoint.certificate_der_b64 {
        // Convert DER → PEM (pseudocode; use openssl::x509::X509::from_der in production).
        format!("-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----\n", cert_b64)
    } else {
        return Err(asx_rs::AsxError::new(
            asx_rs::ErrorCode::InvalidInput,
            "SMP endpoint has no certificate",
            asx_rs::ErrorContext::new("smp_lookup"),
        ));
    };

    // 3. Build a P-Mode from the resolved endpoint.
    let pmode = PMode {
        partner_id:      "partner-acme".to_string(),
        service:         "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1".to_string(),
        action:          "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0".to_string(),
        mep:             MepType::OneWayPush,
        endpoint_url:    endpoint.url.clone(),
        security:        PModeSecurity {
            sign:    true,
            encrypt: false, // PEPPOL BIS Billing 3.0 mandates sign-only
            ..Default::default()
        },
        ..Default::default()
    };

    // 4. Register the P-Mode for use at send time.
    let mut registry = PModeRegistry::new();
    registry.register(pmode);
    Ok(Arc::new(registry))
}
```

### SSRF Considerations

The SMP client validates the constructed URL (scheme, host, path) before
making any network request.  The `sml_zone` value in `SmpClient::new(...)` is
**operator-controlled** — never pass user-supplied data as the SML zone.  See
the `smp` module documentation for the full SSRF mitigation notes.

### Certificate Pinning After SMP Lookup

Always validate the certificate returned by SMP before adding it to a
`SessionContext`:

1. Decode the `certificate_der_b64` field and parse it with `openssl::x509::X509::from_der`.
2. Check the certificate against your PEPPOL trust anchor (e.g. the PEPPOL
   Intermediate CA certificate for the relevant PKI zone).
3. Only then construct a `CertHandle` with `fingerprint_sha256` set to the
   certificate's SHA-256 fingerprint and `trust_anchor_pems` containing your
   validated PEPPOL root CA.

Accepting an SMP certificate without trust-anchor validation exposes you to
SMP-layer MITM attacks.

### Refreshing P-Modes

SMP endpoint records are time-limited (see `service_expiration_date`).
Implement a background task that re-resolves expiring or expired entries and
calls `PModeRegistry::register` on a new registry instance, then swaps the
`Arc<PModeRegistry>` atomically.  Because `PModeRegistry` is immutable after
construction, in-flight sends always use a consistent snapshot.