diff --git a/crates/radicle-cli/examples/rad-id-multi-delegate.md b/crates/radicle-cli/examples/rad-id-multi-delegate.md index 4cf1192a..c297c1c5 100644 --- a/crates/radicle-cli/examples/rad-id-multi-delegate.md +++ b/crates/radicle-cli/examples/rad-id-multi-delegate.md @@ -3,6 +3,16 @@ $ rad id update --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --title "Add Bob" --des 069e7d58faa9a7473d27f5510d676af33282796f ``` +A note for test authors: +> The following `rad watch` command will time out if the target given via `-t` changes. +> This happens for example when the generation of sigrefs changes. +> To recover, temporarily change from `rad watch` to something like +> +> $ sleep 5 +> $ rad inspect --sigrefs rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji +> +> And pick out the result for `z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z`. + ``` ~bob $ rad watch --repo rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji --node z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z -r 'refs/rad/sigrefs' -t c9a828fc2fb01f893d6e6e9e17b9092dea2b3aba -i 500 --timeout 5000ms $ rad sync --fetch rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji diff --git a/crates/radicle-cli/examples/rad-id-threshold.md b/crates/radicle-cli/examples/rad-id-threshold.md index 06a6c85b..9ec2800a 100644 --- a/crates/radicle-cli/examples/rad-id-threshold.md +++ b/crates/radicle-cli/examples/rad-id-threshold.md @@ -150,7 +150,7 @@ Similarly, she still does not have Bob's `rad/sigrefs`: ``` ~alice $ rad inspect --sigrefs -z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ae3c6b77dc1ed51c1c1e6a2772339c2779fa9ba8 +z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi e0e55994a9a234f0b1cd36d8812e2948e2672b7a ``` And she can still list the project, without any worries: @@ -198,6 +198,6 @@ Fetching rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji from the network, found 2 potential s 🌱 Fetched from z6Mkux1aUQD2voWWukVb5nNUR7thrHveQG4pDQua8nVhib7Z 🌱 Fetched from z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk $ rad inspect --sigrefs -z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi ae3c6b77dc1ed51c1c1e6a2772339c2779fa9ba8 +z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi e0e55994a9a234f0b1cd36d8812e2948e2672b7a z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk dace6fe948548168a2bb687718949d9b5d9076ee ``` diff --git a/crates/radicle-cli/examples/rad-sync.md b/crates/radicle-cli/examples/rad-sync.md index f90cd35d..6909be9d 100644 --- a/crates/radicle-cli/examples/rad-sync.md +++ b/crates/radicle-cli/examples/rad-sync.md @@ -15,7 +15,7 @@ $ rad sync status --sort-by alias ╭───────────────────────────────────────────────────╮ │ Node ID Alias ? SigRefs Timestamp │ ├───────────────────────────────────────────────────┤ -│ (you) alice ! 056b1db [..] │ +│ (you) alice ! f2dfe80 [..] │ │ z6Mkt67…v4N1tRk bob ✗ 99c5497 [..] │ │ z6Mkux1…nVhib7Z eve ✗ 99c5497 [..] │ ╰───────────────────────────────────────────────────╯ @@ -37,9 +37,9 @@ $ rad sync status --sort-by alias ╭───────────────────────────────────────────────────╮ │ Node ID Alias ? SigRefs Timestamp │ ├───────────────────────────────────────────────────┤ -│ (you) alice ✓ 056b1db [..] │ -│ z6Mkt67…v4N1tRk bob ✓ 056b1db [..] │ -│ z6Mkux1…nVhib7Z eve ✓ 056b1db [..] │ +│ (you) alice ✓ f2dfe80 [..] │ +│ z6Mkt67…v4N1tRk bob ✓ f2dfe80 [..] │ +│ z6Mkux1…nVhib7Z eve ✓ f2dfe80 [..] │ ╰───────────────────────────────────────────────────╯ ``` diff --git a/crates/radicle-fetch/src/sigrefs.rs b/crates/radicle-fetch/src/sigrefs.rs index 5f353435..77c7c15c 100644 --- a/crates/radicle-fetch/src/sigrefs.rs +++ b/crates/radicle-fetch/src/sigrefs.rs @@ -21,7 +21,7 @@ pub mod error { Load(#[from] Load), } - pub type Load = radicle::storage::refs::Error; + pub type Load = radicle::storage::refs::sigrefs::read::error::Read; } /// A data carrier that associates that data with whether a given @@ -57,7 +57,10 @@ impl DelegateStatus { pub fn load( self, cached: &Cached, - ) -> Result>, radicle::storage::refs::Error> + ) -> Result< + DelegateStatus>, + radicle::storage::refs::sigrefs::read::error::Read, + > where R: AsRef, { diff --git a/crates/radicle-fetch/src/state.rs b/crates/radicle-fetch/src/state.rs index 9de9d0d5..fc75ef61 100644 --- a/crates/radicle-fetch/src/state.rs +++ b/crates/radicle-fetch/src/state.rs @@ -68,7 +68,7 @@ pub mod error { #[error(transparent)] Resolve(#[from] repository::error::Resolve), #[error(transparent)] - Refs(#[from] radicle::storage::refs::Error), + Refs(#[from] radicle::storage::refs::sigrefs::read::error::Read), #[error(transparent)] RemoteRefs(#[from] sigrefs::error::RemoteRefs), #[error("failed to get remote namespaces: {0}")] @@ -683,7 +683,10 @@ where self.verified(oid).map(Some).map_err(error::Canonical::from) } - pub fn load(&self, remote: &PublicKey) -> Result, sigrefs::error::Load> { + pub fn load( + &self, + remote: &PublicKey, + ) -> Result, radicle::storage::refs::sigrefs::read::error::Read> { match self.state.sigrefs.get(remote) { None => SignedRefsAt::load(*remote, self.handle.repository()), Some(tip) => SignedRefsAt::load_at(*tip, *remote, self.handle.repository()).map(Some), diff --git a/crates/radicle/src/identity/doc/update.rs b/crates/radicle/src/identity/doc/update.rs index 0c4b4865..c17c1520 100644 --- a/crates/radicle/src/identity/doc/update.rs +++ b/crates/radicle/src/identity/doc/update.rs @@ -8,7 +8,7 @@ use crate::{ git, identity::crefs::GetCanonicalRefs as _, prelude::Did, - storage::{refs, ReadRepository, RepositoryError}, + storage::{self, refs, ReadRepository, RepositoryError}, }; use super::{Doc, PayloadError, PayloadId, RawDoc, Visibility}; @@ -114,15 +114,12 @@ pub fn privacy_allow_list( /// # Errors /// /// This will fail if an operation using the repository fails. -pub fn delegates( +pub fn delegates( mut raw: RawDoc, additions: Vec, removals: Vec, - repo: &S, -) -> Result>, RepositoryError> -where - S: ReadRepository, -{ + repo: &storage::git::Repository, +) -> Result>, RepositoryError> { if additions.is_empty() && removals.is_empty() { return Ok(Ok(raw)); } @@ -243,20 +240,19 @@ pub fn verify(raw: RawDoc) -> Result { Ok(proposal) } -fn verify_delegates( +fn verify_delegates( proposal: &RawDoc, - repo: &S, -) -> Result>, RepositoryError> -where - S: ReadRepository, -{ + repo: &storage::git::Repository, +) -> Result>, RepositoryError> { let dids = &proposal.delegates; let threshold = proposal.threshold; let (canonical, _) = repo.canonical_head()?; let mut missing = Vec::with_capacity(dids.len()); for did in dids { - match refs::SignedRefsAt::load((*did).into(), repo)? { + match refs::SignedRefsAt::load((*did).into(), repo) + .map_err(|err| storage::Error::Refs(storage::refs::Error::Read(err)))? + { None => { missing.push(error::DelegateVerification::MissingDelegate { did: *did }); } diff --git a/crates/radicle/src/storage.rs b/crates/radicle/src/storage.rs index 22e47be1..e36498f1 100644 --- a/crates/radicle/src/storage.rs +++ b/crates/radicle/src/storage.rs @@ -410,7 +410,7 @@ impl Deref for Remote { /// Read-only operations on a storage instance. pub trait ReadStorage { - type Repository: ReadRepository; + type Repository: ReadRepository + self::refs::sigrefs::git::reference::Reader; /// Get user info for this storage. fn info(&self) -> &UserInfo; diff --git a/crates/radicle/src/storage/git.rs b/crates/radicle/src/storage/git.rs index 61fbd8e9..b6b1d3cb 100644 --- a/crates/radicle/src/storage/git.rs +++ b/crates/radicle/src/storage/git.rs @@ -36,7 +36,7 @@ use crate::git::RefError; use crate::git::UserInfo; pub use crate::storage::{Error, RepositoryError}; -use super::refs::RefsAt; +use super::refs::{sigrefs, RefsAt}; use super::{RemoteId, RemoteRepository, ValidateRepository}; pub static NAMESPACES_GLOB: LazyLock = @@ -169,7 +169,8 @@ impl ReadStorage for Storage { } }; // Nb. This will be `None` if they were not found. - let refs = refs::SignedRefsAt::load(self.info.key, &repo)?; + let refs = refs::SignedRefsAt::load(self.info.key, &repo) + .map_err(|err| Error::Refs(refs::Error::Read(err)))?; let synced_at = refs .as_ref() .map(|r| node::SyncedAt::new(r.at, &repo)) @@ -202,7 +203,9 @@ impl WriteStorage for Storage { let repo = self.repository(rid)?; // N.b. we remove the repository if the `local` peer has no // `rad/sigrefs`. There's no risk of them corrupting data. - let has_sigrefs = SignedRefsAt::load(self.info.key, &repo)?.is_some(); + let has_sigrefs = SignedRefsAt::load(self.info.key, &repo) + .map_err(|err| RepositoryError::Storage(Error::Refs(refs::Error::Read(err))))? + .is_some(); if has_sigrefs { repo.clean(&self.info.key) } else { @@ -256,7 +259,8 @@ impl Storage { rids.map(|rid| { let repo = self.repository(*rid)?; let (_, head) = repo.head()?; - let refs = refs::SignedRefsAt::load(self.info.key, &repo)?; + let refs = refs::SignedRefsAt::load(self.info.key, &repo) + .map_err(|err| RepositoryError::Refs(refs::Error::Read(err)))?; let synced_at = refs .as_ref() .map(|r| SyncedAt::new(r.at, &repo)) @@ -989,15 +993,72 @@ impl SignRepository for Repository { ) -> Result, RepositoryError> { let remote = signer.public_key(); // Ensure the root reference is set, which is checked during sigref verification. - if self.identity_root_of(remote).is_err() { + if self + .reference_oid(remote, &git::refs::storage::IDENTITY_ROOT) + .is_err() + { self.set_remote_identity_root(remote)?; } - let mut refs = self.references_of(remote)?; - refs.remove_sigrefs(); - let signed = refs.signed(signer)?.verified(self)?; - signed.save(self)?; - Ok(signed) + let committer = refs::sigrefs::git::committer(remote, &self.backend.signature()?)?; + let signed = self + .references_of(remote)? + .save(*remote, committer, self, signer)?; + + Ok(signed.sigrefs) + } +} + +impl sigrefs::git::object::Reader for Repository { + fn read_commit( + &self, + oid: &Oid, + ) -> Result>, sigrefs::git::object::error::ReadCommit> { + self.backend.read_commit(oid) + } + + fn read_blob( + &self, + commit: &Oid, + path: &Path, + ) -> Result, sigrefs::git::object::error::ReadBlob> { + self.backend.read_blob(commit, path) + } +} + +impl sigrefs::git::object::Writer for Repository { + fn write_tree( + &self, + refs: sigrefs::git::object::RefsEntry, + signature: sigrefs::git::object::SignatureEntry, + ) -> Result { + self.backend.write_tree(refs, signature) + } + + fn write_commit(&self, bytes: &[u8]) -> Result { + self.backend.write_commit(bytes) + } +} + +impl sigrefs::git::reference::Reader for Repository { + fn find_reference( + &self, + reference: &git::fmt::Namespaced, + ) -> Result, sigrefs::git::reference::error::FindReference> { + sigrefs::git::reference::Reader::find_reference(&self.backend, reference) + } +} + +impl sigrefs::git::reference::Writer for Repository { + fn write_reference( + &self, + reference: &git::fmt::Namespaced, + commit: Oid, + parent: Option, + reflog: String, + ) -> Result<(), sigrefs::git::reference::error::WriteReference> { + self.backend + .write_reference(reference, commit, parent, reflog) } } diff --git a/crates/radicle/src/storage/refs.rs b/crates/radicle/src/storage/refs.rs index a18a6970..c9ad6cd3 100644 --- a/crates/radicle/src/storage/refs.rs +++ b/crates/radicle/src/storage/refs.rs @@ -9,11 +9,10 @@ use std::io; use std::io::{BufRead, BufReader}; use std::marker::PhantomData; use std::ops::Deref; -use std::path::Path; use std::str::FromStr; -use crypto::signature::Signer; -use crypto::{PublicKey, Signature, Unverified, Verified}; +use crypto::signature; +use crypto::{PublicKey, Signature, Verified}; use radicle_core::NodeId; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -21,47 +20,30 @@ use thiserror::Error; use crate::git; use crate::git::raw::ErrorExt as _; use crate::git::Oid; -use crate::node::device::Device; -use crate::profile::env; use crate::storage; -use crate::storage::{ReadRepository, RemoteId, RepoId, WriteRepository}; +use crate::storage::{ReadRepository, RemoteId}; pub use crate::git::refs::storage::*; +use super::HasRepoId; + /// 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] crypto::signature::Error), - #[error("canonical refs: {0}")] - Canonical(#[from] canonical::Error), #[error("invalid reference")] InvalidRef, - #[error("missing identity root reference '{0}'")] - MissingIdentityRoot(git::fmt::RefString), - #[error("missing identity object '{0}'")] - MissingIdentity(Oid), - #[error("mismatched identity: local {local}, remote {remote}")] - MismatchedIdentity { local: RepoId, remote: RepoId }, #[error("invalid reference: {0}")] Ref(#[from] git::RefError), #[error(transparent)] Git(#[from] git::raw::Error), + #[error(transparent)] + Read(#[from] sigrefs::read::error::Read), + #[error(transparent)] + Write(#[from] sigrefs::write::error::Write), } impl Error { @@ -85,26 +67,46 @@ impl Refs { Self(BTreeMap::new()) } - /// Verify the given signature on these refs, and return [`SignedRefs`] on success. - pub fn verified( + /// 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, - signer: PublicKey, - signature: Signature, + namespace: NodeId, + committer: sigrefs::git::Committer, repo: &R, - ) -> Result, Error> { - SignedRefs::new(self, signer, signature).verified(repo) - } - - /// Sign these refs with the given signer and return [`SignedRefs`]. - pub fn signed(self, device: &Device) -> Result, Error> + signer: &S, + ) -> Result where - G: Signer, + S: signature::Signer, + R: sigrefs::git::object::Reader + sigrefs::git::object::Writer, + R: sigrefs::git::reference::Reader + sigrefs::git::reference::Writer, { - let refs = self; - let msg = refs.canonical(); - let signature = device.try_sign(&msg)?; - - Ok(SignedRefs::new(refs, *device.public_key(), signature)) + let msg = "Update signed refs\n"; + let reflog = format!("Save {} signed references", self.len()); + let update = sigrefs::write::SignedRefsWriter::new(self, namespace, repo, signer).write( + committer, + msg.to_string(), + reflog, + )?; + match update { + sigrefs::write::Update::Changed { entry } => Ok(entry.into_sigrefs_at(namespace)), + sigrefs::write::Update::Unchanged { + commit, + refs, + signature, + } => { + let sigrefs = SignedRefs { + refs, + signature, + id: namespace, + _verified: PhantomData, + }; + Ok(SignedRefsAt { + sigrefs, + at: commit, + }) + } + } } /// Get a particular ref. @@ -248,8 +250,7 @@ where /// 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`]. +/// The type parameter keeps track of whether the signature was [`Verified`]. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct SignedRefs { /// The signed refs. @@ -264,61 +265,6 @@ pub struct SignedRefs { _verified: PhantomData, } -impl SignedRefs { - pub fn new(refs: Refs, author: PublicKey, signature: Signature) -> Self { - Self { - refs, - signature, - id: author, - _verified: PhantomData, - } - } - - pub fn verified(self, repo: &R) -> Result, Error> { - match self.verify(repo) { - Ok(()) => Ok(SignedRefs { - refs: self.refs, - signature: self.signature, - id: self.id, - _verified: PhantomData, - }), - Err(e) => Err(e), - } - } - - pub fn verify(&self, repo: &R) -> Result<(), Error> { - let canonical = self.refs.canonical(); - let local = repo.id(); - - // Verify signature. - if let Err(e) = self.id.verify(canonical, &self.signature) { - return Err(e.into()); - } - // If the identity root was signed, verify it points to the right place. - if let Some(id_root) = self.refs.get(&IDENTITY_ROOT) { - // Get the identity at the given oid. - let Ok(doc) = repo.identity_doc_at(id_root) else { - return Err(Error::MissingIdentity(id_root)); - }; - let remote = RepoId::from(doc.blob); - - // Make sure the signed identity points to the local repo identity. - if remote != local { - return Err(Error::MismatchedIdentity { local, remote }); - } - } else { - // TODO(cloudhead): Make this into a hard error (`Error::MissingIdentityRoot`) for - // repos that have migrated to the new identity document schema. - log::debug!( - target: "storage", - "Signed ref verification for {} in {local}: {} is not provided", - self.id, *IDENTITY_ROOT - ); - } - Ok(()) - } -} - impl SignedRefs { /// Returns the [`NodeId`] of the [`SignedRefs`]. pub fn id(&self) -> NodeId { @@ -330,99 +276,44 @@ impl SignedRefs { &self.refs } - pub fn load(remote: RemoteId, repo: &S) -> Result + pub fn load(remote: RemoteId, repo: &R) -> Result where - S: ReadRepository, + R: HasRepoId, + R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader, { - 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()?; - let refs = Refs::from_canonical(refs.content())?; - - SignedRefs::new(refs, remote, signature).verified(repo) - } - - /// 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, repo: &S) -> Result { - let sigref = &SIGREFS_BRANCH; - let remote = &self.id; - let raw = repo.raw(); - - // N.b. if the signatures match then there are no updates - let parent = match SignedRefsAt::load(*remote, repo)? { - Some(SignedRefsAt { sigrefs, at }) if sigrefs.signature == self.signature => { - return Ok(Updated::Unchanged { oid: at }); - } - Some(SignedRefsAt { at, .. }) => Some(raw.find_commit(at.into())?), - None => None, - }; - - let tree = { - 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) - }?; - - let sigref = sigref.with_namespace(remote.into()); - let author = if let Ok(s) = env::var(env::GIT_COMMITTER_DATE) { - let Ok(timestamp) = s.trim().parse::() else { - panic!( - "Invalid timestamp value {s:?} for `{}`", - env::GIT_COMMITTER_DATE - ); - }; - let time = git::raw::Time::new(timestamp, 0); - git::raw::Signature::new("radicle", remote.to_string().as_str(), &time)? - } else { - raw.signature()? - }; - - let commit = raw.commit( - Some(&sigref), - &author, - &author, - "Update signed refs\n", - &tree, - &parent.iter().collect::>(), - ); - - match commit { - Ok(oid) => Ok(Updated::Updated { oid: oid.into() }), - Err(e) => match (e.class(), e.code()) { - (git::raw::ErrorClass::Object, git::raw::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, - id: self.id, + let root = repo.rid(); + let tip = sigrefs::read::Tip::Reference(remote); + let latest = sigrefs::SignedRefsReader::new(root, tip, repo, &remote).read()?; + let signature = *latest.signature(); + let refs = latest.into_refs(); + Ok(SignedRefs { + refs, + signature, + id: remote, _verified: PhantomData, - } + }) + } + + pub fn load_at( + oid: Oid, + remote: RemoteId, + repo: &R, + ) -> Result + where + R: HasRepoId, + R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader, + { + let root = repo.rid(); + let tip = sigrefs::read::Tip::Commit(oid); + let latest = sigrefs::SignedRefsReader::new(root, tip, repo, &remote).read()?; + let signature = *latest.signature(); + let refs = latest.into_refs(); + Ok(SignedRefs { + refs, + signature, + id: remote, + _verified: PhantomData, + }) } } @@ -452,15 +343,24 @@ pub struct RefsAt { } impl RefsAt { - pub fn new( - repo: &S, - remote: RemoteId, - ) -> Result { - let at = repo.reference_oid(&remote, &storage::refs::SIGREFS_BRANCH)?; + pub fn new(repo: &R, remote: RemoteId) -> Result + where + R: sigrefs::git::reference::Reader, + { + let at = repo + .find_reference( + &storage::refs::SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(&remote)), + ) + .map_err(sigrefs::read::error::Read::FindReference)? + .ok_or_else(|| sigrefs::read::error::Read::MissingSigrefs { namespace: remote })?; Ok(RefsAt { remote, at }) } - pub fn load(&self, repo: &S) -> Result { + pub fn load(&self, repo: &R) -> Result + where + R: HasRepoId, + R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader, + { SignedRefsAt::load_at(self.at, self.remote, repo) } @@ -488,21 +388,28 @@ impl SignedRefsAt { /// /// This will return `None` if the branch was not found, all other /// errors are returned. - pub fn load(remote: RemoteId, repo: &S) -> Result, Error> + pub fn load(remote: RemoteId, repo: &R) -> Result, sigrefs::read::error::Read> where - S: ReadRepository, + R: HasRepoId, + R: ReadRepository, + R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader, { let at = match RefsAt::new(repo, remote) { Ok(RefsAt { at, .. }) => at, - Err(e) if e.is_not_found() => return Ok(None), - Err(e) => return Err(e.into()), + Err(sigrefs::read::error::Read::MissingSigrefs { .. }) => return Ok(None), + Err(e) => return Err(e), }; Self::load_at(at, remote, repo).map(Some) } - pub fn load_at(at: Oid, remote: RemoteId, repo: &S) -> Result + pub fn load_at( + at: Oid, + remote: RemoteId, + repo: &R, + ) -> Result where - S: storage::ReadRepository, + R: HasRepoId, + R: sigrefs::git::object::Reader + sigrefs::git::reference::Reader, { Ok(Self { sigrefs: SignedRefs::load_at(at, remote, repo)?, @@ -547,6 +454,8 @@ mod tests { use super::*; use crate::assert_matches; + use crate::node::device::Device; + use crate::storage::WriteRepository as _; use crate::{cob::identity::Identity, cob::Title, rad, test::fixtures, Storage}; #[quickcheck] @@ -709,8 +618,14 @@ mod tests { // only modifies his own namespace. Note that anyone (eg. Eve) could create a reference // under her copy of Bob's namespace, and this would only be rejected during signed ref // validation. - let result = bob_paris_sigrefs.save(&london).unwrap(); - assert_matches!(result, Updated::Updated { .. }); + { + let name = &SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(bob.node_id())); + let id = paris.backend.refname_to_id(name.as_str()).unwrap(); + london + .backend + .reference(name.as_str(), id, true, "Graft attack") + .unwrap(); + } london .raw() @@ -727,11 +642,13 @@ mod tests { // The graft is not allowed. assert_matches!( london.remote(bob.public_key()), - Err(Error::MismatchedIdentity { - local, - remote, - }) - if local == london_rid && remote == paris_rid + Err(Error::Read(sigrefs::read::error::Read::Verify(sigrefs::read::error::Verify::MismatchedIdentity { + expected, + found, + sigrefs_commit: _, + identity_commit: _, + }))) + if expected == london_rid && found == paris_rid ); } } diff --git a/crates/radicle/src/storage/refs/sigrefs/write.rs b/crates/radicle/src/storage/refs/sigrefs/write.rs index 59ce3123..fae8d687 100644 --- a/crates/radicle/src/storage/refs/sigrefs/write.rs +++ b/crates/radicle/src/storage/refs/sigrefs/write.rs @@ -3,9 +3,11 @@ pub mod error; #[cfg(test)] mod test; +use std::marker::PhantomData; use std::path::Path; use crypto::signature::Signer; +use crypto::PublicKey; use radicle_core::NodeId; use radicle_git_metadata::author::Author; use radicle_git_metadata::commit::{headers::Headers, trailers::OwnedTrailer, CommitData}; @@ -16,6 +18,7 @@ use crate::storage::refs::sigrefs::git::{object, reference, Committer}; use crate::storage::refs::{ Refs, IDENTITY_ROOT, REFS_BLOB_PATH, SIGNATURE_BLOB_PATH, SIGREFS_BRANCH, SIGREFS_PARENT, }; +use crate::storage::refs::{SignedRefs, SignedRefsAt}; /// The result of calling [`SignedRefsWriter::write`]. #[derive(Clone, Debug, PartialEq, Eq)] @@ -160,11 +163,23 @@ pub struct Commit { signature: crypto::Signature, } -#[cfg(test)] impl Commit { + #[cfg(test)] pub(super) fn into_refs(self) -> Refs { self.refs } + + pub(crate) fn into_sigrefs_at(self, id: PublicKey) -> SignedRefsAt { + SignedRefsAt { + at: self.oid, + sigrefs: SignedRefs { + id, + signature: self.signature, + refs: self.refs, + _verified: PhantomData, + }, + } + } } struct CommitWriter<'a, R, S> { diff --git a/crates/radicle/src/test/storage.rs b/crates/radicle/src/test/storage.rs index 69d19f51..c8ca919a 100644 --- a/crates/radicle/src/test/storage.rs +++ b/crates/radicle/src/test/storage.rs @@ -4,6 +4,8 @@ use std::io; use std::path::{Path, PathBuf}; use std::str::FromStr; +use crypto::PublicKey; + pub use crate::git; use crate::git::fmt; @@ -152,6 +154,30 @@ impl MockRepository { } } +impl self::refs::sigrefs::git::reference::Reader for MockRepository { + fn find_reference( + &self, + reference: &git::fmt::Namespaced, + ) -> Result, refs::sigrefs::git::reference::error::FindReference> { + use refs::sigrefs::git::reference::error::FindReference; + let ns = reference.namespace(); + + let remote: PublicKey = ns.as_str().parse().map_err(FindReference::other)?; + let reference = reference.strip_namespace(); + + match self.remotes.get(&remote) { + None => Ok(None), + Some(refs) => { + if reference == *refs::SIGREFS_BRANCH { + Ok(Some(refs.at)) + } else { + Ok(refs.sigrefs.get(&reference)) + } + } + } + } +} + impl RemoteRepository for MockRepository { fn remote(&self, id: &RemoteId) -> Result, refs::Error> { self.remotes