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),
|
MismatchedRoot(Oid),
|
||||||
#[error("the document root is missing")]
|
#[error("the document root is missing")]
|
||||||
MissingRoot,
|
MissingRoot,
|
||||||
|
#[error("root commit is missing one or more delegate signatures")]
|
||||||
|
MissingRootSignatures,
|
||||||
#[error("commit signature for {0} is invalid: {1}")]
|
#[error("commit signature for {0} is invalid: {1}")]
|
||||||
InvalidSignature(PublicKey, crypto::Error),
|
InvalidSignature(PublicKey, crypto::Error),
|
||||||
#[error("commit message for {0} is invalid")]
|
#[error("commit message for {0} is invalid")]
|
||||||
InvalidCommitMessage(Oid),
|
InvalidCommitMessage(Oid),
|
||||||
#[error("commit trailers for {0} are invalid: {1}")]
|
#[error("commit trailers for {0} are invalid: {1}")]
|
||||||
InvalidCommitTrailers(Oid, trailers::Error),
|
InvalidCommitTrailers(Oid, trailers::Error),
|
||||||
#[error("quorum not reached: {0} signatures for a threshold of {1}")]
|
#[error("threshold not reached: {0} signatures for a threshold of {1}")]
|
||||||
QuorumNotReached(usize, usize),
|
ThresholdNotReached(usize, usize),
|
||||||
#[error("identity document error: {0}")]
|
#[error("identity document error: {0}")]
|
||||||
Doc(#[from] doc::DocError),
|
Doc(#[from] doc::DocError),
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +108,27 @@ impl Identity<Untrusted> {
|
||||||
let trusted = Doc::from_json(root_blob.content())?;
|
let trusted = Doc::from_json(root_blob.content())?;
|
||||||
let revision = history.len() as u32;
|
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 trusted = trusted.verified()?;
|
||||||
let mut current = root;
|
let mut current = root;
|
||||||
let mut signatures = Vec::new();
|
let mut signatures = Vec::new();
|
||||||
|
|
@ -136,7 +159,10 @@ impl Identity<Untrusted> {
|
||||||
.filter(|(key, _)| trusted.delegates.iter().any(|d| &**d == key))
|
.filter(|(key, _)| trusted.delegates.iter().any(|d| &**d == key))
|
||||||
.count();
|
.count();
|
||||||
if quorum < trusted.threshold {
|
if quorum < trusted.threshold {
|
||||||
return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold));
|
return Err(IdentityError::ThresholdNotReached(
|
||||||
|
quorum,
|
||||||
|
trusted.threshold,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
trusted = untrusted;
|
trusted = untrusted;
|
||||||
|
|
|
||||||
|
|
@ -167,10 +167,11 @@ impl Doc<Verified> {
|
||||||
pub fn init(
|
pub fn init(
|
||||||
doc: &[u8],
|
doc: &[u8],
|
||||||
remote: &RemoteId,
|
remote: &RemoteId,
|
||||||
|
signatures: &[(&PublicKey, Signature)],
|
||||||
repo: &git2::Repository,
|
repo: &git2::Repository,
|
||||||
) -> Result<git::Oid, DocError> {
|
) -> Result<git::Oid, DocError> {
|
||||||
let tree = git::write_tree(*PATH, doc, repo)?;
|
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)
|
Ok(oid)
|
||||||
}
|
}
|
||||||
|
|
@ -182,17 +183,11 @@ impl Doc<Verified> {
|
||||||
signatures: &[(&PublicKey, Signature)],
|
signatures: &[(&PublicKey, Signature)],
|
||||||
repo: &git2::Repository,
|
repo: &git2::Repository,
|
||||||
) -> Result<git::Oid, DocError> {
|
) -> 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 (_, doc) = self.encode()?;
|
||||||
let tree = git::write_tree(*PATH, doc.as_slice(), repo)?;
|
let tree = git::write_tree(*PATH, doc.as_slice(), repo)?;
|
||||||
let id_ref = git::refs::storage::id(remote);
|
let id_ref = git::refs::storage::id(remote);
|
||||||
let head = repo.find_reference(&id_ref)?.peel_to_commit()?;
|
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)
|
Ok(oid)
|
||||||
}
|
}
|
||||||
|
|
@ -202,14 +197,21 @@ impl Doc<Verified> {
|
||||||
tree: &git2::Tree,
|
tree: &git2::Tree,
|
||||||
msg: &str,
|
msg: &str,
|
||||||
parents: &[&git2::Commit],
|
parents: &[&git2::Commit],
|
||||||
|
signatures: &[(&PublicKey, Signature)],
|
||||||
repo: &git2::Repository,
|
repo: &git2::Repository,
|
||||||
) -> Result<git::Oid, DocError> {
|
) -> Result<git::Oid, DocError> {
|
||||||
let sig = repo
|
let sig = repo
|
||||||
.signature()
|
.signature()
|
||||||
.or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
|
.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 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())
|
Ok(oid.into())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ pub fn init<G: Signer>(
|
||||||
default_branch: default_branch.clone(),
|
default_branch: default_branch.clone(),
|
||||||
};
|
};
|
||||||
let doc = identity::Doc::initial(proj, delegate).verified()?;
|
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);
|
let url = git::Url::from(project.id).with_namespace(*pk);
|
||||||
|
|
||||||
git::configure_remote(repo, &REMOTE_NAME, &url)?;
|
git::configure_remote(repo, &REMOTE_NAME, &url)?;
|
||||||
|
|
|
||||||
|
|
@ -213,15 +213,21 @@ impl Repository {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create the repository's identity branch.
|
/// Create the repository's identity branch.
|
||||||
pub fn init(
|
pub fn init<G: Signer>(
|
||||||
doc: &Doc<Verified>,
|
doc: &Doc<Verified>,
|
||||||
remote: &RemoteId,
|
remote: &RemoteId,
|
||||||
storage: &Storage,
|
storage: &Storage,
|
||||||
|
signer: &G,
|
||||||
) -> Result<(Self, git::Oid), Error> {
|
) -> Result<(Self, git::Oid), Error> {
|
||||||
let (doc_oid, doc) = doc.encode()?;
|
let (doc_oid, doc) = doc.encode()?;
|
||||||
let id = Id::from(doc_oid);
|
let id = Id::from(doc_oid);
|
||||||
let repo = Self::open(paths::repository(storage, &id), id)?;
|
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))
|
Ok((repo, oid))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue