asx-rs 0.12.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
# Security Model

## Design Principles

1. **Fail-closed by default** — absent configuration causes rejection, not acceptance.
2. **Explicit trust transitions** — trust is not implicit; it must be established at each cryptographic stage.
3. **No silent bypasses** — signature verification results are always propagated; `let _ = verify(...)` is forbidden.
4. **Algorithm agility with compliance first** — AS4 WS-Security runtime verification is strict-only (no legacy inbound fallback paths); any non-default compatibility behavior must be explicit and scoped through interop policy outside WS-Security cryptographic verification.
5. **Security invariants are floors, not defaults** — a safe default that any configuration layer can silently overwrite is not an invariant. Where a policy layer is publicly mutable, the safe value is additionally pinned by a floor that validation enforces; see [Profile security floor](#profile-security-floor).

---

## Profile Security Floor

`ProfileStack.overrides` and `ProfileStack.partner_overrides` are public `Vec`
fields, so a single overlay can rewrite `SecurityPolicy` for one partner.
`BaseProfile::security_floor` is the invariant that survives that.

| Property | Behaviour |
|---|---|
| Default | `SecurityPolicy::SIGN_AND_ENCRYPT` — matches PEPPOL, CEF eDelivery and BDEW AS4-Profil v1.2 §2.2.6.2.2 |
| Overridable by a layer | **No.** The floor lives on `BaseProfile` only, so no extension, global override, partner overlay, or regional pack can lower it |
| Enforced by | `ProfileStack::validate()`, against the **resolved** policy of the deployment baseline and of every declared partner |
| Enforced by a host mandate | `validate_with_floor(floor)` — takes the stronger of the host floor and the profile's, so it can only tighten |
| Visible in audit | Recorded in `EffectivePolicySnapshot::security_floor`; `diff_effective_policy_snapshots` grades a **lowered** floor as `High` risk and blocks release even when the resolved policy is unchanged |

A layer that weakens security while still clearing the floor is reported as a
`ProfileLintCode::SecurityRelaxation` lint at `ProfileLintSeverity::Critical`;
`ProfileValidationOptions::forbid_security_relaxation` escalates it to an error.

**Interop mode is a separate axis.** Neither `InteropMode::Strict` nor the
`interop-strict` feature implies a strict security policy — they govern header
and ambiguity handling. Enforce the security axis with a floor.

---

## Certificate and Trust Configuration

All certificate and trust material is carried in `CertHandle`, which is set on `SessionContext` via `.with_cert_handle(handle)`.

```rust
pub struct CertHandle {
    pub signing_key_pem: String,          // PEM private key for signing (AS2 S/MIME or AS4 WS-Security)
    pub signing_cert_pem: String,         // PEM certificate corresponding to signing_key_pem
    pub encryption_cert_pem: String,      // PEM certificate for encrypting to this party
    pub trust_anchor_pems: Vec<String>,   // PEM CA certificates for PKIX chain validation
    pub fingerprint_sha256: String,       // Expected SHA-256 fingerprint of partner's signing cert (empty = pinning disabled)
    pub ocsp_config: OcspConfig,          // OCSP mode and responder override
}
```

### PKIX chain validation

When `RevocationPolicy::require_chain_validation = true` (the default when at least one trust anchor is provided), all signer certificates are validated against `trust_anchor_pems` using PKIX chain building. An empty `trust_anchor_pems` with `require_chain_validation = true` **fails closed** — no certificate will pass validation.

To explicitly allow any certificate (testing only):
```rust
RevocationPolicy {
    require_chain_validation: false,
    trust_anchor_pems: vec![],
    ..Default::default()
}
```

### Certificate fingerprint pinning

When `fingerprint_sha256` is non-empty, the signer certificate's SHA-256 fingerprint is compared against this value after PKIX validation. A mismatch fails the receive.

For AS4 push receive, `As4PushPolicy` now controls trust mode explicitly:
1. Pinned sender mode: requires `fingerprint_sha256` to be configured for signed inbound message verification.

```rust
CertHandle {
    fingerprint_sha256: "AA:BB:CC:...".to_string(),  // enforce pinning
    ..
}
```

---

## OCSP (Online Certificate Status Protocol)

OCSP is configured per session via `OcspConfig` in `CertHandle`:

```rust
pub struct OcspConfig {
    pub mode: OcspMode,
    pub responder_override: Option<String>,  // Optional URL override
}

pub enum OcspMode {
    Disabled,
    Required,
    BestEffort,  // OCSP failure does not fail the receive
}
```

OCSP responses are fetched via `reqwest` (`async-ocsp` feature, enabled by default). The async path (`fetch_ocsp_responses_with_cache_async*`) is the preferred entry point and runs entirely on the caller's Tokio runtime with no additional thread overhead. The sync compatibility wrapper (`fetch_ocsp_responses_with_cache_provider_scoped`) bridges into async by using `tokio::task::block_in_place` + `block_on` on the current runtime handle — no dedicated OS thread is spawned. Responses are cached (`ProcessLocalOcspResponseCache`, 5-minute TTL, max 512 entries with lazy eviction) to minimise live round-trips — in steady state, most certificate checks are served from cache.

### Response freshness

OCSP responses are validated for freshness:
- `thisUpdate` must be within 300 seconds of the current time (clock skew tolerance).
- `nextUpdate` must not be more than 86400 seconds (24 hours) in the past.

Stale or expired OCSP responses are rejected.

---

## Cryptographic Algorithms

### AS2 (S/MIME)

| Operation | Algorithm |
|---|---|
| Signing | S/MIME CMS — RSA + SHA-256 |
| Encryption | S/MIME CMS — AES-256 CBC (or AES-128-GCM for newer partners) |
| MIC computation | SHA-256 over `Content-Type: …\r\n\r\n<payload>` (RFC 4130 §7.3.1) |

### AS4 (WS-Security / XML Encryption)

| Operation | Algorithm | URI |
|---|---|---|
| Payload encryption (outbound) | AES-256-GCM (XMLenc11) | `http://www.w3.org/2009/xmlenc11#aes256-gcm` |
| Payload encryption (inbound) | AES-128-GCM or AES-256-GCM (XMLenc11) | `http://www.w3.org/2009/xmlenc11#aes128-gcm`, `http://www.w3.org/2009/xmlenc11#aes256-gcm` |
| Key transport | RSA-OAEP (XMLenc11) | `http://www.w3.org/2009/xmlenc11#rsa-oaep` |
| Key transport MGF | MGF1-SHA256 | `http://www.w3.org/2009/xmlenc11#mgf1sha256` |
| Key transport digest | SHA-256 | `http://www.w3.org/2001/04/xmlenc#sha256` |
| XML Signature | RSA-SHA256 | `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` |
| Canonicalization | Exclusive C14N | `http://www.w3.org/2001/10/xml-exc-c14n#` |

ASX uses **AES-256-GCM** (authenticated encryption) for outbound AS4 encryption and accepts only XMLenc11 AES-GCM inbound. Legacy AES-CBC and XMLenc 1.0 OAEP variants are rejected fail-closed to reduce downgrade and padding-oracle risk.

---

## WS-Security XML Signatures

### Canonicalization (C14N)

WS-Security signature computation uses XML Exclusive C14N (W3C `exc-c14n`).

> **Whitespace is signed.** Exclusive and Inclusive C14N differ *only* in
> namespace rendering; neither removes whitespace-only text nodes. ASX has no
> option to strip them — a `strip_blank_text` knob existed until 0.12.0 and made
> every signature non-interoperable. Conformance is
> now checked against xmlsec1, the XMLDSig reference implementation, by
> `tests/interop_xmlsec_oracle.rs`.

The implementation:
- Correctly handles namespace propagation for visibly-utilized namespaces.
- Forwards processing instruction nodes (`<?target data?>`).
- Implements `InclusiveNamespaces PrefixList` per W3C Exc-C14N §2.1 — ancestor namespace bindings for listed prefixes are rendered even when not directly utilized at the element.
- Strips comments in default mode; preserves them when `include_comments = true`.
- Sorts attributes in lexicographic order (namespace URI, local name) as required by C14N.

Validated against W3C C14N test vectors (namespace propagation, attribute ordering, text/attribute escaping, PI forwarding, comment stripping, comment preservation).

### Payload attachment coverage

The AS4 payload — the actual business document — travels as a detached MIME
part, not inside the SOAP envelope. Signing `eb:Messaging` and the SOAP Body
therefore proves nothing about it.

On receive, when a signature is present, ASX requires the payload attachment to
be covered by a verified `ds:Reference URI="cid:…"` matching the attachment's
`Content-ID`. A message whose signature omits the attachment reference is
rejected with `ErrorCode::SecurityVerificationFailed` — otherwise an
intermediary could swap the payload without invalidating the signature.

Two supporting rules make this enforceable:

| Rule | Why |
|---|---|
| The SOAP body must carry an `xop:Include href="cid:…"` | Without a Content-ID there is nothing to match a `cid:` reference against, so coverage could never be proven. A multipart message without one is rejected as having no payload attachment. |
| `href` is matched in both XML quoting styles | `href='cid:…'` is legal XML; matching only the double-quoted form left the attachment unidentified. |

### Signed scope and XML Signature Wrapping defence

The AS4 push signature covers **three** references: the entire `eb:Messaging`
header block (`wsu:Id="as4-messaging"` — all `UserMessage` routing/authorization
metadata: From, To, Service, Action, MPC, MessageProperties, PartInfo), the SOAP
Body, and a detached `cid:` reference for the MIME payload attachment. (Earlier
revisions signed only `ebms:MessageId`, leaving the routing metadata tamperable.)

On receive, verification returns the set of verified same-document `wsu:Id`s and
the AS4 layer requires that the document contains **exactly one** `eb:Messaging`
block whose `wsu:Id` is in that set. This binds the block the pipeline routes on
to the block the signature actually covered, defeating XML Signature Wrapping
(relocating the signed element and injecting an unsigned replacement).

### Signature verification

Signature verification uses:
1. Digest verification over C14N-serialized referenced elements.
2. RSA/ECDSA signature verification using `secure_eq` (constant-time comparison) for digest values.
3. Minimum signing-key strength enforcement (RSA `< 2048` bits is rejected).
4. PKIX chain validation of the signing certificate.
5. OCSP status check (if configured).
6. Binding of the consumed `eb:Messaging` block to the verified signature (above).

Verification is fail-closed: any error at any step propagates immediately via `?`. The caller cannot ignore a failed verification.

### `wsu:Timestamp` validation

Inbound WS-Security timestamps are validated:
- `wsu:Created` must be within 5 minutes of the current time.
- `wsu:Expires` (if present) must not be in the past.

Outbound timestamps include `wsu:Created` (now) and `wsu:Expires` (now + 5 minutes).

---

## Non-Repudiation of Receipt (AS4 send path)

A counterparty's `eb:Receipt` is only delivery evidence once it has been checked
against the message that was actually sent. `asx_rs::as4::verify_sync_response`
(and `As4HttpTransport::send_and_verify`) performs that check; nothing else in
the send path does.

Threat model for the receipt, and what defends against each:

| Threat | Defence |
|---|---|
| Attacker or misconfigured MSH returns a receipt it did not sign | WS-Security signature verified against the pinned partner certificate (`cert_handle.fingerprint_sha256`), trust anchors and revocation policy |
| **Signature wrapping** — leave a genuinely signed element in place so the signature verifies, and append an unsigned acknowledgement for another message | the `eb:SignalMessage` acted on must itself be covered by the verified signature, directly or via a signed ancestor; at most one `eb:Messaging` and one `eb:SignalMessage` are accepted |
| Element injection — a duplicate `eb:RefToMessageId` / `eb:MessageId` / `eb:Timestamp` shadowing the real one | a repeated `eb:MessageInfo` child is rejected outright rather than resolved first-wins |
| Digests parked outside the `eb:Receipt` to fake non-repudiation | `NonRepudiationInformation` is read only from inside the `eb:Receipt` of that SignalMessage |
| Counterparty acknowledges a *different* message | `eb:RefToMessageId` compared to the sent `message_id` |
| Counterparty acknowledges *different bytes* — the core NRR guarantee | every `ds:Reference` of the sent message's own signature must be echoed by a `MessagePartNRInformation` entry with a matching digest algorithm and digest value |
| Receipt acknowledges only part of a multi-part message | a sent reference with no echoed entry is rejected; the MIME package is unwrapped so `cid:` attachment references are covered |
| Padding a valid entry with a second, conflicting one for the same URI | duplicate `MessagePartNRInformation` URIs are rejected outright |
| Entries for URIs the sender never signed | rejected under `reject_unexpected_references` (on in `regulated()`) |
| Replay of an old receipt | `eb:Timestamp` freshness window, 5 minutes by default; a receipt with **no** timestamp is rejected rather than skipping the check |
| A `ds:Signature` element that is not a verifiable enveloped signature | treated as a verification failure, never downgraded to "unsigned" |
| An `eb:Error` for someone else's message causing a wrongful dead-letter | the error signal's correlation (`eb:MessageInfo/eb:RefToMessageId` or the `eb:Error/@refToMessageId` attribute) must match the sent message when present |
| A signal that both acknowledges and rejects | rejected as ambiguous — neither confirmed delivered nor confirmed rejected |
| Resource exhaustion from a hostile response | 256 KiB body cap, XML element-count cap, bounded NRI and error-entry counts |

The same wrapping and ambiguity defences apply to the **inbound** receipt path
(`receipt_payload` on `As4ReceivePushRequest`), which shares the parser and the
signature-coverage binding.

### Error signals are not authenticated

`As4SyncSignal::Error` reports what the connection returned; it does not prove
it. Error signals are typically unsigned, so treat one as a routing hint (retry
vs dead-letter), never as evidence about the message's fate. Only
`As4VerifiedReceipt::is_non_repudiation_evidence()` asserts a cryptographically
proven outcome.

A digest that is present and wrong is **always** an error
(`SecurityVerificationFailed`) regardless of policy — `As4NonRepudiation` has no
"mismatch" variant, so a mismatch cannot be returned as a value the caller might
ignore. `As4ReceiptPolicy::require_non_repudiation = false` only tolerates the
*absence* of digests, and the resulting `As4NonRepudiation::NotProvided` makes
`As4VerifiedReceipt::is_non_repudiation_evidence()` return `false`.

Do not detect receipts by scanning the response body for `<eb:Receipt`.
Namespace prefixes are arbitrary and element text may be CDATA-wrapped, so a
substring match yields false delivery failures against conformant partners — and
it cannot verify non-repudiation at all.

---

## `InsecureBypassTrustVerifier`

```rust
use asx_rs::lifecycle::InsecureBypassTrustVerifier;
```

**For testing only.** This verifier passes any payload as fully trusted and decryptable without performing any cryptographic checks. Its name is intentionally explicit.

Never use `InsecureBypassTrustVerifier` in production. It bypasses:
- Signature verification
- PKIX chain validation
- OCSP status checking
- Fingerprint pinning

---

## Payload Size Limits

All inbound reads are bounded. The default limit is **256 MiB** (`DEFAULT_MAX_BODY_BYTES`). This applies to:
- `asx_rs::as2::receive_with_mdn_with_reliability`
- `asx_rs::as4::receive_push_with_dedup_sync`
- `transport::server` layer (axum handlers)

Override per-session:
```rust
As2PushPolicy::builder().max_body_bytes(64 * 1024 * 1024)  // 64 MiB
```

An over-limit body fails with `ErrorCode::PayloadTooLarge`, which maps to
HTTP 413. (Before v0.11.0 `wire::enforce_payload_limit` returned
`PolicyViolation` → HTTP 403, misreporting an oversize body as an authorization
failure; ingress handlers matching on `PolicyViolation` for size must be
updated.)

AS4 synchronous receipts are bounded separately and much more tightly at
**256 KiB** (`as4::DEFAULT_MAX_RECEIPT_BYTES`, tunable via
`As4ReceiptPolicy::max_receipt_bytes`) — a SignalMessage carries no business
payload, so anything larger is malformed or hostile.

---

## Temp File Security

Streaming receive operations that require on-disk spooling (e.g., for signature verification rewinding) use `tempfile::NamedTempFile` for atomic, exclusive temp file creation. This prevents symlink attacks on world-writable `/tmp`.

---

## Operator Hardening Expectations (Core Dumps and Host Memory)

ASX zeroizes owned private-key PEM buffers on drop where possible, and recent send-path refactors minimize transient key-buffer duplication. However, ASX is only a library and cannot enforce host OS process-dump policy, swap policy, or debugger attach policy.

Production operators are expected to harden runtime environments accordingly:

1. Disable process core dumps for ASX-hosting services (for example `ulimit -c 0`, systemd `LimitCORE=0`, container runtime equivalents).
2. Restrict dumpability and ptrace/debug attachment to trusted operators only.
3. Ensure swap/pagefile policy is encrypted or disabled for regulated deployments handling private keys.
4. Keep crash-reporting pipelines from uploading raw process memory unless a formally approved secret-scrubbing policy is in place.

These controls are mandatory complements to in-process zeroization when handling cryptographic private key material in production.

---

## SMP Discovery Verification

`smp::SmpClient` resolves the AS4 endpoint URL and the recipient's certificate.
TLS authenticates the SMP *host*, not the metadata it serves, so a rogue or
compromised SMP could otherwise redirect traffic and substitute its own
recipient certificate. Verify the response signature with
`SmpSignaturePolicy::Verify`.

| Control | Status |
|---|---|
| SMP URL SSRF validation + DNS pinning + no redirects | ✅ enforced |
| `ds:Signature` **present** on the response | ✅ enforced by default (`SmpSignaturePolicy::RequireSignaturePresent`) |
| `ds:Signature` **verifies** | ✅ with `SmpSignaturePolicy::Verify` |
| SMP certificate chains to the network SMP CA | ✅ with `SmpSignaturePolicy::Verify` |

```rust
let config = SmpConfig {
    signature_policy: SmpSignaturePolicy::verify_with_trust_anchors(vec![smp_ca_pem]),
    ..SmpConfig::peppol_production()
};
```

`SmpEndpoint::verified_signer_fingerprint_sha256` is `Some` only when the
signature was actually verified — check it rather than assuming, since the
other two policies leave it `None`. `SmpEndpoint::signed_document` still
returns the exact response bytes for callers that want to verify independently.

**Assurance note.** The enveloped whole-document path is newer than the AS4
signing path and is not yet cross-validated against an independent
implementation, so treat it accordingly.

---

## Known Limitations

| Limitation | Mitigation |
|---|---|
| SMP enveloped-signature path not yet cross-validated against another stack | Pin partner certificates out of band as defence in depth (see above) |
| Custom XML Exclusive C14N implementation | Validated against W3C test vectors and interop compatibility matrix; not yet replaced by a vetted library |
| In-memory dedup provides no replay protection across restarts | Use `TtlDedupStorage` with a distributed backend for production; document required 48h window per RFC 4130 §5.2.1 |
| No TLS mutual authentication (mTLS) at the library level | Configure mTLS at the TLS terminator / reverse proxy layer |
| OCSP `thisUpdate`/`nextUpdate` clock skew tolerance is fixed at ±300s / 86400s | Adjust via `OcspConfig` if partner OCSP responders have larger clock drift |
| OCSP sync wrapper uses `block_in_place`/`block_on` (not a new OS thread) — must be called from a multi-thread Tokio runtime | Use the async `fetch_ocsp_responses_with_cache_async` entry point directly from async call sites; at very high message rates with many distinct certificates, consider a shared persistent OCSP cache backend |

---

## Crypto Backend Roadmap

### Current State: Mixed OpenSSL + Pure-Rust

`asx-rs` currently uses two separate crypto ecosystems:

| Subsystem | Current backend | Role |
|---|---|---|
| AS2 S/MIME signing / encryption | `openssl` (C FFI) | CMS `SignedData` / `EnvelopedData` |
| AS4 WS-Security XML signing | `openssl` (C FFI) | RSA-SHA256 `ds:Signature`, RSA-OAEP key wrap |
| AS4 payload symmetric encryption | `aes-gcm` (pure Rust) | AES-128/256-GCM `xenc:EncryptedData` |
| X.509 certificate parsing | `openssl` (C FFI) | Trust-anchor validation, chain building, OCSP |

This mixed model has several implications:

- **Build system**: consumers must have a working OpenSSL installation (or
  accept the `openssl-sys` vendored build). Cross-compilation (e.g., to
  `x86_64-unknown-linux-musl` static binaries) requires extra care.
- **FIPS compliance**: OpenSSL can be compiled in FIPS mode; the `aes-gcm`
  crate is *not* FIPS 140-2 validated. Regulated deployments (US federal,
  healthcare) requiring FIPS-validated crypto across **all** algorithms must
  either replace `aes-gcm` with the OpenSSL AES-GCM primitives or wait for
  the pure-Rust migration path below.
- **Vulnerability management**: OpenSSL and `aes-gcm` have separate CVE
  timelines and patch cadences. Both must be tracked independently.

### Migration Path: Full Pure-Rust Crypto

The long-term goal is to eliminate the OpenSSL C-FFI dependency and converge
on a single pure-Rust crypto stack. The planned migration path is:

1. **Phase 1 (in progress)**: Symmetric crypto is already pure Rust (`aes-gcm`).
2. **Phase 2**: Replace OpenSSL X.509 parsing with `x509-cert` + `rustls-pki-types`.
3. **Phase 3**: Replace OpenSSL RSA operations (CMS key wrap, WS-Security signing)
   with `rsa` (pure Rust) or `aws-lc-rs` (FIPS-validated pure-Rust interface).
4. **Phase 4**: Remove the `openssl` crate dependency entirely. Provide an
   optional `openssl-fips` feature gate for regulated environments that require
   FIPS 140-2 validated modules.

> **Note**: Phases 2–4 are pre-1.0 roadmap items and will be treated as
> breaking-dependency changes. Subscribe to the GitHub releases feed or
> `CHANGELOG.md` for status.

### FIPS Deployment Today

If you need FIPS-validated crypto today, use the following configuration:

1. Compile OpenSSL in FIPS mode (OpenSSL 3.x with `OPENSSL_FIPS=1`).
2. Do **not** enable AS4 XML encryption (set `As4SendPolicy { encrypt: false, .. }` for
   outbound; for inbound, `As4PushPolicy::default()` already allows unencrypted payloads
   when none arrive encrypted), since the `aes-gcm` symmetric layer is not FIPS
   140-2 validated.
3. Contact your compliance officer before enabling AS4 payload encryption in
   a regulated deployment until Phase 3 is complete.