use std::sync::Arc;
use zeroize::Zeroize;
use crate::core::{AsxError, ErrorCode, ErrorContext, ReceivedBodyHandle, Result, SessionContext};
use crate::crypto::as2_smime::VerifiedSmimeEntity;
#[cfg(feature = "as2")]
use crate::crypto::as2_smime::{
As2SmimeVerificationOptions, SmimeFormat, decrypt_smime_enveloped_payload, detect_smime_format,
verify_smime_signed_payload,
};
#[cfg(test)]
use crate::lifecycle::TrustEvidence;
use crate::lifecycle::{DecryptionMaterial, SignatureVerification};
pub(crate) mod private {
pub trait Sealed {}
}
#[cfg(feature = "testing")]
pub use private::Sealed as TrustVerifierSeal;
#[derive(Debug)]
pub struct TrustResult {
pub signature: SignatureVerification,
pub decryption: DecryptionMaterial,
pub protected_entity: Option<VerifiedSmimeEntity>,
}
impl TrustResult {
pub fn signed_only(entity: VerifiedSmimeEntity) -> Self {
Self {
signature: SignatureVerification::Verified,
decryption: DecryptionMaterial::Available,
protected_entity: Some(entity),
}
}
pub fn decrypted(entity: VerifiedSmimeEntity) -> Self {
Self {
signature: SignatureVerification::Verified,
decryption: DecryptionMaterial::Available,
protected_entity: Some(entity),
}
}
pub fn unsigned(entity: VerifiedSmimeEntity) -> Self {
Self {
signature: SignatureVerification::NotSigned,
decryption: DecryptionMaterial::Available,
protected_entity: Some(entity),
}
}
}
pub trait As2TrustVerifier: private::Sealed + std::fmt::Debug {
fn verify_and_decrypt(
&self,
session: &SessionContext,
body: &ReceivedBodyHandle,
) -> Result<TrustResult>;
}
pub trait AsyncAs2TrustVerifier: Send + Sync + private::Sealed {
fn verify_and_decrypt<'a>(
&'a self,
session: &'a SessionContext,
body: &'a ReceivedBodyHandle,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<TrustResult>> + Send + 'a>>;
}
#[derive(Debug)]
pub struct SyncToAsyncTrustVerifier<V: As2TrustVerifier + Send + Sync + 'static>(pub Arc<V>);
impl<V: As2TrustVerifier + Send + Sync + 'static> SyncToAsyncTrustVerifier<V> {
pub fn new(verifier: V) -> Self {
Self(Arc::new(verifier))
}
}
impl<V: As2TrustVerifier + Send + Sync + 'static> Clone for SyncToAsyncTrustVerifier<V> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<V: As2TrustVerifier + Send + Sync + 'static> private::Sealed for SyncToAsyncTrustVerifier<V> {}
impl<V: As2TrustVerifier + Send + Sync + 'static> AsyncAs2TrustVerifier
for SyncToAsyncTrustVerifier<V>
{
fn verify_and_decrypt<'a>(
&'a self,
session: &'a SessionContext,
body: &'a ReceivedBodyHandle,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<TrustResult>> + Send + 'a>> {
let verifier = Arc::clone(&self.0);
let blocking_session = session.clone();
let blocking_body = body.clone();
let error_session = session.clone();
Box::pin(async move {
let permit = crate::core::CryptoAdmissionControl::process_global()
.acquire("as2_trust_verify_async_admission", &blocking_session)
.await?;
tokio::task::spawn_blocking(move || {
let _permit = permit;
verifier.verify_and_decrypt(&blocking_session, &blocking_body)
})
.await
.map_err(|err| {
AsxError::new(
ErrorCode::TransportFailure,
format!("AS2 trust verification blocking task failed: {err}"),
ErrorContext::for_session("as2_trust_verify_async_join", &error_session),
)
})?
})
}
}
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InsecureBypassTrustVerifier {
trust: TrustEvidence,
}
#[cfg(test)]
impl InsecureBypassTrustVerifier {
pub fn new(trust: TrustEvidence) -> Self {
Self { trust }
}
}
#[cfg(test)]
impl private::Sealed for InsecureBypassTrustVerifier {}
#[cfg(test)]
impl As2TrustVerifier for InsecureBypassTrustVerifier {
fn verify_and_decrypt(
&self,
_session: &SessionContext,
_body: &ReceivedBodyHandle,
) -> Result<TrustResult> {
Ok(TrustResult {
signature: self.trust.signature,
decryption: self.trust.decryption,
protected_entity: None,
})
}
}
#[derive(Clone, Default)]
pub struct CmsSmimeTrustVerifier {
pub decryption_key_pem: Option<Vec<u8>>,
pub decryption_cert_pem: Option<Vec<u8>>,
pub signature_policy: As2SignaturePolicy,
}
impl std::fmt::Debug for CmsSmimeTrustVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CmsSmimeTrustVerifier")
.field(
"decryption_key_pem",
&crate::core::redact_present(self.decryption_key_pem.is_some()),
)
.field("decryption_cert_pem", &self.decryption_cert_pem)
.field("signature_policy", &self.signature_policy)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum As2SignaturePolicy {
#[default]
Required,
Optional,
}
impl Drop for CmsSmimeTrustVerifier {
fn drop(&mut self) {
if let Some(key) = self.decryption_key_pem.as_mut() {
key.zeroize();
}
}
}
impl CmsSmimeTrustVerifier {
pub fn with_decryption_credentials(key_pem: Vec<u8>, cert_pem: Vec<u8>) -> Self {
Self {
decryption_key_pem: Some(key_pem),
decryption_cert_pem: Some(cert_pem),
signature_policy: As2SignaturePolicy::Required,
}
}
#[must_use]
pub fn allowing_unsigned(mut self) -> Self {
self.signature_policy = As2SignaturePolicy::Optional;
self
}
fn build_revocation_policy<'a>(
session: &'a SessionContext,
) -> Result<crate::crypto::wssec::RevocationPolicy<'a>> {
if session.cert_handle().trust_anchor_pems.is_empty() {
return Err(AsxError::new(
ErrorCode::SecurityVerificationFailed,
"PKIX chain validation requires at least one trust anchor PEM; \
call with_cert_handle() and provide trust_anchor_pems before verifying signed AS2 messages",
ErrorContext::for_session("as2_smime_build_revocation_policy", session),
));
}
Ok(crate::crypto::wssec::RevocationPolicy {
trust_anchor_pems: &session.cert_handle().trust_anchor_pems,
revocation_crl_pems: &session.cert_handle().revocation_crl_pems,
ocsp_mode: session.cert_handle().ocsp_mode,
ocsp_failure_mode: session.cert_handle().ocsp_failure_mode,
stapled_ocsp_responses_der: &session.cert_handle().stapled_ocsp_responses_der,
responder_ocsp_responses_der: &session.cert_handle().responder_ocsp_responses_der,
ocsp_cache_namespace: session.partner_id(),
require_chain_validation: true,
pre_parsed_trust_anchors: Some(session.trust_anchors_x509()?),
pre_built_x509_store: Some(session.trust_anchor_x509_store()?),
})
}
}
impl private::Sealed for CmsSmimeTrustVerifier {}
impl As2TrustVerifier for CmsSmimeTrustVerifier {
fn verify_and_decrypt(
&self,
session: &SessionContext,
body: &ReceivedBodyHandle,
) -> Result<TrustResult> {
#[cfg(feature = "as2")]
{
let payload = body.materialize_contiguous("as2_smime_verify", session)?;
let expected_fingerprint = match session.cert_handle().fingerprint_sha256.trim() {
"" => None,
value => Some(value),
};
let build_options = || -> Result<As2SmimeVerificationOptions<'_>> {
Ok(As2SmimeVerificationOptions {
expected_signer_fingerprint_sha256: expected_fingerprint,
revocation_policy: Self::build_revocation_policy(session)?,
intermediate_ca_pems: &session.cert_handle().intermediate_ca_pems,
})
};
match detect_smime_format(payload.as_ref()) {
SmimeFormat::Enveloped => {
let (key, cert) = match (&self.decryption_key_pem, &self.decryption_cert_pem) {
(Some(k), Some(c)) => (k.as_slice(), c.as_slice()),
_ => {
return Err(AsxError::new(
ErrorCode::DecryptionFailed,
"AS2 message is encrypted but no decryption key is configured \
on CmsSmimeTrustVerifier; supply decryption_key_pem and \
decryption_cert_pem",
ErrorContext::for_session("as2_smime_decrypt", session),
));
}
};
let decrypted = decrypt_smime_enveloped_payload(payload.as_ref(), cert, key)?;
let entity = match detect_smime_format(&decrypted) {
SmimeFormat::OpaqueSignedData | SmimeFormat::MultipartSigned => {
verify_smime_signed_payload(&decrypted, build_options()?).map_err(|err| {
AsxError::new(
ErrorCode::SecurityVerificationFailed,
format!(
"AS2 inner signed message verification failed after decryption: {err}"
),
ErrorContext::for_session("as2_smime_verify_inner", session),
)
})?
}
_ => {
if self.signature_policy == As2SignaturePolicy::Required {
return Err(AsxError::new(
ErrorCode::SecurityVerificationFailed,
"AS2 message is encrypted but not signed; encryption alone \
does not authenticate the sender because the recipient \
certificate is not secret. Sign the message, or set \
As2SignaturePolicy::Optional to accept this",
ErrorContext::for_session("as2_smime_signature_policy", session),
));
}
return Ok(TrustResult::unsigned(
VerifiedSmimeEntity::from_entity_bytes(decrypted),
));
}
};
Ok(TrustResult::decrypted(entity))
}
SmimeFormat::OpaqueSignedData | SmimeFormat::MultipartSigned => {
Ok(TrustResult::signed_only(verify_smime_signed_payload(
payload.as_ref(),
build_options()?,
)?))
}
SmimeFormat::Unknown => {
if self.signature_policy == As2SignaturePolicy::Required {
return Err(AsxError::new(
ErrorCode::SecurityVerificationFailed,
"AS2 message carries no S/MIME signature or encryption layer; \
signatures are required. Ask the partner to sign, or set \
As2SignaturePolicy::Optional to accept unsigned payloads",
ErrorContext::for_session("as2_smime_signature_policy", session),
));
}
Ok(TrustResult {
signature: SignatureVerification::NotSigned,
decryption: DecryptionMaterial::Available,
protected_entity: None,
})
}
SmimeFormat::AuthenticatedData => Err(AsxError::new(
ErrorCode::InteropViolation,
"CMS AuthenticatedData (smime-type=authenticated-data) is not supported \
for AS2 message delivery; partner must use SignedData or EnvelopedData",
ErrorContext::for_session("as2_smime_verify", session),
)),
SmimeFormat::DigestedData => Err(AsxError::new(
ErrorCode::InteropViolation,
"CMS DigestedData (smime-type=digested-data) is not supported \
for AS2 message delivery; partner must use SignedData or EnvelopedData",
ErrorContext::for_session("as2_smime_verify", session),
)),
}
}
#[cfg(not(feature = "as2"))]
{
let _ = (session, body);
Err(AsxError::new(
ErrorCode::PolicyViolation,
"CmsSmimeTrustVerifier requires the 'as2' feature",
ErrorContext::new("as2_smime_feature_disabled"),
))
}
}
}