use auths_core::ports::transparency_log::{LogError, TransparencyLog};
use auths_transparency::checkpoint::SignedCheckpoint;
use auths_transparency::proof::InclusionProof;
use thiserror::Error;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct LogSubmissionBundle {
pub log_id: String,
pub leaf_index: u64,
pub inclusion_proof: InclusionProof,
pub signed_checkpoint: SignedCheckpoint,
}
#[derive(Debug, Error)]
pub enum LogSubmitError {
#[error("log error: {0}")]
LogError(#[from] LogError),
#[error("post-submission verification failed: {0}")]
VerificationFailed(String),
}
pub async fn submit_attestation_to_log(
attestation_json: &[u8],
public_key: &[u8],
curve: auths_crypto::CurveType,
signature: &[u8],
log: &dyn TransparencyLog,
) -> Result<LogSubmissionBundle, LogSubmitError> {
let submission = log
.submit(attestation_json, public_key, curve, signature)
.await?;
if submission.inclusion_proof.hashes.is_empty() && submission.inclusion_proof.size > 1 {
return Err(LogSubmitError::VerificationFailed(
"inclusion proof has no hashes for a non-trivial tree".into(),
));
}
let metadata = log.metadata();
let trust = auths_transparency::TrustConfig::default_config();
if let Some(root) = trust.get_log(&metadata.log_id) {
match auths_transparency::verify_checkpoint_signature(&submission.signed_checkpoint, root) {
auths_transparency::CheckpointStatus::Verified
| auths_transparency::CheckpointStatus::NotProvided => {}
other => {
return Err(LogSubmitError::VerificationFailed(format!(
"checkpoint signature verification failed for log '{}': {other:?}",
metadata.log_id
)));
}
}
}
Ok(LogSubmissionBundle {
log_id: metadata.log_id,
leaf_index: submission.leaf_index,
inclusion_proof: submission.inclusion_proof,
signed_checkpoint: submission.signed_checkpoint,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::fakes::FakeTransparencyLog;
#[tokio::test]
async fn submit_succeeds_with_fake() {
let log = FakeTransparencyLog::succeeding();
let result = submit_attestation_to_log(
b"test attestation",
b"public_key",
auths_crypto::CurveType::default(),
b"signature",
&log,
)
.await;
assert!(result.is_ok());
let bundle = result.unwrap();
assert_eq!(bundle.log_id, "fake-test-log");
assert_eq!(bundle.leaf_index, 0);
}
#[tokio::test]
async fn submit_propagates_rate_limit() {
let log = FakeTransparencyLog::rate_limited(30);
let result = submit_attestation_to_log(
b"test",
b"pk",
auths_crypto::CurveType::default(),
b"sig",
&log,
)
.await;
match result {
Err(LogSubmitError::LogError(LogError::RateLimited { retry_after_secs })) => {
assert_eq!(retry_after_secs, 30);
}
other => panic!("expected RateLimited, got: {:?}", other),
}
}
#[tokio::test]
async fn submit_propagates_network_error() {
let log = FakeTransparencyLog::failing(LogError::NetworkError("connection refused".into()));
let result = submit_attestation_to_log(
b"test",
b"pk",
auths_crypto::CurveType::default(),
b"sig",
&log,
)
.await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("connection refused")
);
}
}