mako-as4 0.20.0

BDEW MaKo AS4 profile — AS4/ebMS3 transport for German energy market communication
Documentation
# mako-as4

**BDEW MaKo AS4 profile — sign + encrypt with BrainpoolP256r1.**

Encodes the **BDEW AS4-Profil v1.2** requirements on top of
[`asx-rs`](https://crates.io/crates/asx-rs), providing pre-configured
P-Modes, security policy constants, a bilateral partner directory, and
test helpers for the German electricity and gas AS4 network.

---

## Security policy — stricter than the AS4 baseline

`bdew_mako_profile_stack()` sets `require_signature: true` and
`require_encryption: true` per **BDEW AS4-Profil v1.2 §2.2.6.2.2**.

It also declares that pair as the stack's **security floor**, which `asx-rs`
enforces across the base and every override layer during `validate()`. A layer
that relaxes it is rejected with `ProfileValidationCode::SecurityFloorViolation`.

The floor is what makes this strict enough. The generic AS4 invariant rejects a
layer only when it disables signing *and* encryption — a sensible baseline, but
weaker than §2.2.6.2.2, which mandates both. Since `ProfileStack::overrides` and
`partner_overrides` are public fields, a single partner overlay that keeps
signing and turns encryption off is reachable by configuration, and without a
declared floor it would validate cleanly while every message to that partner
went out in the clear.

Validation runs at startup, so a downgraded profile never serves traffic.

## BDEW AS4-Profil v1.2 requirements

| Requirement | Value | Spec |
|---|---|---|
| Transport | HTTPS + mTLS (TLS 1.2 minimum) | §2.2.1 |
| SOAP version | 1.2 | §2.2.3 |
| MEP | One-Way/Push (mandatory) | §2.2.5 |
| **Signing algorithm** | **ECDSA-SHA256 + BrainpoolP256r1** | §2.2.6.2.1 / BSI TR-03116-3 §9.1 |
| **Signing token type** | **`BinarySecurityToken` / X509PKIPathv1** | §2.2.6.2.1 |
| **Encryption** | **ECDH-ES + ConcatKDF + AES-128-GCM — mandatory** | §2.2.6.2.2 / BSI TR-03116-3 §9.2 |
| **Key reference** | **X509SKI** | §2.2.6.2.2 |
| Payload | gzip-compressed, carried as `application/octet-stream` in its own MIME part; the SOAP Body stays empty | §2.2.3.2 |
| Party ID | 13-digit GLN, ISO 6523 ICD 0088 | §2.2.4 |
| Retry window | 72 hours, up to 5 attempts | §2.2.7 |
| Deduplication | Required (persistent dedup store) | §4.2 |
| Synchronous receipt | Mandatory | §4.6.3 |

> **All algorithms are auto-detected** — supply an EC (BrainpoolP256r1) signing key
> and asx-rs automatically uses ECDSA-SHA256. Supply an EC encryption certificate
> and asx-rs automatically uses ECDH-ES + ConcatKDF + AES-128-GCM.

AS4 became mandatory for electricity on **1 April 2024** (BK6-22-024) and
for gas on **1 April 2025** (BK7-22-023).

---

## Certificate triplet

BDEW requires **three separate X.509 keypairs**, all using BrainpoolP256r1:

```
mTLS certificate         WS-Security signing cert    XML Encryption cert
KeyUsage: digitalSig     KeyUsage: digitalSignature  KeyUsage: keyAgreement
        ↕ mTLS                  ↕ sign payload              ↕ ECDH-ES
   HTTPS transport          outbound messages           recipient's pubkey
```

---

## API overview

| Module | Contents |
|---|---|
| `constants` | BDEW-specific service URIs, algorithm identifiers (`SIG_ALGO_ECDSA_SHA256`, `ENC_KEY_AGREEMENT_ECDH_ES`, …) |
| `pmode` | `BdewAction` enum + `bdew_pmode()` / `bdew_pmode_sign_only()` factory functions; `WsSecOutboundKeyInfoProfile` |
| `profile` | `BdewAs4Profile` entry point + `bdew_mako_profile_stack()` + `bdew_push_policy()` |
| `partner_directory` | `PartnerDirectory` — GLN-to-endpoint resolution |
| `server` *(feature)* | Axum AS4 inbound router + `bdew_router_config()` |
| `testing` *(feature)* | `BdewTestPki`, `generate_self_signed_bdew_keypair()`, `MockAs4Endpoint` |

---

## Quick start

```rust
use mako_as4::{BdewAs4Profile, BdewAction, bdew_pmode, constants};

// 1. Register bilateral P-Modes for each trading partner.
let mut profile = BdewAs4Profile::new();
profile.register_partner_all_actions(
    "9900000000001",
    "https://partner.example/as4/inbox",
);

// 2. Register the partner's EC encryption certificate (BrainpoolP256r1).
//    asx-rs auto-selects ECDH-ES when an EC cert is supplied.
let partner_encrypt_cert_pem: Vec<u8> = std::fs::read("/etc/partner/encrypt.pem").unwrap_or_default();
profile.register_partner_encryption_cert("9900000000001", partner_encrypt_cert_pem);

// 3. Fail-fast at startup.
profile.validate().expect("BDEW MaKo profile must satisfy all security invariants");

// 4. Build inbound push policy with own decryption key.
let own_decrypt_key_pem: Vec<u8> = std::fs::read("/etc/certs/as4-encrypt.key.pem").unwrap_or_default();
let push_policy = mako_as4::bdew_push_policy(Some(own_decrypt_key_pem));
// push_policy.require_encrypted_inbound == true (rejects unencrypted inbound)
```

---

## Testing without WIRK certificates

Enable the `testing` feature to generate BrainpoolP256r1 test keypairs in memory.
The test helpers build on the `asx-rs` testing API:

```bash
cargo add mako-as4 --dev --features testing
```

### Full sign+encrypt round-trip test

```rust
use mako_as4::testing::{BdewTestPki, MockAs4Endpoint};
use asx_rs::core::SessionContextBuilder;
use asx_rs::observability::EventBus;
use asx_rs::transport::As4HttpTransport;

#[tokio::test]
async fn test_as4_sign_encrypt_round_trip() {
    let sender_pki = BdewTestPki::generate("Test NB 9900357000004");
    let receiver_pki = BdewTestPki::generate("Test LF 9900357000005");

    // Build a mock endpoint configured to decrypt ECDH-ES messages.
    let mock = MockAs4Endpoint::builder()
        .with_decryption_key_pem(receiver_pki.encryption.key_pem.clone())
        .bind("127.0.0.1:0")
        .await
        .unwrap();

    // Build sender session — with_signing_material() atomically sets cert + key
    // and auto-derives key_id = "cert:{partner_id}" (no manual CertHandle needed).
    let session = std::sync::Arc::new(
        SessionContextBuilder::new("sess-test", "9900357000004")
            .with_signing_material(
                sender_pki.signing.cert_pem_str(),
                sender_pki.signing.key_pem_str(),
            )
            .with_trust_anchor_pem(sender_pki.signing.cert_pem_str())
            .build()
            .unwrap(),
    );

    // EventBus::new_for_testing() — BestEffort mode, no durable audit sink needed.
    let event_bus = std::sync::Arc::new(EventBus::new_for_testing());

    // ... send a sign+encrypt message using As4SendRequest ...

    // As4HttpTransport::new_for_localhost_testing() — SSRF guard disabled for tests.
    // Use send_to_localhost() instead of send() to bypass the URL validator.
    let transport = As4HttpTransport::new_for_localhost_testing().unwrap();
    // transport.send_to_localhost(&mock.local_url(), &output).await.unwrap();

    // Mock delivers the decrypted payload via next_received().
    // let received = mock.next_received().await.unwrap();
    // assert!(received.payload.starts_with(b"UNB"));
}
```

### BdewTestPki — three-keypair bundle

```rust
use mako_as4::testing::BdewTestPki;

// All three keypairs (TLS, signing, encryption) on BrainpoolP256r1.
let pki = BdewTestPki::generate("Test NB 9900357000004");

println!("Signing cert (PEM): {}", pki.signing.cert_pem_str());
println!("Encryption key (PEM): {}", pki.encryption.key_pem_str());
// pki.tls, pki.signing, pki.encryption — each has cert_pem + key_pem
```

---

## Feature flags

| Flag | Enables | Extra deps |
|---|---|---|
| `server` | Axum AS4 inbound router (`bdew_router_config`) | `axum` |
| `testing` | `BdewTestPki`, `MockAs4Endpoint`, `generate_self_signed_bdew_keypair` | none (uses asx-rs testing) |

---

## Security test coverage in makod

`services/makod/tests/as4_security.rs` verifies BDEW AS4 compliance:

| Test | Verifies |
|---|---|
| `sign_encrypt_pmode_defaults` | `bdew_pmode()` defaults to `sign=true, encrypt=true` per §2.2.6.2.2 |
| `sign_only_pmode_disables_encryption` | `bdew_pmode_sign_only()` disables encryption (dev/test only) |
| `policy_with_key_requires_encryption` | `bdew_push_policy(Some(key))` enforces `require_encrypted_inbound` |
| `policy_without_key_no_encryption_required` | Dev-mode without decryption key does not block onboarding |
| `fragment_scope_is_strict_default` | strict default kept; fragment scope consulted for fragmented messages only |
| `sign_encrypt_policy_is_bdew_compliant` | SOAP policy constants satisfy §2.2.6.2.1 + §2.2.6.2.2 |
| `replay_dedup_blocks_duplicate_message_id` | 72-hour dedup window (§4.2) |
| `tampered_signature_is_rejected` | Real `As4WsSecVerifier` rejects payload tampering |
| `inbound_encryption_enforced_when_decryption_key_set` | `require_encrypted_inbound` rejects unencrypted messages |
| `sign_encrypt_round_trip_via_mock_endpoint` | Full sign+encrypt→transport→decrypt round-trip |
| `sign_only_round_trip_envelope_contains_wssec_signature` | Sign-only round-trip preserves WS-Security elements |
| `sync_receipt_is_verified_and_correlated` | Synchronous `eb:Receipt` is parsed and correlated to the sent `message_id` (§4.6.3 NRR) |

---

## Regulatory references

| Document | Scope |
|---|---|
| **BDEW AS4-Profil v1.2** (01.04.2026) | Complete AS4 transport specification |
| **BSI TR-03116-3** | Cryptographic algorithms (ECDSA §9.1, ECDH-ES §9.2) |
| **BNetzA BK6-22-024** | Mandatory AS4 for Strom (2024-04-01) |
| **BNetzA BK7-22-023** | Mandatory AS4 for Gas (2025-04-01) |
| **RFC 6090** | EC cryptography interoperability (referenced by BDEW §2.2.6.2.1/2) |

---

## Related crates

| Crate | Role |
|---|---|
| [`mako-as4`]https://docs.rs/mako-as4**this crate** | BDEW AS4 profile — P-Modes, constants, security policy, test PKI |
| [`asx-rs`]https://docs.rs/asx-rs | The AS4/ebMS3 transport engine this crate profiles (ECDSA signing, ECDH-ES encryption, signed-receipt NRR, dedup) |
| [`edi-energy`]https://docs.rs/edi-energy | The EDIFACT payloads an AS4 interchange carries |
| [`energy-api`]https://docs.rs/energy-api | The parallel REST/WebSocket channel |
| [`makod`]https://hupe1980.github.io/mako/docs/services/makod/ | Production daemon — assembles AS4 ingest, sender, and every BDEW workflow |

Part of **mako**, an open-source Rust platform for German energy market
communication (Marktkommunikation). Full documentation: <https://hupe1980.github.io/mako/>