use std::collections::HashMap;
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use serde::Serialize;
use crate::attestation::Attestation;
use crate::error::AttestationError;
use crate::hash::canonical_hash;
pub struct Attestor {
key: SigningKey,
key_url: String,
}
impl Attestor {
pub fn new(key: SigningKey, key_url: String) -> Self {
Self { key, key_url }
}
pub fn sign<T: Serialize>(&self, body: &T) -> Result<Attestation, AttestationError> {
let signed_hash = canonical_hash(body)?;
let signature = self.key.sign(signed_hash.as_bytes());
Ok(Attestation::new(
signed_hash,
&signature.to_bytes(),
self.key_url.clone(),
))
}
pub fn verifying_key(&self) -> VerifyingKey {
self.key.verifying_key()
}
pub fn key_url(&self) -> &str {
&self.key_url
}
#[cfg(feature = "audit-stream")]
pub async fn sign_with_audit<T: Serialize>(
&self,
client: &reqwest::Client,
body: &T,
) -> Result<Attestation, AttestationError> {
let signed = self.sign(body)?;
crate::audit_stream::emit(
client,
"attestation_signed",
serde_json::json!({
"key_url": signed.key_url,
"signed_hash": signed.signed_hash,
"signed_at": signed.signed_at,
}),
)
.await;
Ok(signed)
}
}
#[derive(Debug, Default, Clone)]
pub struct Verifier {
keys: HashMap<String, VerifyingKey>,
}
impl Verifier {
pub fn new() -> Self {
Self::default()
}
pub fn trust(&mut self, key_url: impl Into<String>, key: VerifyingKey) -> &mut Self {
self.keys.insert(key_url.into(), key);
self
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn verify<T: Serialize>(
&self,
attestation: &Attestation,
body: &T,
) -> Result<(), AttestationError> {
let Some(key) = self.keys.get(&attestation.key_url) else {
return Err(AttestationError::UntrustedKey(attestation.key_url.clone()));
};
attestation.verify(key, body)
}
#[cfg(feature = "audit-stream")]
pub async fn verify_with_audit<T: Serialize>(
&self,
client: &reqwest::Client,
attestation: &Attestation,
body: &T,
) -> Result<(), AttestationError> {
let outcome = self.verify(attestation, body);
let (kind, reason) = match &outcome {
Ok(()) => ("attestation_verified", None),
Err(err) => ("attestation_failed", Some(err.to_string())),
};
let mut payload = serde_json::json!({
"key_url": attestation.key_url,
"signed_hash": attestation.signed_hash,
"signed_at": attestation.signed_at,
"trusted_keys": self.keys.len(),
});
if let Some(r) = reason {
payload["reason"] = serde_json::Value::String(r);
}
crate::audit_stream::emit(client, kind, payload).await;
outcome
}
}