Security model

Trust boundaries, fail-closed defaults, certificate validation and revocation, XML Signature Wrapping defences, and what asx-rs does not protect against.

On this page

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

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.

PropertyBehaviour
DefaultSecurityPolicy::SIGN_AND_ENCRYPT — matches PEPPOL, CEF eDelivery and BDEW AS4-Profil v1.2 §2.2.6.2.2
Overridable by a layerNo. The floor lives on BaseProfile only, so no extension, global override, partner overlay, or regional pack can lower it
Enforced byProfileStack::validate(), against the resolved policy of the deployment baseline and of every declared partner
Enforced by a host mandatevalidate_with_floor(floor) — takes the stronger of the host floor and the profile's, so it can only tighten
Visible in auditRecorded 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).

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

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.

AS4 pull authorization

A PullRequest asks the receiver to hand over queued messages, is not signed, and names an MPC that is not secret. As4PullPolicy::authorization therefore defaults to As4PullAuthorization::Deny: an MPC that was never explicitly opened serves nothing. Choose SharedSecret (constant-time comparison against eb:AuthorizationInfo) or EnforcedByTransport (mTLS or an API gateway authorizes the caller upstream). There is no "unauthenticated" setting reachable by leaving a field unset.

WS-Security verification always requires an X.509 signer token, and the verifying key is taken from that certificate. A signature that verifies only against an inline ds:KeyValue/ds:RSAKeyValue is rejected: anyone can generate a key pair and inline its public half, so such a signature is cryptographically valid while authenticating nobody. An inline RSAKeyValue sent alongside a certificate is still checked for consistency with it.

Three KeyInfo shapes resolve to the signer certificate:

KeyInfo shapeNotes
ds:X509Data/ds:X509Certificate inlineWhat ASX emits by default
wsse:SecurityTokenReferencewsse:BinarySecurityToken ValueType="...#X509v3"Single DER certificate — the default shape of WSS4J-based stacks (phase4, Domibus, Oxalis)
wsse:SecurityTokenReferencewsse:BinarySecurityToken ValueType="...#X509PKIPathv1"DER certificate path, leaf first (BDEW)

Key-identifier styles that need out-of-document lookup (wsse:KeyIdentifier with SKI or thumbprint, ds:X509IssuerSerial) are not resolvable from the message alone and fail with a named error.

A document carrying multiple ds:Signature elements is resolved fail-closed: exactly one signature directly inside wsse:Security is used (a counter-signature elsewhere in the document is tolerated but not verified); two or more inside wsse:Security, or several with none inside wsse:Security, are rejected as ambiguous rather than resolved by document order — which would let an attacker prepend a signature-shaped element to steer which signature gets verified.

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.
CertHandle {
    fingerprint_sha256: "AA:BB:CC:...".to_string(),  // enforce pinning
    ..
}

OCSP (Online Certificate Status Protocol)

OCSP is configured per session via OcspConfig in CertHandle:

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.


Outbound requests are validated and pinned

Three paths make an outbound HTTP request, and two take their target from something the counterparty influences:

PathTarget chosen by
AS2/AS4 egressThe deployment's own P-Mode or partner configuration
SMP lookupThe SML zone (operator) plus a participant identifier (not)
OCSPThe certificate's AIA extension — by whoever presented the certificate, before it is trusted

All three refuse private, loopback and link-local targets, refuse non-HTTP schemes, and never follow redirects: a 3xx would reach a host nothing checked.

Validating is not enough on its own. Resolving DNS to check the address and then letting the HTTP client resolve again is a time-of-check/time-of-use hole — the attacker answers the check with a public address and the connection with a private one. Every path therefore pins the connection to the addresses that were validated.

Plain HTTP is refused everywhere except OCSP: RFC 6960 responses are signed by the responder, and public PKIs publish http:// responder URLs, so requiring TLS there would break revocation checking rather than improve it.


Cryptographic Algorithms

AS2 (S/MIME)

OperationAlgorithm
SigningS/MIME CMS — RSA + SHA-256
EncryptionS/MIME CMS — AES-256 CBC (or AES-128-GCM for newer partners)
MIC computationSHA-256 over the signed/decrypted MIME entity, or over content alone for unprotected messages (RFC 4130 §7.3.1 — see as2.md)

AS4 (WS-Security / XML Encryption)

OperationAlgorithmURI
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 transportRSA-OAEP (XMLenc11)http://www.w3.org/2009/xmlenc11#rsa-oaep
Key transport MGFMGF1-SHA256http://www.w3.org/2009/xmlenc11#mgf1sha256
Key transport digestSHA-256http://www.w3.org/2001/04/xmlenc#sha256
XML SignatureRSA-SHA256http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
CanonicalizationExclusive C14Nhttp://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 through 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, including the xmlns="" undeclaration for a no-namespace element under an inherited non-empty default namespace (Exc-C14N §2.3).
  • 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. The list is honored both on per-reference ds:Transforms and on the ds:SignedInfo CanonicalizationMethod itself, where WSS4J-based signers declare their SOAP envelope prefix by default.
  • Strips comments in default mode; preserves them when include_comments = true.
  • Sorts attributes in lexicographic order (namespace URI, local name) as required by C14N.

For cid: attachment references, the WSS SwA profile Attachment-Content-Signature-Transform is declared on send and accepted on verify — the AS4 profile mandates it, and its digest input for binary content is the raw attachment octets. The Attachment-Complete-Signature-Transform (MIME part headers folded into the digest) is not implemented and is rejected by name.

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:

RuleWhy
The envelope must reference the attachment by href="cid:…" — an eb:PartInfo in the signed eb:Messaging header (SwA, the AS4 packaging), or a legacy MTOM xop:Include in the BodyWithout 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 styleshref='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:Ids 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:

ThreatDefence
Attacker or misconfigured MSH returns a receipt it did not signWS-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 messagethe 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 onea repeated eb:MessageInfo child is rejected outright rather than resolved first-wins
Digests parked outside the eb:Receipt to fake non-repudiationNonRepudiationInformation is read only from inside the eb:Receipt of that SignalMessage
Counterparty acknowledges a different messageeb:RefToMessageId compared to the sent message_id
Counterparty acknowledges different bytes — the core NRR guaranteeevery 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 messagea 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 URIduplicate MessagePartNRInformation URIs are rejected outright
Entries for URIs the sender never signedrejected under reject_unexpected_references (on in regulated())
Replay of an old receipteb: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 signaturetreated as a verification failure, never downgraded to "unsigned"
An eb:Error for someone else's message causing a wrongful dead-letterthe 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 rejectsrejected as ambiguous — neither confirmed delivered nor confirmed rejected
Resource exhaustion from a hostile response256 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

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:

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.

ControlStatus
SMP URL SSRF validation + DNS pinning + no redirects✅ enforced — the lookup validates and pins in one step, so the addresses that were checked are the ones connected to
The NAPTR record naming the SMP may only yield an https URI, and may not use a backreference✅ enforced — the SMP URL is read out of DNS, so the record's contents are an input to where a message goes
The result is usable at allrefused by default (SmpSignaturePolicy::Deny) — a lookup whose authenticity was never established does not silently become a routing decision
ds:Signature present on the response✅ with SmpSignaturePolicy::RequireSignaturePresent — catches an unsigned SMP and nothing else
ds:Signature verifies✅ with SmpSignaturePolicy::Verify
SMP certificate chains to the network SMP CA✅ with SmpSignaturePolicy::Verify
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. DNSSEC validation of the SML zone is the resolver's job — the Peppol zones are signed, and BdxlResolver is where a deployment supplies a validating client.


Known Limitations

LimitationMitigation
SMP enveloped-signature path not yet cross-validated against another stackPin partner certificates out of band as defence in depth (see above)
Custom XML Exclusive C14N implementationValidated against W3C test vectors and interop compatibility matrix; not yet replaced by a vetted library
In-memory dedup provides no replay protection across restartsUse 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 levelConfigure mTLS at the TLS terminator / reverse proxy layer
OCSP thisUpdate/nextUpdate clock skew tolerance is fixed at ±300s / 86400sAdjust 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 runtimeUse 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:

SubsystemCurrent backendRole
AS2 S/MIME signing / encryptionopenssl (C FFI)CMS SignedData / EnvelopedData
AS4 WS-Security XML signingopenssl (C FFI)RSA-SHA256 ds:Signature, RSA-OAEP key wrap
AS4 payload symmetric encryptionaes-gcm (pure Rust)AES-128/256-GCM xenc:EncryptedData
X.509 certificate parsingopenssl (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.

Removing the OpenSSL dependency

Converging on a single pure-Rust crypto stack is a direction this project is committed to, not a dated plan: the symmetric layer already is pure Rust, and X.509, RSA and CMS are not. It will land as a breaking-dependency change and be announced in CHANGELOG.md.

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: the symmetric layer it uses is not FIPS-validated.