Verify identity root commit signatures

In RIP-2, we specify that identity document root commits must include
the signature of all founding delegates.

This commit adds the required signatures during document creation
and verifies them on load.

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-12-12 13:41:59 +01:00
parent c183f80a22
commit edafe055b4
No known key found for this signature in database
4 changed files with 49 additions and 15 deletions

View File

@ -35,14 +35,16 @@ pub enum IdentityError {
MismatchedRoot(Oid),
#[error("the document root is missing")]
MissingRoot,
#[error("root commit is missing one or more delegate signatures")]
MissingRootSignatures,
#[error("commit signature for {0} is invalid: {1}")]
InvalidSignature(PublicKey, crypto::Error),
#[error("commit message for {0} is invalid")]
InvalidCommitMessage(Oid),
#[error("commit trailers for {0} are invalid: {1}")]
InvalidCommitTrailers(Oid, trailers::Error),
#[error("quorum not reached: {0} signatures for a threshold of {1}")]
QuorumNotReached(usize, usize),
#[error("threshold not reached: {0} signatures for a threshold of {1}")]
ThresholdNotReached(usize, usize),
#[error("identity document error: {0}")]
Doc(#[from] doc::DocError),
}
@ -106,6 +108,27 @@ impl Identity<Untrusted> {
let trusted = Doc::from_json(root_blob.content())?;
let revision = history.len() as u32;
{
let root_commit = repo.commit(root_oid)?;
let root_msg = root_commit
.message_raw()
.ok_or(IdentityError::InvalidCommitMessage(root_oid))?;
let root_sigs = trailers::parse_signatures(root_msg)
.map_err(|e| IdentityError::InvalidCommitTrailers(root_oid, e))?;
for (pk, sig) in &root_sigs {
if let Err(err) = pk.verify(root_blob.content(), sig) {
return Err(IdentityError::InvalidSignature(*pk, err));
}
}
// Every identity founder must have signed the root document.
for founder in &trusted.delegates {
if !root_sigs.iter().any(|(k, _)| k == &**founder) {
return Err(IdentityError::MissingRootSignatures);
}
}
}
let mut trusted = trusted.verified()?;
let mut current = root;
let mut signatures = Vec::new();
@ -136,7 +159,10 @@ impl Identity<Untrusted> {
.filter(|(key, _)| trusted.delegates.iter().any(|d| &**d == key))
.count();
if quorum < trusted.threshold {
return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
return Err(IdentityError::ThresholdNotReached(
quorum,
trusted.threshold,
));
}
trusted = untrusted;

View File

@ -167,10 +167,11 @@ impl Doc<Verified> {
pub fn init(
doc: &[u8],
remote: &RemoteId,
signatures: &[(&PublicKey, Signature)],
repo: &git2::Repository,
) -> Result<git::Oid, DocError> {
let tree = git::write_tree(*PATH, doc, repo)?;
let oid = Doc::commit(remote, &tree, "Initialize Radicle\n", &[], repo)?;
let oid = Doc::commit(remote, &tree, "Initialize Radicle\n", &[], signatures, repo)?;
Ok(oid)
}
@ -182,17 +183,11 @@ impl Doc<Verified> {
signatures: &[(&PublicKey, Signature)],
repo: &git2::Repository,
) -> Result<git::Oid, DocError> {
let mut msg = format!("{}\n\n", msg.trim());
for (key, sig) in signatures {
writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER)
.expect("in-memory writes don't fail");
}
let (_, doc) = self.encode()?;
let tree = git::write_tree(*PATH, doc.as_slice(), repo)?;
let id_ref = git::refs::storage::id(remote);
let head = repo.find_reference(&id_ref)?.peel_to_commit()?;
let oid = Doc::commit(remote, &tree, &msg, &[&head], repo)?;
let oid = Doc::commit(remote, &tree, msg, &[&head], signatures, repo)?;
Ok(oid)
}
@ -202,14 +197,21 @@ impl Doc<Verified> {
tree: &git2::Tree,
msg: &str,
parents: &[&git2::Commit],
signatures: &[(&PublicKey, Signature)],
repo: &git2::Repository,
) -> Result<git::Oid, DocError> {
let sig = repo
.signature()
.or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
let mut msg = format!("{}\n\n", msg.trim());
for (key, sig) in signatures {
writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER)
.expect("in-memory writes don't fail");
}
let id_ref = git::refs::storage::id(remote);
let oid = repo.commit(Some(&id_ref), &sig, &sig, msg, tree, parents)?;
let oid = repo.commit(Some(&id_ref), &sig, &sig, &msg, tree, parents)?;
Ok(oid.into())
}

View File

@ -65,7 +65,7 @@ pub fn init<G: Signer>(
default_branch: default_branch.clone(),
};
let doc = identity::Doc::initial(proj, delegate).verified()?;
let (project, _) = Repository::init(&doc, pk, storage)?;
let (project, _) = Repository::init(&doc, pk, storage, signer)?;
let url = git::Url::from(project.id).with_namespace(*pk);
git::configure_remote(repo, &REMOTE_NAME, &url)?;

View File

@ -213,15 +213,21 @@ impl Repository {
}
/// Create the repository's identity branch.
pub fn init(
pub fn init<G: Signer>(
doc: &Doc<Verified>,
remote: &RemoteId,
storage: &Storage,
signer: &G,
) -> Result<(Self, git::Oid), Error> {
let (doc_oid, doc) = doc.encode()?;
let id = Id::from(doc_oid);
let repo = Self::open(paths::repository(storage, &id), id)?;
let oid = Doc::init(doc.as_slice(), remote, repo.raw())?;
let oid = Doc::init(
doc.as_slice(),
remote,
&[(signer.public_key(), signer.sign(&doc))],
repo.raw(),
)?;
Ok((repo, oid))
}