Fully verify fetches

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-09-27 14:14:19 +02:00
parent 27e39c0df5
commit 85f9a1afc2
No known key found for this signature in database
1 changed files with 54 additions and 26 deletions

View File

@ -83,7 +83,7 @@ impl WriteStorage for Storage {
type Repository = Repository; type Repository = Repository;
fn repository(&self, proj: Id) -> Result<Self::Repository, Error> { fn repository(&self, proj: Id) -> Result<Self::Repository, Error> {
Repository::open(paths::repository(self, &proj)) Repository::open(paths::repository(self, &proj), proj)
} }
fn sign_refs<G: Signer>( fn sign_refs<G: Signer>(
@ -154,10 +154,8 @@ impl Storage {
} }
pub struct Repository { pub struct Repository {
pub id: Id,
pub(crate) backend: git2::Repository, pub(crate) backend: git2::Repository,
// TODO: Add project id here so we can refer to it
// in a bunch of places. We could write it to the
// git config for later.
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
@ -168,6 +166,8 @@ pub enum VerifyError {
InvalidRefTarget(RemoteId, RefString, git2::Oid), InvalidRefTarget(RemoteId, RefString, git2::Oid),
#[error("invalid reference")] #[error("invalid reference")]
InvalidRef, InvalidRef,
#[error("invalid identity: {0}")]
InvalidIdentity(#[from] IdentityError),
#[error("ref error: {0}")] #[error("ref error: {0}")]
Ref(#[from] git::RefError), Ref(#[from] git::RefError),
#[error("refs error: {0}")] #[error("refs error: {0}")]
@ -181,7 +181,7 @@ pub enum VerifyError {
} }
impl Repository { impl Repository {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> { pub fn open<P: AsRef<Path>>(path: P, id: Id) -> Result<Self, Error> {
let backend = match git2::Repository::open_bare(path.as_ref()) { let backend = match git2::Repository::open_bare(path.as_ref()) {
Err(e) if ext::is_not_found_err(&e) => { Err(e) if ext::is_not_found_err(&e) => {
let backend = git2::Repository::init_opts( let backend = git2::Repository::init_opts(
@ -203,7 +203,7 @@ impl Repository {
Err(e) => Err(e), Err(e) => Err(e),
}?; }?;
Ok(Self { backend }) Ok(Self { id, backend })
} }
pub fn head(&self) -> Result<git2::Commit, git2::Error> { pub fn head(&self) -> Result<git2::Commit, git2::Error> {
@ -242,12 +242,14 @@ impl Repository {
} }
} }
// The refs that are left in the map, are ones that were signed, but are not for (remote, refs) in remotes.into_iter() {
// in the repository. // The refs that are left in the map, are ones that were signed, but are not
for (id, refs) in remotes.into_iter() { // in the repository.
if let Some((name, _)) = refs.into_iter().next() { if let Some((name, _)) = refs.into_iter().next() {
return Err(VerifyError::MissingRef(id, name)); return Err(VerifyError::MissingRef(remote, name));
} }
// Verify identity history of remote.
self.identity(&remote)?.verified(self.id)?;
} }
Ok(()) Ok(())
@ -478,29 +480,54 @@ impl ReadRepository for Repository {
impl WriteRepository for Repository { impl WriteRepository for Repository {
/// Fetch all remotes of a project from the given URL. /// Fetch all remotes of a project from the given URL.
/// This is the primary way in which projects are updated on the network.
///
/// Since we're operating in an untrusted network, we have to be take some precautions
/// when fetching from a remote. We don't want to fetch straight into a public facing
/// repository because if the updates were to be invalid, we'd be allowing others to
/// read this invalid state. We also don't want to lock our repositories during the fetch
/// or verification, as this will make the repositories unavailable. Therefore, we choose
/// to perform the fetch into a "staging" copy of the given repository we're fetching, and
/// then transfer the changes to the canonical, public copy of the repository.
///
/// To do this, we first create a temporary directory, and clone the canonical repo into it.
/// This local clone takes advantage of the fact that both repositories live on the same
/// host (or even file-system). We now have a "staging" copy and the canonical copy.
///
/// We then fetch the *remote* repo into the *staging* copy. We turn off pruning because we
/// don't want to accidentally delete any objects before verification is complete.
///
/// We proceed to verify the staging copy through the usual verification process.
///
/// If verification succeeds, we fetch from the staging copy into the canonical repo,
/// with pruning *on*, and discard the staging copy. If it fails, we just discard the
/// staging copy.
///
fn fetch(&mut self, url: &git::Url) -> Result<Vec<RefUpdate>, FetchError> { fn fetch(&mut self, url: &git::Url) -> Result<Vec<RefUpdate>, FetchError> {
// TODO: Have function to fetch specific remotes. // TODO: Have function to fetch specific remotes.
// //
// Repository layout should look like this: // The steps are summarized in the following diagram:
// //
// /refs/remotes/<remote> // staging <- git-clone -- local (canonical) # create staging copy
// /heads // staging <- git-fetch -- remote # fetch from remote
// /master //
// /tags // ... verify ...
// ... //
// local <- git-fetch -- staging # fetch from staging copy
// //
let url = url.to_string(); let url = url.to_string();
let refs: &[&str] = &["refs/remotes/*:refs/remotes/*"]; let refs: &[&str] = &["refs/remotes/*:refs/remotes/*"];
let mut updates = Vec::new(); let mut updates = Vec::new();
let mut callbacks = git2::RemoteCallbacks::new(); let mut callbacks = git2::RemoteCallbacks::new();
let tempdir = tempfile::tempdir()?; let tempdir = tempfile::tempdir()?;
// TODO: Comment
// Create staging copy.
let staging = { let staging = {
let mut builder = git2::build::RepoBuilder::new(); let mut builder = git2::build::RepoBuilder::new();
let path = tempdir.path().join("git"); let path = tempdir.path().join("git");
let staging_repo = builder let staging_repo = builder
.bare(true) .bare(true)
// TODO: Comment // Using `clone_local` will try to hard-link the ODBs for better performance.
// TODO: Due to this, I think we'll have to run GC when there is a failure. // TODO: Due to this, I think we'll have to run GC when there is a failure.
.clone_local(git2::build::CloneLocal::Local) .clone_local(git2::build::CloneLocal::Local)
.clone( .clone(
@ -517,11 +544,17 @@ impl WriteRepository for Repository {
let mut opts = git2::FetchOptions::default(); let mut opts = git2::FetchOptions::default();
opts.prune(git2::FetchPrune::Off); opts.prune(git2::FetchPrune::Off);
// Fetch from the remote into the staging copy.
staging_repo staging_repo
.remote_anonymous(&url)? .remote_anonymous(&url)?
.fetch(refs, Some(&mut opts), None)?; .fetch(refs, Some(&mut opts), None)?;
// TODO: Comment
Repository::from(staging_repo).verify()?; // Verify the staging copy as if it was the canonical copy.
Repository {
id: self.id,
backend: staging_repo,
}
.verify()?;
path path
}; };
@ -552,6 +585,7 @@ impl WriteRepository for Repository {
// TODO: Make sure we verify before pruning, as pruning may get us into // TODO: Make sure we verify before pruning, as pruning may get us into
// a state we can't roll back. // a state we can't roll back.
opts.prune(git2::FetchPrune::On); opts.prune(git2::FetchPrune::On);
// Fetch from the staging copy into the canonical repo.
remote.fetch(refs, Some(&mut opts), None)?; remote.fetch(refs, Some(&mut opts), None)?;
} }
@ -563,12 +597,6 @@ impl WriteRepository for Repository {
} }
} }
impl From<git2::Repository> for Repository {
fn from(backend: git2::Repository) -> Self {
Self { backend }
}
}
pub mod trailers { pub mod trailers {
use std::str::FromStr; use std::str::FromStr;