asx-rs 0.13.0

AS2 and AS4 B2B messaging library for Rust — signing, encryption, MDN, and ebMS3/AS4 profile support
Documentation
use super::super::parser::{parse_as4_receipt, precheck_as4_receipt_structure_bytes};
use super::super::services::{
    emit_receive_push_receipt_received, emit_receive_push_receipt_taxonomy_outcome,
    enforce_signal_signature_coverage, expected_fingerprint_from_session,
    wssec_revocation_policy_from_session,
};
use super::super::types::As4PushPolicy;
use crate::core::{AsxError, ErrorCode, ErrorContext, Result, SessionContext};
use crate::crypto::wssec::WsSecVerifyOptions;
use crate::observability::EventBus;
use crate::wire::enforce_payload_limit;
use std::sync::Arc;

const MAX_AS4_RECEIPT_BYTES: usize = 256 * 1024;

pub(super) fn parse_and_verify_receipt_if_present(
    session: &SessionContext,
    event_bus: &EventBus,
    receipt_payload: Option<&[u8]>,
    policy: &As4PushPolicy,
    message_id: &str,
) -> Result<Option<super::super::ParsedAs4Receipt>> {
    let Some(raw) = receipt_payload else {
        return Ok(None);
    };
    let message_id = Arc::<str>::from(message_id);

    enforce_payload_limit("as4_receive_receipt", raw.len(), MAX_AS4_RECEIPT_BYTES)?;
    precheck_as4_receipt_structure_bytes(raw, session, "as4_receive_receipt")?;
    let receipt_xml = crate::core::bytes_to_utf8_str(raw, "as4_receive_receipt", session)?;

    let parsed_receipt = parse_as4_receipt(
        session,
        event_bus,
        receipt_xml,
        policy.interop,
        policy.fail_closed_audit_events,
    )?;

    if parsed_receipt.is_signed {
        let expected_fingerprint = expected_fingerprint_from_session(session);
        if expected_fingerprint.is_none() {
            emit_receive_push_receipt_taxonomy_outcome(
                session,
                event_bus,
                &message_id,
                "security_verification_failed",
                "receipt_signer_fingerprint_missing",
                policy.fail_closed_audit_events,
            )?;
            return Err(AsxError::new(
                ErrorCode::PolicyViolation,
                "AS4 signed receipt verification requires cert_handle.fingerprint_sha256",
                ErrorContext::for_session_with_message(
                    "as4_receive_receipt",
                    session,
                    message_id.as_ref(),
                ),
            ));
        }
        let revocation_policy = wssec_revocation_policy_from_session(session)?;
        let receipt_doc = roxmltree::Document::parse(receipt_xml).map_err(|err| {
            AsxError::new(
                ErrorCode::ParseFailed,
                format!("failed to parse AS4 receipt for signature verification: {err}"),
                ErrorContext::for_session_with_message(
                    "as4_receive_receipt",
                    session,
                    message_id.as_ref(),
                ),
            )
        })?;

        let coverage =
            match crate::crypto::wssec::verify::verify_enveloped_signature_optional_with_doc(
                &receipt_doc,
                receipt_xml,
                WsSecVerifyOptions::new()
                    .with_expected_fingerprint(expected_fingerprint)
                    .with_revocation(revocation_policy),
            ) {
                Ok(Some(coverage)) => coverage,
                // The structural parse saw a ds:Signature but WS-Security found no
                // verifiable enveloped signature — never downgrade that to
                // "unsigned", which would silently skip the requirement below.
                Ok(None) => {
                    emit_receive_push_receipt_taxonomy_outcome(
                        session,
                        event_bus,
                        &message_id,
                        "security_verification_failed",
                        "receipt_signature_not_verifiable",
                        policy.fail_closed_audit_events,
                    )?;
                    return Err(AsxError::new(
                        ErrorCode::SecurityVerificationFailed,
                        "AS4 receipt contains a ds:Signature element that is not a verifiable \
                     WS-Security enveloped signature",
                        ErrorContext::for_session_with_message(
                            "as4_receive_receipt",
                            session,
                            message_id.as_ref(),
                        ),
                    ));
                }
                Err(err) => {
                    emit_receive_push_receipt_taxonomy_outcome(
                        session,
                        event_bus,
                        &message_id,
                        "security_verification_failed",
                        "receipt_signature_verification_failed",
                        policy.fail_closed_audit_events,
                    )?;
                    return Err(normalize_receipt_signature_verification_error(
                        session,
                        message_id.as_ref(),
                        err,
                    ));
                }
            };

        // XML Signature Wrapping defence: the eb:Messaging block whose
        // RefToMessageId is checked below must be the block the signature
        // actually covered.  Otherwise a genuine signature over some other
        // exchange could authenticate an injected, unsigned acknowledgement.
        if let Err(err) = enforce_signal_signature_coverage(
            session,
            &receipt_doc,
            "as4_receive_receipt",
            message_id.as_ref(),
            &coverage,
        ) {
            emit_receive_push_receipt_taxonomy_outcome(
                session,
                event_bus,
                &message_id,
                "security_verification_failed",
                "receipt_signal_not_signed",
                policy.fail_closed_audit_events,
            )?;
            return Err(err);
        }
    } else if policy.require_signed_receipt {
        emit_receive_push_receipt_taxonomy_outcome(
            session,
            event_bus,
            &message_id,
            "security_verification_failed",
            "receipt_signature_required_but_missing",
            policy.fail_closed_audit_events,
        )?;
        return Err(AsxError::new(
            ErrorCode::SecurityVerificationFailed,
            "AS4 receipt signature is required but not present",
            ErrorContext::for_session_with_message(
                "as4_receive_receipt",
                session,
                message_id.as_ref(),
            ),
        ));
    }

    if parsed_receipt.ref_to_message_id != message_id.as_ref() {
        emit_receive_push_receipt_taxonomy_outcome(
            session,
            event_bus,
            &message_id,
            "semantic_interop_failure",
            "receipt_ref_to_message_id_mismatch",
            policy.fail_closed_audit_events,
        )?;
        return Err(AsxError::new(
            ErrorCode::InteropViolation,
            "receipt RefToMessageId does not match AS4 user message id",
            ErrorContext::for_session_with_message(
                "as4_receive_receipt",
                session,
                message_id.as_ref(),
            ),
        ));
    }

    emit_receive_push_receipt_received(session, event_bus, &message_id)?;
    Ok(Some(parsed_receipt))
}

fn normalize_receipt_signature_verification_error(
    session: &SessionContext,
    message_id: &str,
    err: AsxError,
) -> AsxError {
    AsxError::new(
        ErrorCode::SecurityVerificationFailed,
        format!("AS4 receipt signature verification failed: {}", err.message),
        ErrorContext::for_session_with_message("as4_receive_receipt", session, message_id),
    )
}