radicle/sigrefs: Switch to new implementation

This commit is contained in:
Fintan Halpenny 2026-02-24 15:16:36 +00:00 committed by Lorenz Leutgeb
parent d3bc868e84
commit d40fa9a347
11 changed files with 276 additions and 245 deletions

View File

@ -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

View File

@ -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
```

View File

@ -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 [..] │
╰───────────────────────────────────────────────────╯
```

View File

@ -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<T> DelegateStatus<T> {
pub fn load<R, S>(
self,
cached: &Cached<R, S>,
) -> Result<DelegateStatus<Option<SignedRefsAt>>, radicle::storage::refs::Error>
) -> Result<
DelegateStatus<Option<SignedRefsAt>>,
radicle::storage::refs::sigrefs::read::error::Read,
>
where
R: AsRef<Repository>,
{

View File

@ -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<Option<SignedRefsAt>, sigrefs::error::Load> {
pub fn load(
&self,
remote: &PublicKey,
) -> Result<Option<SignedRefsAt>, 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),

View File

@ -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<S>(
pub fn delegates(
mut raw: RawDoc,
additions: Vec<Did>,
removals: Vec<Did>,
repo: &S,
) -> Result<Result<RawDoc, Vec<error::DelegateVerification>>, RepositoryError>
where
S: ReadRepository,
{
repo: &storage::git::Repository,
) -> Result<Result<RawDoc, Vec<error::DelegateVerification>>, RepositoryError> {
if additions.is_empty() && removals.is_empty() {
return Ok(Ok(raw));
}
@ -243,20 +240,19 @@ pub fn verify(raw: RawDoc) -> Result<Doc, error::DocVerification> {
Ok(proposal)
}
fn verify_delegates<S>(
fn verify_delegates(
proposal: &RawDoc,
repo: &S,
) -> Result<Option<Vec<error::DelegateVerification>>, RepositoryError>
where
S: ReadRepository,
{
repo: &storage::git::Repository,
) -> Result<Option<Vec<error::DelegateVerification>>, 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 });
}

View File

@ -410,7 +410,7 @@ impl<V> Deref for Remote<V> {
/// 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;

View File

@ -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<PatternString> =
@ -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<SignedRefs<Verified>, 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<Option<Vec<u8>>, sigrefs::git::object::error::ReadCommit> {
self.backend.read_commit(oid)
}
fn read_blob(
&self,
commit: &Oid,
path: &Path,
) -> Result<Option<sigrefs::git::object::Blob>, 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<Oid, sigrefs::git::object::error::WriteTree> {
self.backend.write_tree(refs, signature)
}
fn write_commit(&self, bytes: &[u8]) -> Result<Oid, sigrefs::git::object::error::WriteCommit> {
self.backend.write_commit(bytes)
}
}
impl sigrefs::git::reference::Reader for Repository {
fn find_reference(
&self,
reference: &git::fmt::Namespaced,
) -> Result<Option<Oid>, 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<Oid>,
reflog: String,
) -> Result<(), sigrefs::git::reference::error::WriteReference> {
self.backend
.write_reference(reference, commit, parent, reflog)
}
}

View File

@ -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<R: ReadRepository>(
/// Save the signed refs to disk.
/// This creates a new commit on the signed refs branch, and updates the branch pointer.
pub fn save<R, S>(
self,
signer: PublicKey,
signature: Signature,
namespace: NodeId,
committer: sigrefs::git::Committer,
repo: &R,
) -> Result<SignedRefs<Verified>, Error> {
SignedRefs::new(self, signer, signature).verified(repo)
}
/// Sign these refs with the given signer and return [`SignedRefs`].
pub fn signed<G>(self, device: &Device<G>) -> Result<SignedRefs<Unverified>, Error>
signer: &S,
) -> Result<SignedRefsAt, Error>
where
G: Signer<crypto::Signature>,
S: signature::Signer<crypto::Signature>,
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<V> {
/// The signed refs.
@ -264,61 +265,6 @@ pub struct SignedRefs<V> {
_verified: PhantomData<V>,
}
impl SignedRefs<Unverified> {
pub fn new(refs: Refs, author: PublicKey, signature: Signature) -> Self {
Self {
refs,
signature,
id: author,
_verified: PhantomData,
}
}
pub fn verified<R: ReadRepository>(self, repo: &R) -> Result<SignedRefs<Verified>, 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<R: ReadRepository>(&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<Verified> {
/// Returns the [`NodeId`] of the [`SignedRefs`].
pub fn id(&self) -> NodeId {
@ -330,99 +276,44 @@ impl SignedRefs<Verified> {
&self.refs
}
pub fn load<S>(remote: RemoteId, repo: &S) -> Result<Self, Error>
pub fn load<R>(remote: RemoteId, repo: &R) -> Result<Self, sigrefs::read::error::Read>
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<S>(oid: Oid, remote: RemoteId, repo: &S) -> Result<Self, Error>
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<S: WriteRepository>(&self, repo: &S) -> Result<Updated, Error> {
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::<i64>() 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::<Vec<&git::raw::Commit>>(),
);
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<Unverified> {
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<R>(
oid: Oid,
remote: RemoteId,
repo: &R,
) -> Result<Self, sigrefs::read::error::Read>
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<S: ReadRepository>(
repo: &S,
remote: RemoteId,
) -> Result<Self, crate::git::raw::Error> {
let at = repo.reference_oid(&remote, &storage::refs::SIGREFS_BRANCH)?;
pub fn new<R>(repo: &R, remote: RemoteId) -> Result<Self, sigrefs::read::error::Read>
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<S: ReadRepository>(&self, repo: &S) -> Result<SignedRefsAt, Error> {
pub fn load<R>(&self, repo: &R) -> Result<SignedRefsAt, sigrefs::read::error::Read>
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<S>(remote: RemoteId, repo: &S) -> Result<Option<Self>, Error>
pub fn load<R>(remote: RemoteId, repo: &R) -> Result<Option<Self>, 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<S>(at: Oid, remote: RemoteId, repo: &S) -> Result<Self, Error>
pub fn load_at<R>(
at: Oid,
remote: RemoteId,
repo: &R,
) -> Result<Self, sigrefs::read::error::Read>
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
);
}
}

View File

@ -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> {

View File

@ -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<Option<Oid>, 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<Remote<Verified>, refs::Error> {
self.remotes