use super::{
As2SendPolicy, AsxError, ErrorCode, ErrorContext, MimeEnvelope, Result, SessionContext,
extract_content_type_header,
mic::{build_outbound_entity, compute_mic},
pipeline,
};
fn validate_as2_from_identifier(
stage: &'static str,
session: &SessionContext,
value: &str,
) -> Result<()> {
const AS2_ID_MAX_LEN: usize = 128;
let (inner, allow_space) = if value.starts_with('"') && value.ends_with('"') && value.len() >= 2
{
(&value[1..value.len() - 1], true)
} else {
(value, false)
};
if inner.is_empty() {
return Err(AsxError::new(
ErrorCode::PolicyViolation,
"AS2-From identifier must not be empty",
ErrorContext::for_session(stage, session),
));
}
if inner.len() > AS2_ID_MAX_LEN {
return Err(AsxError::new(
ErrorCode::PolicyViolation,
format!(
"AS2-From identifier exceeds maximum length \
({} bytes, limit is {AS2_ID_MAX_LEN})",
inner.len()
),
ErrorContext::for_session(stage, session),
));
}
let min_char: u8 = if allow_space { 0x20 } else { 0x21 };
if let Some(bad) = inner.bytes().find(|&b| !(min_char..=0x7E).contains(&b)) {
return Err(AsxError::new(
ErrorCode::PolicyViolation,
format!(
"AS2-From identifier contains character 0x{bad:02X} \
not allowed by RFC 4130 §4 (US-ASCII printable only)"
),
ErrorContext::for_session(stage, session),
));
}
Ok(())
}
pub(crate) struct OutboundEntity {
pub(crate) entity: Vec<u8>,
pub(crate) mic_base64: String,
pub(crate) digest_alg: &'static str,
}
pub(crate) fn outbound_entity(
session: &SessionContext,
message_id: &str,
payload: &[u8],
policy: &As2SendPolicy,
content_type: Option<&str>,
) -> Result<OutboundEntity> {
pipeline::validate_message_id(message_id, "as2_send_validate", session)?;
tracing::debug!(
target: "asx",
session_id = %session.session_id(),
partner_id = %session.partner_id(),
message_id = %message_id,
sign = policy.sign,
encrypt = policy.encrypt,
"AS2 send: building outbound message",
);
if payload.is_empty() {
return Err(AsxError::new(
ErrorCode::PolicyViolation,
"strict interop mode rejects empty AS2 payload",
ErrorContext::for_session("as2_send_strict_policy", session),
));
}
if !policy.as2_from_id.trim().is_empty() {
validate_as2_from_identifier("as2_send_validate", session, policy.as2_from_id.as_str())?;
}
let compressed;
let payload = if policy.compress {
compressed = compress_payload(payload)?;
compressed.as_slice()
} else {
payload
};
let entity_content_type = payload_entity_content_type(policy, content_type);
let entity: Vec<u8> = if policy.sign || policy.encrypt {
build_outbound_entity(entity_content_type, payload)
} else {
payload.to_vec()
};
let (mic_base64, digest_alg) = compute_mic(&entity, policy.mic_algorithm);
Ok(OutboundEntity {
entity,
mic_base64,
digest_alg,
})
}
pub(crate) fn envelope(
signed: Vec<u8>,
recipient: Option<&openssl::x509::X509Ref>,
policy: &As2SendPolicy,
content_type: Option<&str>,
) -> Result<MimeEnvelope> {
let body = match (policy.encrypt, recipient) {
(true, Some(recipient)) => encrypt_payload(&signed, recipient, policy)?,
(true, None) => {
return Err(AsxError::new(
ErrorCode::PolicyViolation,
"AS2 recipient certificate is missing",
ErrorContext::new("as2_send_encrypt"),
));
}
(false, _) => signed,
};
Ok(build_mime_envelope(body, policy, content_type))
}
pub fn raw_message(
payload: &[u8],
content_type: &str,
as2_from: &str,
signer: Option<(&[u8], &[u8])>,
recipient_cert_pem: Option<&[u8]>,
) -> Result<(String, Vec<u8>)> {
let refuse = |detail: String| {
AsxError::new(
ErrorCode::PolicyViolation,
detail,
ErrorContext::new("as2_raw_message"),
)
};
let partner = crate::party::Partner::new(as2_from);
let resolved = crate::profile::resolve(
&crate::profile::Profile::as2(),
&partner,
&crate::clock::system(),
);
let mut policy = super::client::send_view(&resolved, as2_from);
policy.sign = signer.is_some();
policy.encrypt = recipient_cert_pem.is_some();
let recipient = recipient_cert_pem
.map(openssl::x509::X509::from_pem)
.transpose()
.map_err(|_| {
refuse("AS2 recipient certificate is not a valid PEM X.509 certificate".into())
})?;
let session = SessionContext::new("raw", as2_from, "strict")?;
let entity = outbound_entity(&session, "raw", payload, &policy, Some(content_type))?;
let signed = match signer {
Some((cert, key)) => {
let pair =
crate::keys::KeyPair::from_pem(cert, key).map_err(|e| refuse(e.to_string()))?;
super::cms_keys::sign_detached_ready(&pair, &entity.entity, session.clock().now())
.map_err(|e| refuse(e.to_string()))?
}
None => entity.entity,
};
let mime = envelope(signed, recipient.as_deref(), &policy, Some(content_type))?;
Ok((mime.content_type, mime.body.to_vec()))
}
fn payload_entity_content_type<'a>(
policy: &As2SendPolicy,
content_type: Option<&'a str>,
) -> &'a str {
if policy.compress {
return crate::crypto::compression::AS2_COMPRESSED_CONTENT_TYPE;
}
content_type
.or(policy.payload_content_type)
.unwrap_or("application/octet-stream")
}
fn build_mime_envelope(
payload_to_send: Vec<u8>,
policy: &As2SendPolicy,
content_type: Option<&str>,
) -> MimeEnvelope {
if policy.encrypt {
return MimeEnvelope {
content_type: "application/pkcs7-mime; smime-type=enveloped-data; name=\"smime.p7m\""
.to_string(),
body: payload_to_send.into(),
};
}
if policy.sign {
let content_type = extract_content_type_header(&payload_to_send)
.unwrap_or_else(|| "multipart/signed".to_string());
return MimeEnvelope {
content_type,
body: payload_to_send.into(),
};
}
MimeEnvelope {
content_type: payload_entity_content_type(policy, content_type).to_string(),
body: payload_to_send.into(),
}
}
fn encrypt_payload(
payload: &[u8],
recipient_cert: &openssl::x509::X509Ref,
policy: &As2SendPolicy,
) -> Result<Vec<u8>> {
crate::crypto::as2_smime::encrypt_smime_message_preparsed(
payload,
recipient_cert,
policy.encryption_cipher,
)
}
fn compress_payload(payload: &[u8]) -> Result<Vec<u8>> {
crate::crypto::compression::compress_cms_zlib(payload, 6)
}