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};
#[async_trait::async_trait]
pub trait DeviceStore: Send + Sync {
async fn get_device(&self, key_id: &str) -> Result<Option<DeviceInfo>>;
async fn register_device(
&self,
key_id: &str,
bundle_id: &str,
public_key_base64: &str,
) -> Result<()>;
async fn is_assertion_used(&self, device_id: i64, assertion_hash: &str) -> Result<bool>;
async fn record_assertion(
&self,
device_id: i64,
assertion_hash: &str,
challenge: &str,
) -> Result<()>;
}
#[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,
}
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<()> {
let device = store.get_device(key_id).await?.ok_or_else(|| {
SignError::DeviceNotRegistered(format!("No registration for key_id: {}", key_id))
})?;
let assertion = Assertion::from_base64(assertion_base64)
.map_err(|e| SignError::AppAttest(format!("Failed to parse assertion: {}", e)))?;
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)))?;
let client_data = match client_data_json {
Some(json) => json.to_string(),
None => format!(r#"{{"challenge":"{}","origin":"app-attest"}}"#, challenge),
};
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)))?;
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());
if store.is_assertion_used(device.id, &assertion_hash).await? {
return Err(SignError::TokenReplay(format!(
"Assertion with hash {} has already been used",
assertion_hash
)));
}
store
.record_assertion(device.id, &assertion_hash, challenge)
.await?;
Ok(())
}