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:
parent
c183f80a22
commit
edafe055b4
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue