use actix_web::{http::header, web, App, HttpRequest, HttpResponse, HttpServer};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use bytes::Bytes;
use chrono::{DateTime, Utc};
use log::{debug, error, info};
use mongodb::{Client, Database};
use ring::signature::Ed25519KeyPair;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use atlas_transparency_log::{
detect_content_type, hash_binary, is_valid_manifest_id,
merkle_tree::{ConsistencyProof, InclusionProof, LogLeaf, MerkleProof, MerkleTree},
sign_data, ContentFormat,
};
#[derive(Clone)]
struct AppState {
db: Arc<Database>,
key_pair: Arc<Ed25519KeyPair>,
merkle_tree: Arc<parking_lot::RwLock<MerkleTree>>,
}
#[derive(Debug, Serialize, Deserialize)]
struct ManifestEntry {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<mongodb::bson::oid::ObjectId>,
pub manifest_id: String,
pub manifest_type: String,
#[serde(skip_serializing_if = "should_skip_metadata", default)]
pub content_format: ContentFormat,
#[serde(rename = "manifest", skip_serializing_if = "Option::is_none")]
pub manifest_json: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub manifest_cbor: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
pub manifest_binary: Option<String>, pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "should_skip_metadata", default)]
pub sequence_number: u64,
#[serde(skip_serializing_if = "should_skip_metadata", default)]
pub hash: String,
#[serde(skip_serializing_if = "should_skip_metadata", default)]
pub signature: String,
}
thread_local! {
static INCLUDE_TLOG_METADATA: std::cell::Cell<bool> = std::cell::Cell::new(false);
}
fn should_skip_metadata<T>(_: &T) -> bool {
!INCLUDE_TLOG_METADATA.with(|f| f.get())
}
fn set_include_tlog_metadata(include: bool) {
INCLUDE_TLOG_METADATA.with(|f| f.set(include));
}
#[derive(Debug, Deserialize)]
struct GetManifestQuery {
include_tlog_metadata: Option<bool>,
}
async fn store_manifest(
state: web::Data<AppState>,
req: HttpRequest,
bytes: Bytes,
path: web::Path<String>,
query: web::Query<ManifestQuery>,
) -> HttpResponse {
const MAX_MANIFEST_SIZE: usize = 10 * 1024 * 1024; if bytes.len() > MAX_MANIFEST_SIZE {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": "Manifest too large",
"max_size": MAX_MANIFEST_SIZE
}));
}
let manifest_id = path.to_string();
if !is_valid_manifest_id(&manifest_id) {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": "Invalid manifest ID format",
"details": "Must be a valid C2PA URN, UUID, or alphanumeric string"
}));
}
let collection = state.db.collection::<ManifestEntry>("manifests");
let manifest_type_param = &query.manifest_type;
debug!(
"Received manifest with ID: {}, manifest_type param: {:?}",
&manifest_id, manifest_type_param
);
let content_format = detect_content_type(&req);
let content_hash = hash_binary(&bytes);
let signature = sign_data(&state.key_pair, &content_hash.as_bytes());
let sequence_count = collection
.count_documents(mongodb::bson::doc! {})
.await
.unwrap_or(0);
let sequence_number = sequence_count + 1;
let now = Utc::now();
let manifest_type = manifest_type_param
.as_ref()
.map(|s| s.clone())
.unwrap_or_else(|| "unknown".to_string());
let mut entry = ManifestEntry {
id: None,
manifest_id: manifest_id.clone(),
manifest_type,
content_format: content_format.clone(),
manifest_json: None,
manifest_cbor: None,
manifest_binary: None,
created_at: now,
sequence_number: sequence_number as u64,
hash: content_hash.clone(),
signature,
};
match content_format {
ContentFormat::JSON => {
match serde_json::from_slice::<serde_json::Value>(&bytes) {
Ok(json_value) => {
let json_manifest_type = json_value
.get("manifest")
.and_then(|m| m.get("manifest_type"))
.or_else(|| json_value.get("manifest_type"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(mt) = json_manifest_type {
if manifest_type_param.is_none() {
entry.manifest_type = mt;
}
}
debug!("Using manifest_type: {}", entry.manifest_type);
entry.manifest_json = Some(json_value);
}
Err(e) => {
error!("Failed to parse JSON: {:?}", e);
return HttpResponse::BadRequest().body(format!("Invalid JSON format: {}", e));
}
}
}
ContentFormat::CBOR => {
let encoded = STANDARD.encode(&bytes);
entry.manifest_cbor = Some(encoded);
match serde_cbor::from_slice::<serde_json::Value>(&bytes) {
Ok(cbor_value) => {
let cbor_manifest_type = cbor_value
.get("manifest_type")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
if let Some(mt) = cbor_manifest_type {
if manifest_type_param.is_none() {
entry.manifest_type = mt;
}
} else if manifest_type_param.is_none() {
entry.manifest_type = "cbor_manifest".to_string();
}
}
Err(e) => {
debug!("Could not extract manifest_type from CBOR: {:?}", e);
if manifest_type_param.is_none() {
entry.manifest_type = "cbor_manifest".to_string();
}
}
}
}
ContentFormat::Binary => {
let encoded = STANDARD.encode(&bytes);
entry.manifest_binary = Some(encoded);
if manifest_type_param.is_none() {
entry.manifest_type = "binary_manifest".to_string();
}
}
}
match collection.insert_one(&entry).await {
Ok(result) => {
info!(
"Successfully stored manifest with ID: {}",
result.inserted_id
);
let leaf = LogLeaf::new(
content_hash,
manifest_id.clone(),
sequence_number as u64,
now,
);
{
let mut tree = state.merkle_tree.write();
tree.add_leaf(leaf);
if let Err(e) = persist_merkle_tree(&state.db, &tree).await {
error!("Failed to persist Merkle tree: {:?}", e);
}
}
HttpResponse::Created().json(serde_json::json!({
"id": result.inserted_id,
"manifest_id": manifest_id,
"sequence_number": sequence_number,
"hash": entry.hash,
"signature": entry.signature,
}))
}
Err(e) => {
error!("Failed to store manifest: {:?}", e);
HttpResponse::InternalServerError().json(serde_json::json!({
"error": "Failed to store manifest",
"details": e.to_string()
}))
}
}
}
async fn persist_merkle_tree(
db: &Database,
tree: &MerkleTree,
) -> Result<(), mongodb::error::Error> {
let collection = db.collection::<serde_json::Value>("merkle_tree_state");
collection.delete_many(mongodb::bson::doc! {}).await?;
let tree_state = serde_json::json!({
"leaves": tree.leaves(),
"tree_size": tree.size(),
"root_hash": tree.root_hash(),
"updated_at": Utc::now(),
});
collection.insert_one(tree_state).await?;
Ok(())
}
async fn load_merkle_tree(db: &Database) -> MerkleTree {
let collection = db.collection::<serde_json::Value>("merkle_tree_state");
match collection.find_one(mongodb::bson::doc! {}).await {
Ok(Some(state)) => {
if let Ok(leaves) = serde_json::from_value::<Vec<LogLeaf>>(state["leaves"].clone()) {
return MerkleTree::from_leaves(leaves);
}
}
_ => {}
}
let manifests_collection = db.collection::<ManifestEntry>("manifests");
if let Ok(cursor) = manifests_collection.find(mongodb::bson::doc! {}).await {
if let Ok(manifests) = futures::stream::TryStreamExt::try_collect::<Vec<_>>(cursor).await {
let mut tree = MerkleTree::new();
for manifest in manifests {
let leaf = LogLeaf::new(
manifest.hash,
manifest.manifest_id,
manifest.sequence_number,
manifest.created_at,
);
tree.add_leaf(leaf);
}
return tree;
}
}
MerkleTree::new()
}
async fn list_manifests(state: web::Data<AppState>, query: web::Query<ListQuery>) -> HttpResponse {
let collection = state.db.collection::<ManifestEntry>("manifests");
let limit = query.limit.unwrap_or(100) as i64;
let skip = query.skip.unwrap_or(0) as u64;
let mut filter = mongodb::bson::Document::new();
if let Some(manifest_type) = &query.manifest_type {
filter.insert("manifest_type", manifest_type);
}
if let Some(format) = &query.format {
let content_format = match format.as_str() {
"json" => "JSON",
"cbor" => "CBOR",
"binary" => "Binary",
_ => "JSON",
};
filter.insert("content_format", content_format);
}
let filter_doc = if filter.is_empty() {
mongodb::bson::doc! {}
} else {
filter
};
match collection
.find(filter_doc)
.sort(mongodb::bson::doc! { "sequence_number": 1 })
.skip(skip)
.limit(limit)
.await
{
Ok(cursor) => match futures::stream::TryStreamExt::try_collect::<Vec<_>>(cursor).await {
Ok(manifests) => HttpResponse::Ok().json(manifests),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
},
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
}
#[derive(Debug, Deserialize)]
struct ManifestQuery {
manifest_type: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ListQuery {
limit: Option<usize>,
skip: Option<u64>,
manifest_type: Option<String>,
format: Option<String>,
}
async fn list_manifests_by_type(
state: web::Data<AppState>,
path: web::Path<String>,
query: web::Query<ListQuery>,
) -> HttpResponse {
let manifest_type = path.into_inner();
let collection = state.db.collection::<ManifestEntry>("manifests");
let limit = query.limit.unwrap_or(100) as i64;
let skip = query.skip.unwrap_or(0) as u64;
let filter = mongodb::bson::doc! { "manifest_type": manifest_type };
match collection
.find(filter)
.sort(mongodb::bson::doc! { "sequence_number": 1 })
.skip(skip)
.limit(limit)
.await
{
Ok(cursor) => match futures::stream::TryStreamExt::try_collect::<Vec<_>>(cursor).await {
Ok(manifests) => HttpResponse::Ok().json(manifests),
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
},
Err(e) => HttpResponse::InternalServerError().body(e.to_string()),
}
}
async fn get_manifest(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<String>,
query: web::Query<GetManifestQuery>,
) -> HttpResponse {
let collection = state.db.collection::<ManifestEntry>("manifests");
debug!("Searching for manifest with ID: {}", &*path);
let include_tlog_metadata = query.include_tlog_metadata.unwrap_or(false);
set_include_tlog_metadata(include_tlog_metadata);
match collection
.find_one(mongodb::bson::doc! { "manifest_id": &*path })
.await
{
Ok(Some(manifest)) => {
info!("Found manifest for ID: {}", &*path);
let accept_cbor = req
.headers()
.get(header::ACCEPT)
.and_then(|h| h.to_str().ok())
.map(|s| s.contains("application/cbor"))
.unwrap_or(false);
match manifest.content_format {
ContentFormat::CBOR if accept_cbor => {
if let Some(ref cbor_data) = manifest.manifest_cbor {
if let Ok(decoded) = STANDARD.decode(cbor_data) {
return HttpResponse::Ok()
.content_type("application/cbor")
.body(decoded);
}
}
}
ContentFormat::Binary => {
if let Some(ref binary_data) = manifest.manifest_binary {
if let Ok(decoded) = STANDARD.decode(binary_data) {
return HttpResponse::Ok()
.content_type("application/octet-stream")
.body(decoded);
}
}
}
_ => {} }
HttpResponse::Ok().json(manifest)
}
Ok(None) => {
debug!("No manifest found for ID: {}", &*path);
HttpResponse::NotFound().body(format!("Manifest not found for ID: {}", &*path))
}
Err(e) => {
error!("Error fetching manifest {}: {:?}", &*path, e);
HttpResponse::InternalServerError().body(format!("Error fetching manifest: {}", e))
}
}
}
async fn get_inclusion_proof(state: web::Data<AppState>, path: web::Path<String>) -> HttpResponse {
let manifest_id = path.into_inner();
if !is_valid_manifest_id(&manifest_id) {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": "Invalid manifest ID format"
}));
}
let tree = state.merkle_tree.read();
match tree.generate_inclusion_proof(&manifest_id) {
Some(proof) => HttpResponse::Ok().json(proof),
None => HttpResponse::NotFound().json(serde_json::json!({
"error": "No proof available",
"manifest_id": manifest_id,
"reason": "Manifest not found in tree"
})),
}
}
async fn get_merkle_root(state: web::Data<AppState>) -> HttpResponse {
let tree = state.merkle_tree.read();
match tree.root_hash() {
Some(root) => HttpResponse::Ok().json(serde_json::json!({
"root_hash": root,
"tree_size": tree.size()
})),
None => HttpResponse::NotFound().body("No Merkle root available yet"),
}
}
async fn verify_proof(
state: web::Data<AppState>,
proof: web::Json<InclusionProof>,
) -> HttpResponse {
let tree = state.merkle_tree.read();
let is_valid = tree.verify_inclusion_proof(&proof);
HttpResponse::Ok().json(serde_json::json!({
"valid": is_valid,
"manifest_id": proof.manifest_id,
"proof_description": (&*proof as &dyn MerkleProof).describe()
}))
}
#[derive(Debug, Deserialize)]
struct ConsistencyProofRequest {
old_size: usize,
new_size: usize,
}
async fn get_consistency_proof(
state: web::Data<AppState>,
query: web::Query<ConsistencyProofRequest>,
) -> HttpResponse {
let tree = state.merkle_tree.read();
if query.old_size == 0 || query.new_size == 0 {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": "Tree sizes must be greater than 0"
}));
}
if query.old_size > query.new_size {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": "Old size must be less than or equal to new size"
}));
}
match tree.generate_consistency_proof(query.old_size, query.new_size) {
Some(proof) => HttpResponse::Ok().json(serde_json::json!({
"proof": proof,
"description": (&proof as &dyn MerkleProof).describe()
})),
None => HttpResponse::NotFound().json(serde_json::json!({
"error": "Cannot generate consistency proof",
"old_size": query.old_size,
"new_size": query.new_size,
"current_tree_size": tree.size()
})),
}
}
async fn verify_consistency_proof(
state: web::Data<AppState>,
proof: web::Json<ConsistencyProof>,
) -> HttpResponse {
let tree = state.merkle_tree.read();
let is_valid = tree.verify_consistency_proof(&proof);
HttpResponse::Ok().json(serde_json::json!({
"valid": is_valid,
"old_size": proof.old_size,
"new_size": proof.new_size,
"proof_elements": proof.proof_hashes.len(),
"description": (&*proof as &dyn MerkleProof).describe()
}))
}
async fn get_tree_stats(state: web::Data<AppState>) -> HttpResponse {
let tree = state.merkle_tree.read();
let total_leaves = tree.size();
let has_root = tree.root_hash().is_some();
let estimated_depth = if total_leaves > 0 {
(total_leaves as f64).log2().ceil() as usize
} else {
0
};
HttpResponse::Ok().json(serde_json::json!({
"current_size": total_leaves,
"root_hash": tree.root_hash(),
"estimated_depth": estimated_depth,
"has_root": has_root,
"timestamp": Utc::now(),
"tree_health": if has_root { "healthy" } else { "empty" }
}))
}
async fn get_historical_root(state: web::Data<AppState>, path: web::Path<usize>) -> HttpResponse {
let tree_size = path.into_inner();
let tree = state.merkle_tree.read();
if tree_size == 0 || tree_size > tree.size() {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": "Invalid tree size",
"requested_size": tree_size,
"current_size": tree.size()
}));
}
let root_hash = tree.compute_root_for_size(tree_size);
match root_hash {
Some(root) => HttpResponse::Ok().json(serde_json::json!({
"tree_size": tree_size,
"root_hash": root,
"current_size": tree.size()
})),
None => HttpResponse::InternalServerError().json(serde_json::json!({
"error": "Failed to compute historical root"
})),
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let mongodb_uri =
std::env::var("MONGODB_URI").unwrap_or_else(|_| "mongodb://localhost:27017".to_string());
let server_host = std::env::var("SERVER_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
let server_port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "8080".to_string());
let server_addr = format!("{}:{}", server_host, server_port);
let key_path =
std::env::var("KEY_PATH").unwrap_or_else(|_| "transparency_log_key.pem".to_string());
let key_pair = match std::fs::read(&key_path) {
Ok(pkcs8_bytes) => Ed25519KeyPair::from_pkcs8(&pkcs8_bytes).expect("Failed to parse key"),
Err(_) => {
let rng = ring::rand::SystemRandom::new();
let pkcs8_bytes = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate key");
std::fs::write(&key_path, pkcs8_bytes.as_ref()).expect("Failed to save key");
Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref())
.expect("Failed to parse newly generated key")
}
};
let client = Client::with_uri_str(&mongodb_uri)
.await
.expect("Failed to connect to MongoDB");
let db_name = std::env::var("DB_NAME").unwrap_or_else(|_| "c2pa_manifests".to_string());
let db = Arc::new(client.database(&db_name));
let merkle_tree = Arc::new(parking_lot::RwLock::new(load_merkle_tree(&db).await));
let state = web::Data::new(AppState {
db: db.clone(),
key_pair: Arc::new(key_pair),
merkle_tree,
});
println!(
"Starting transparency log server at http://{}:{}",
if server_host == "0.0.0.0" {
"localhost"
} else {
&server_host
},
server_port
);
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.app_data(web::PayloadConfig::new(10 * 1024 * 1024)) .route("/manifests", web::get().to(list_manifests))
.route("/manifests/{id}", web::post().to(store_manifest))
.route("/manifests/{id}", web::get().to(get_manifest))
.route("/manifests/{id}/proof", web::get().to(get_inclusion_proof))
.route("/merkle/root", web::get().to(get_merkle_root))
.route("/merkle/verify", web::post().to(verify_proof))
.route("/merkle/stats", web::get().to(get_tree_stats))
.route("/merkle/consistency", web::get().to(get_consistency_proof))
.route(
"/merkle/consistency/verify",
web::post().to(verify_consistency_proof),
)
.route("/merkle/root/{size}", web::get().to(get_historical_root))
.route(
"/types/{manifest_type}/manifests",
web::get().to(list_manifests_by_type),
)
})
.bind(&server_addr)?
.run()
.await
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web;
use base64::engine::general_purpose::STANDARD;
use chrono::Utc;
use ring::signature::Ed25519KeyPair;
use atlas_common::hash::calculate_hash;
use atlas_transparency_log::sign_data;
fn hash_string(data: &str) -> String {
calculate_hash(data.as_bytes())
}
#[actix_web::test]
async fn test_hashing() {
let data = "test data";
let hash1 = hash_string(data);
let hash2 = hash_string(data);
assert_eq!(hash1, hash2);
let hash3 = hash_string("different data");
assert_ne!(hash1, hash3);
let raw_hash = calculate_hash(data.as_bytes());
assert_eq!(raw_hash.len(), 96); }
#[actix_web::test]
async fn test_signing() {
let rng = ring::rand::SystemRandom::new();
let pkcs8_bytes = Ed25519KeyPair::generate_pkcs8(&rng).expect("Failed to generate key");
let key_pair =
Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()).expect("Failed to parse key");
let data = "test data";
let signature = sign_data(&key_pair, data.as_bytes());
assert!(!signature.is_empty());
let decoded = STANDARD.decode(&signature).unwrap();
assert_eq!(decoded.len(), 64);
}
#[actix_web::test]
async fn test_manifest_serialization_with_metadata_flag() {
let now = Utc::now();
let entry = ManifestEntry {
id: None,
manifest_id: "test-id".to_string(),
manifest_type: "Dataset".to_string(),
content_format: ContentFormat::JSON,
manifest_json: Some(serde_json::json!({"test": "data"})),
manifest_cbor: None,
manifest_binary: None,
created_at: now,
sequence_number: 42,
hash: "test-hash".to_string(),
signature: "test-sig".to_string(),
};
set_include_tlog_metadata(false);
let json_without = serde_json::to_value(&entry).unwrap();
assert!(json_without.get("sequence_number").is_none());
assert!(json_without.get("hash").is_none());
assert!(json_without.get("signature").is_none());
assert!(json_without.get("content_format").is_none());
assert!(json_without.get("manifest").is_some());
set_include_tlog_metadata(true);
let json_with = serde_json::to_value(&entry).unwrap();
assert_eq!(json_with.get("sequence_number").unwrap(), 42);
assert_eq!(json_with.get("hash").unwrap(), "test-hash");
assert_eq!(json_with.get("signature").unwrap(), "test-sig");
assert!(json_with.get("content_format").is_some());
assert!(json_with.get("manifest").is_some());
}
}