radicle/sigrefs: Switch to new implementation
This commit is contained in:
parent
d3bc868e84
commit
d40fa9a347
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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 [..] │
|
||||
╰───────────────────────────────────────────────────╯
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue