use auths_id::attestation::encoders::json_encoder;
use auths_id::attestation::export::{AttestationEncoder, AttestationSink};
use auths_id::error::StorageError;
use auths_id::storage::layout::{
StorageLayoutConfig, attestation_blob_name, attestation_ref_for_device,
};
use auths_verifier::core::{Attestation, VerifiedAttestation};
use auths_verifier::types::DeviceDID;
use git2::{Repository, Signature, Tree};
use log::{debug, info};
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone)]
pub struct GitRefSink {
repo_path: PathBuf,
encoder: AttestationEncoder,
config: StorageLayoutConfig,
}
impl GitRefSink {
pub fn new(
repo_path: impl Into<PathBuf>,
encoder: AttestationEncoder,
config: StorageLayoutConfig,
) -> Self {
Self {
repo_path: repo_path.into(),
encoder,
config,
}
}
pub fn with_config(repo_path: impl Into<PathBuf>, config: StorageLayoutConfig) -> Self {
Self::new(repo_path, Arc::new(json_encoder), config)
}
pub fn with_defaults(repo_path: impl Into<PathBuf>) -> Self {
Self::new(
repo_path,
Arc::new(json_encoder),
StorageLayoutConfig::default(),
)
}
fn open_repo(&self) -> Result<Repository, StorageError> {
Ok(Repository::open(&self.repo_path)?)
}
fn read_attestation_from_tree(
&self,
repo: &Repository,
tree: &Tree,
) -> Result<Attestation, StorageError> {
let blob_filename = attestation_blob_name(&self.config);
let entry = tree.get_name(blob_filename).ok_or_else(|| {
StorageError::NotFound(format!(
"Attestation tree missing blob named '{}'",
blob_filename
))
})?;
let blob = repo.find_blob(entry.id())?;
Ok(serde_json::from_slice(blob.content())?)
}
}
impl AttestationSink for GitRefSink {
fn export(&self, attestation: &VerifiedAttestation) -> Result<(), StorageError> {
let attestation = attestation.inner();
info!(
"Exporting attestation for device {} using configured layout...",
attestation.subject
);
let repo = self.open_repo()?;
let content = (self.encoder)(attestation)?;
let blob_oid = repo.blob(&content)?;
let blob_filename = attestation_blob_name(&self.config);
let mut tree_builder = repo.treebuilder(None)?;
tree_builder.insert(blob_filename, blob_oid, 0o100644)?;
let tree_oid = tree_builder.write()?;
let tree = repo.find_tree(tree_oid)?;
debug!(
"Created tree {} with blob {} ('{}')",
tree_oid, blob_oid, blob_filename
);
#[allow(clippy::disallowed_methods)]
let device_did = DeviceDID::new_unchecked(attestation.subject.as_str());
let ref_path = attestation_ref_for_device(&self.config, &device_did);
debug!("Target ref path for export: {}", ref_path);
let parent_commit = repo
.find_reference(&ref_path)
.ok()
.and_then(|reference| reference.peel_to_commit().ok());
let parents = parent_commit.iter().collect::<Vec<_>>();
if let Some(p) = parent_commit.as_ref() {
debug!("Found parent commit on ref '{}': {}", ref_path, p.id());
} else {
debug!(
"No parent commit found for ref '{}', creating initial commit on this ref.",
ref_path
);
}
let previous_attestation = parent_commit.as_ref().and_then(|p| {
p.tree()
.ok()
.and_then(|t| self.read_attestation_from_tree(&repo, &t).ok())
});
let message = if attestation.is_revoked()
&& !previous_attestation
.as_ref()
.is_some_and(|pa| pa.is_revoked())
{
"🛑 Revoked device attestation"
} else if let Some(prev) = &previous_attestation {
if *attestation != *prev {
"🔄 Updated device attestation"
} else {
"📄 Updated device attestation record (no change detected)"
}
} else {
"✅ Linked device attestation"
};
debug!("Commit message determined: '{}'", message);
#[allow(clippy::disallowed_methods)]
let now = chrono::Utc::now();
let author = repo.signature().or_else(|_| {
Signature::new(
"auths",
"auths@localhost",
&git2::Time::new(now.timestamp(), 0),
)
})?;
debug!("Using Git author/committer: {}", author);
let commit_oid = repo.commit(None, &author, &author, message, &tree, &parents)?;
debug!("Created attestation commit object {}", commit_oid);
let ref_log_message = format!("commit (attestation): {}", message);
repo.reference(&ref_path, commit_oid, true, &ref_log_message)?;
info!(
"Saved attestation commit {} and updated ref '{}'",
commit_oid, ref_path
);
Ok(())
}
}