use async_trait::async_trait;
use auths_transparency::checkpoint::SignedCheckpoint;
use auths_transparency::proof::{ConsistencyProof, InclusionProof};
use auths_transparency::types::LogOrigin;
use auths_verifier::Ed25519PublicKey;
#[derive(Debug, Clone)]
pub struct LogSubmission {
pub leaf_index: u64,
pub inclusion_proof: InclusionProof,
pub signed_checkpoint: SignedCheckpoint,
}
#[derive(Debug, Clone)]
pub struct LogMetadata {
pub log_id: String,
pub log_origin: LogOrigin,
pub log_public_key: Ed25519PublicKey,
pub api_url: Option<String>,
}
#[derive(Debug, thiserror::Error)]
pub enum LogError {
#[error("submission rejected: {reason}")]
SubmissionRejected {
reason: String,
},
#[error("network error: {0}")]
NetworkError(String),
#[error("rate limited, retry after {retry_after_secs}s")]
RateLimited {
retry_after_secs: u64,
},
#[error("invalid response: {0}")]
InvalidResponse(String),
#[error("entry not found")]
EntryNotFound,
#[error("consistency violation: {0}")]
ConsistencyViolation(String),
#[error("log unavailable: {0}")]
Unavailable(String),
}
impl auths_crypto::AuthsErrorInfo for LogError {
fn error_code(&self) -> &'static str {
match self {
Self::SubmissionRejected { .. } => "AUTHS-E9001",
Self::NetworkError(_) => "AUTHS-E9002",
Self::RateLimited { .. } => "AUTHS-E9003",
Self::InvalidResponse(_) => "AUTHS-E9004",
Self::EntryNotFound => "AUTHS-E9005",
Self::ConsistencyViolation(_) => "AUTHS-E9006",
Self::Unavailable(_) => "AUTHS-E9007",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::SubmissionRejected { .. } => {
Some("Check the attestation format and payload size")
}
Self::NetworkError(_) => Some("Check your internet connection and the log's API URL"),
Self::RateLimited { .. } => Some("Wait and retry; the log is rate-limiting requests"),
Self::InvalidResponse(_) => {
Some("The log returned an unexpected response; check the log version")
}
Self::EntryNotFound => Some("The entry may not be sequenced yet; retry after a moment"),
Self::ConsistencyViolation(_) => {
Some("The log returned data that does not match what was submitted")
}
Self::Unavailable(_) => {
Some("The transparency log is unavailable; retry later or use --allow-unlogged")
}
}
}
}
#[async_trait]
pub trait TransparencyLog: Send + Sync {
async fn submit(
&self,
leaf_data: &[u8],
public_key: &[u8],
curve: auths_crypto::CurveType,
signature: &[u8],
) -> Result<LogSubmission, LogError>;
async fn get_checkpoint(&self) -> Result<SignedCheckpoint, LogError>;
async fn get_inclusion_proof(
&self,
leaf_index: u64,
tree_size: u64,
) -> Result<InclusionProof, LogError>;
async fn get_consistency_proof(
&self,
old_size: u64,
new_size: u64,
) -> Result<ConsistencyProof, LogError>;
fn metadata(&self) -> LogMetadata;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn log_error_display() {
let err = LogError::SubmissionRejected {
reason: "payload too large".into(),
};
assert_eq!(err.to_string(), "submission rejected: payload too large");
let err = LogError::RateLimited {
retry_after_secs: 30,
};
assert_eq!(err.to_string(), "rate limited, retry after 30s");
let err = LogError::NetworkError("connection refused".into());
assert_eq!(err.to_string(), "network error: connection refused");
let err = LogError::Unavailable("service unavailable".into());
assert_eq!(err.to_string(), "log unavailable: service unavailable");
}
#[test]
fn log_error_codes() {
use auths_crypto::AuthsErrorInfo;
assert_eq!(
LogError::SubmissionRejected {
reason: String::new()
}
.error_code(),
"AUTHS-E9001"
);
assert_eq!(
LogError::NetworkError(String::new()).error_code(),
"AUTHS-E9002"
);
assert_eq!(
LogError::RateLimited {
retry_after_secs: 0
}
.error_code(),
"AUTHS-E9003"
);
assert_eq!(
LogError::InvalidResponse(String::new()).error_code(),
"AUTHS-E9004"
);
assert_eq!(LogError::EntryNotFound.error_code(), "AUTHS-E9005");
assert_eq!(
LogError::ConsistencyViolation(String::new()).error_code(),
"AUTHS-E9006"
);
assert_eq!(
LogError::Unavailable(String::new()).error_code(),
"AUTHS-E9007"
);
}
#[test]
fn log_error_suggestions_not_none() {
use auths_crypto::AuthsErrorInfo;
let variants: Vec<LogError> = vec![
LogError::SubmissionRejected {
reason: String::new(),
},
LogError::NetworkError(String::new()),
LogError::RateLimited {
retry_after_secs: 0,
},
LogError::InvalidResponse(String::new()),
LogError::EntryNotFound,
LogError::ConsistencyViolation(String::new()),
LogError::Unavailable(String::new()),
];
for v in &variants {
assert!(
v.suggestion().is_some(),
"missing suggestion for {}",
v.error_code()
);
}
}
fn _assert_object_safe(_: std::sync::Arc<dyn TransparencyLog>) {}
}