proofmode 0.9.0

Capture, share, and preserve verifiable photos and videos
Documentation
pub mod assertion;
pub mod attestation;
pub(crate) mod authenticator;
pub mod error;

#[allow(unused_imports)]
pub use assertion::Assertion;
#[allow(unused_imports)]
pub use attestation::Attestation;
#[allow(unused_imports)]
pub use error::AppAttestError;

use super::error::{Result, SignError};

/// Trait for storing and retrieving App Attest device registrations.
/// proofsign-rust implements this with PostgreSQL; proofmode only defines the interface.
#[async_trait::async_trait]
pub trait DeviceStore: Send + Sync {
    /// Look up a device registration by key_id.
    /// Returns (public_key_bytes_base64, current_assertion_counter) or None.
    async fn get_device(&self, key_id: &str) -> Result<Option<DeviceInfo>>;

    /// Register a new device with the given key_id and public key.
    async fn register_device(
        &self,
        key_id: &str,
        bundle_id: &str,
        public_key_base64: &str,
    ) -> Result<()>;

    /// Check if an assertion hash has been used before (replay protection).
    async fn is_assertion_used(&self, device_id: i64, assertion_hash: &str) -> Result<bool>;

    /// Record an assertion use and increment the counter atomically.
    async fn record_assertion(
        &self,
        device_id: i64,
        assertion_hash: &str,
        challenge: &str,
    ) -> Result<()>;
}

/// Device registration info returned by DeviceStore.
#[derive(Debug, Clone)]
pub struct DeviceInfo {
    pub id: i64,
    pub key_id: String,
    pub bundle_id: String,
    pub public_key_base64: String,
    pub assertion_counter: u32,
}

/// Verify an App Attest assertion against a device store.
/// This is the core verification logic extracted from proofsign-rust's AppAttestService.
pub async fn verify_device_assertion(
    store: &dyn DeviceStore,
    app_id: &str,
    key_id: &str,
    assertion_base64: &str,
    challenge: &str,
    client_data_json: Option<&str>,
) -> Result<()> {
    // 1. Look up the device registration
    let device = store.get_device(key_id).await?.ok_or_else(|| {
        SignError::DeviceNotRegistered(format!("No registration for key_id: {}", key_id))
    })?;

    // 2. Parse the assertion
    let assertion = Assertion::from_base64(assertion_base64)
        .map_err(|e| SignError::AppAttest(format!("Failed to parse assertion: {}", e)))?;

    // 3. Decode the stored public key
    use base64::{engine::general_purpose::STANDARD, Engine};
    let public_key_bytes = STANDARD
        .decode(&device.public_key_base64)
        .map_err(|e| SignError::AppAttest(format!("Failed to decode stored public key: {}", e)))?;

    // 4. Prepare client data
    let client_data = match client_data_json {
        Some(json) => json.to_string(),
        None => format!(r#"{{"challenge":"{}","origin":"app-attest"}}"#, challenge),
    };

    // 5. Verify the assertion
    assertion
        .verify(
            client_data.into_bytes(),
            app_id,
            public_key_bytes,
            device.assertion_counter,
            challenge,
        )
        .map_err(|e| SignError::AppAttest(format!("Assertion verification failed: {}", e)))?;

    // 6. Replay protection - hash the assertion
    use sha2::{Digest, Sha256};
    let assertion_bytes = STANDARD
        .decode(assertion_base64)
        .map_err(|e| SignError::AppAttest(format!("Failed to decode assertion: {}", e)))?;
    let mut hasher = Sha256::new();
    hasher.update(&assertion_bytes);
    let assertion_hash = hex::encode(hasher.finalize());

    // 7. Check replay
    if store.is_assertion_used(device.id, &assertion_hash).await? {
        return Err(SignError::TokenReplay(format!(
            "Assertion with hash {} has already been used",
            assertion_hash
        )));
    }

    // 8. Record the assertion
    store
        .record_assertion(device.id, &assertion_hash, challenge)
        .await?;

    Ok(())
}