use std::collections::BTreeMap; use std::fmt::Debug; use std::io; use std::io::{BufRead, BufReader}; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::path::Path; use std::str::FromStr; use crypto::{PublicKey, Signature, Signer, SignerError, Unverified, Verified}; use serde::Serialize; use thiserror::Error; use crate::git; use crate::git::ext as git_ext; use crate::git::Oid; use crate::storage; use crate::storage::{ReadRepository, RemoteId, WriteRepository}; pub use crate::git::refs::storage::*; /// File in which the signed references are stored, in the `refs/rad/sigrefs` branch. pub const REFS_BLOB_PATH: &str = "refs"; /// File in which the signature over the references is stored in the `refs/rad/sigrefs` branch. pub const SIGNATURE_BLOB_PATH: &str = "signature"; #[derive(Debug)] pub enum Updated { /// The computed [`Refs`] were stored as a new commit. Updated { oid: Oid }, /// The stored [`Refs`] were the same as the computed ones, so no new commit /// was created. Unchanged { oid: Oid }, } #[derive(Debug, Error)] pub enum Error { #[error("invalid signature: {0}")] InvalidSignature(#[from] crypto::Error), #[error("signer error: {0}")] Signer(#[from] SignerError), #[error("canonical refs: {0}")] Canonical(#[from] canonical::Error), #[error("invalid reference")] InvalidRef, #[error("invalid reference: {0}")] Ref(#[from] git::RefError), #[error(transparent)] Git(#[from] git2::Error), #[error(transparent)] GitExt(#[from] git_ext::Error), } impl Error { /// Whether this error is caused by a reference not being found. pub fn is_not_found(&self) -> bool { match self { Self::GitExt(git::Error::NotFound(_)) => true, Self::GitExt(git::Error::Git(e)) if git::is_not_found_err(e) => true, _ => false, } } } /// The published state of a local repository. #[derive(Default, Clone, Debug, PartialEq, Eq, Serialize)] pub struct Refs(BTreeMap); impl Refs { /// Verify the given signature on these refs, and return [`SignedRefs`] on success. pub fn verified( self, signer: &PublicKey, signature: Signature, ) -> Result, Error> { let refs = self; let msg = refs.canonical(); match signer.verify(msg, &signature) { Ok(()) => Ok(SignedRefs { refs, signature, _verified: PhantomData, }), Err(e) => Err(e.into()), } } /// Sign these refs with the given signer and return [`SignedRefs`]. pub fn signed(self, signer: &G) -> Result, Error> where G: Signer, { let refs = self; let msg = refs.canonical(); let signature = signer.try_sign(&msg)?; Ok(SignedRefs { refs, signature, _verified: PhantomData, }) } /// Get a particular ref. pub fn get(&self, name: &git::Qualified) -> Option { self.0.get(name.to_ref_string().as_refstr()).copied() } /// Get a particular head ref. pub fn head(&self, name: impl AsRef) -> Option { let branch = git::refname!("refs/heads").join(name); self.0.get(&branch).copied() } /// Create refs from a canonical representation. pub fn from_canonical(bytes: &[u8]) -> Result { let reader = BufReader::new(bytes); let mut refs = BTreeMap::new(); for line in reader.lines() { let line = line?; let (oid, name) = line .split_once(' ') .ok_or(canonical::Error::InvalidFormat)?; let name = git::RefString::try_from(name)?; let oid = Oid::from_str(oid)?; if oid.is_zero() { continue; } refs.insert(name, oid); } Ok(Self(refs)) } pub fn canonical(&self) -> Vec { let mut buf = String::new(); let refs = self .iter() .filter(|(name, oid)| name.as_refstr() != SIGREFS_BRANCH.as_ref() && !oid.is_zero()); for (name, oid) in refs { buf.push_str(&oid.to_string()); buf.push(' '); buf.push_str(name); buf.push('\n'); } buf.into_bytes() } } impl IntoIterator for Refs { type Item = (git::RefString, Oid); type IntoIter = std::collections::btree_map::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } impl From for BTreeMap { fn from(refs: Refs) -> Self { refs.0 } } impl From> for Refs { fn from(signed: SignedRefs) -> Self { signed.refs } } impl From> for Refs { fn from(refs: BTreeMap) -> Self { Self(refs) } } impl Deref for Refs { type Target = BTreeMap; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Refs { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } /// Combination of [`Refs`] and a [`Signature`]. The signature is a cryptographic /// signature over the refs. This allows us to easily verify if a set of refs /// came from a particular key. /// /// The type parameter keeps track of whether the signature was [`Verified`] or /// [`Unverified`]. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct SignedRefs { pub refs: Refs, #[serde(skip)] pub signature: Signature, #[serde(skip)] _verified: PhantomData, } impl SignedRefs { pub fn new(refs: Refs, signature: Signature) -> Self { Self { refs, signature, _verified: PhantomData, } } pub fn verified(self, signer: &PublicKey) -> Result, crypto::Error> { match self.verify(signer) { Ok(()) => Ok(SignedRefs { refs: self.refs, signature: self.signature, _verified: PhantomData, }), Err(e) => Err(e), } } pub fn verify(&self, signer: &PublicKey) -> Result<(), crypto::Error> { let canonical = self.refs.canonical(); match signer.verify(canonical, &self.signature) { Ok(()) => Ok(()), Err(e) => Err(e), } } } impl SignedRefs { pub fn load(remote: &RemoteId, repo: &S) -> Result where S: ReadRepository, { let oid = repo.reference_oid(remote, &SIGREFS_BRANCH)?; SignedRefs::load_at(oid, remote, repo) } pub fn load_at(oid: Oid, remote: &RemoteId, repo: &S) -> Result where S: storage::ReadRepository, { let refs = repo.blob_at(oid, Path::new(REFS_BLOB_PATH))?; let signature = repo.blob_at(oid, Path::new(SIGNATURE_BLOB_PATH))?; let signature: crypto::Signature = signature.content().try_into()?; match remote.verify(refs.content(), &signature) { Ok(()) => { let refs = Refs::from_canonical(refs.content())?; Ok(Self { refs, signature, _verified: PhantomData, }) } Err(e) => Err(e.into()), } } /// Save the signed refs to disk. /// This creates a new commit on the signed refs branch, and updates the branch pointer. pub fn save( &self, // TODO: This should be part of the signed refs. remote: &RemoteId, repo: &S, ) -> Result { let sigref = &SIGREFS_BRANCH; let parent = match repo.reference(remote, sigref) { Ok(r) => Some(r.peel_to_commit()?), Err(git_ext::Error::Git(e)) if git_ext::is_not_found_err(&e) => None, Err(git_ext::Error::NotFound(_)) => None, Err(e) => return Err(e.into()), }; let tree = { let raw = repo.raw(); let refs_blob_oid = raw.blob(&self.canonical())?; let sig_blob_oid = raw.blob(self.signature.as_ref())?; let mut builder = raw.treebuilder(None)?; builder.insert(REFS_BLOB_PATH, refs_blob_oid, 0o100_644)?; builder.insert(SIGNATURE_BLOB_PATH, sig_blob_oid, 0o100_644)?; let oid = builder.write()?; raw.find_tree(oid) }?; if let Some(ref parent) = parent { if parent.tree()?.id() == tree.id() { return Ok(Updated::Unchanged { oid: parent.id().into(), }); } } let sigref = sigref.with_namespace(remote.into()); let author = repo.raw().signature()?; let commit = repo.raw().commit( Some(&sigref), &author, &author, &format!("Update signature for {}\n", remote), &tree, &parent.iter().collect::>(), ); match commit { Ok(oid) => Ok(Updated::Updated { oid: oid.into() }), Err(e) => match (e.class(), e.code()) { (git2::ErrorClass::Object, git2::ErrorCode::Modified) => { log::warn!("Concurrent modification of refs: {:?}", e); Err(Error::Git(e)) } _ => Err(e.into()), }, } } pub fn unverified(self) -> SignedRefs { SignedRefs { refs: self.refs, signature: self.signature, _verified: PhantomData, } } } impl Deref for SignedRefs { type Target = Refs; fn deref(&self) -> &Self::Target { &self.refs } } pub mod canonical { use super::*; #[derive(Debug, thiserror::Error)] pub enum Error { #[error(transparent)] InvalidRef(#[from] git_ref_format::Error), #[error("invalid canonical format")] InvalidFormat, #[error(transparent)] Io(#[from] io::Error), #[error(transparent)] Git(#[from] git2::Error), } } #[cfg(test)] mod tests { use super::*; use qcheck_macros::quickcheck; #[quickcheck] fn prop_canonical_roundtrip(refs: Refs) { let encoded = refs.canonical(); let decoded = Refs::from_canonical(&encoded).unwrap(); assert_eq!(refs, decoded); } }