use std::sync::Arc;
use std::time::Duration;
use base64::Engine as _;
use crate::constants::{DEFAULT_MAX_RETRIES, PDF_MAGIC, SHA1_DIGEST_SIZE};
use crate::net::protocol::SigningTransport;
use crate::net::soap::send_soap;
use crate::net::soap_envelope::{
build_enum_certificates_envelope, build_sign_envelope, build_sign_hash_envelope,
build_verify_envelope, SIGNATURE_TYPE_CMS, SIGNATURE_TYPE_FIELD_VERIFY,
};
use crate::net::soap_parsers::{
parse_enum_certificates_response, parse_sign_response, parse_verify_response,
ServerVerifyResult,
};
use crate::net::transport::Transport;
use crate::{Result, RevenantError};
const ACTION_SIGN: &str = "DssSign";
const ACTION_VERIFY: &str = "DssVerify";
fn to_base64(bytes: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(bytes)
}
#[derive(Debug)]
pub struct SoapSigningTransport {
transport: Arc<Transport>,
url: String,
}
impl SoapSigningTransport {
#[must_use]
pub fn new(transport: Arc<Transport>, url: impl Into<String>) -> Self {
Self {
transport,
url: url.into(),
}
}
fn send_and_parse(&self, envelope: &str, timeout: Duration) -> Result<Vec<u8>> {
let response = send_soap(
self.transport.as_ref(),
&self.url,
envelope,
ACTION_SIGN,
timeout,
0,
)?;
let cms = parse_sign_response(&response)?;
log::info!("Received CMS signature: {} bytes", cms.len());
Ok(cms)
}
}
impl SigningTransport for SoapSigningTransport {
fn url(&self) -> Option<&str> {
Some(&self.url)
}
fn sign_data(
&self,
data: &[u8],
username: &str,
password: &str,
timeout: Duration,
) -> Result<Vec<u8>> {
if data.is_empty() {
return Err(RevenantError::Other("Cannot sign empty data.".to_owned()));
}
log::info!("Signing data via SOAP: {} bytes", data.len());
let envelope =
build_sign_envelope(username, password, SIGNATURE_TYPE_CMS, &to_base64(data));
self.send_and_parse(&envelope, timeout)
}
fn sign_hash(
&self,
hash: &[u8],
username: &str,
password: &str,
timeout: Duration,
) -> Result<Vec<u8>> {
if hash.len() != SHA1_DIGEST_SIZE {
return Err(RevenantError::Other(format!(
"Expected {SHA1_DIGEST_SIZE}-byte SHA-1 hash, got {} bytes.",
hash.len()
)));
}
log::debug!("Signing SHA-1 hash via SOAP: {} bytes", hash.len());
let envelope =
build_sign_hash_envelope(username, password, SIGNATURE_TYPE_CMS, &to_base64(hash));
self.send_and_parse(&envelope, timeout)
}
fn sign_pdf_detached(
&self,
pdf: &[u8],
username: &str,
password: &str,
timeout: Duration,
) -> Result<Vec<u8>> {
if !pdf.starts_with(PDF_MAGIC) {
return Err(RevenantError::Pdf(
"Input does not appear to be a PDF file.".to_owned(),
));
}
log::info!("Signing PDF (detached) via SOAP: {} bytes", pdf.len());
let envelope = build_sign_envelope(username, password, SIGNATURE_TYPE_CMS, &to_base64(pdf));
self.send_and_parse(&envelope, timeout)
}
fn enum_certificates(
&self,
username: &str,
password: &str,
timeout: Duration,
) -> Result<Vec<Vec<u8>>> {
enum_certificates(
self.transport.as_ref(),
&self.url,
username,
password,
timeout,
)
}
}
#[must_use]
pub fn verify_pdf_server(
transport: &Transport,
url: &str,
pdf: &[u8],
timeout: Duration,
) -> ServerVerifyResult {
log::info!("Server-side verify: {} bytes, url={url}", pdf.len());
let envelope = build_verify_envelope(&to_base64(pdf), SIGNATURE_TYPE_FIELD_VERIFY);
match send_soap(
transport,
url,
&envelope,
ACTION_VERIFY,
timeout,
DEFAULT_MAX_RETRIES,
) {
Ok(response) => parse_verify_response(&response),
Err(e) => {
log::warn!("Server verify failed: {e}");
ServerVerifyResult::Failed(e.to_string())
}
}
}
pub fn enum_certificates(
transport: &Transport,
url: &str,
username: &str,
password: &str,
timeout: Duration,
) -> Result<Vec<Vec<u8>>> {
log::info!("Enumerating certificates: url={url}");
let envelope = build_enum_certificates_envelope(username, password);
let response = send_soap(
transport,
url,
&envelope,
ACTION_SIGN,
timeout,
DEFAULT_MAX_RETRIES,
)?;
parse_enum_certificates_response(&response)
}
#[cfg(test)]
mod tests {
use super::*;
fn transport() -> SoapSigningTransport {
SoapSigningTransport::new(
Arc::new(Transport::new()),
"https://ca.gov.am/SAPIWS/DSS.asmx",
)
}
#[test]
fn url_is_reported() {
assert_eq!(transport().url(), Some("https://ca.gov.am/SAPIWS/DSS.asmx"));
}
#[test]
fn sign_data_rejects_empty_input() {
let err = transport()
.sign_data(b"", "u", "p", Duration::from_secs(1))
.unwrap_err();
assert!(matches!(err, RevenantError::Other(_)));
}
#[test]
fn sign_hash_rejects_wrong_length() {
let err = transport()
.sign_hash(b"too-short", "u", "p", Duration::from_secs(1))
.unwrap_err();
assert!(matches!(err, RevenantError::Other(_)));
}
#[test]
fn sign_pdf_rejects_non_pdf() {
let err = transport()
.sign_pdf_detached(b"not a pdf", "u", "p", Duration::from_secs(1))
.unwrap_err();
assert!(matches!(err, RevenantError::Pdf(_)));
}
}