From edafe055b467bda7dbbb56c082e12c93fe7975f6 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Mon, 12 Dec 2022 13:41:59 +0100 Subject: [PATCH] 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 --- radicle/src/identity.rs | 32 +++++++++++++++++++++++++++++--- radicle/src/identity/doc.rs | 20 +++++++++++--------- radicle/src/rad.rs | 2 +- radicle/src/storage/git.rs | 10 ++++++++-- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/radicle/src/identity.rs b/radicle/src/identity.rs index 89f949a5..63027d09 100644 --- a/radicle/src/identity.rs +++ b/radicle/src/identity.rs @@ -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 { 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 { .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; diff --git a/radicle/src/identity/doc.rs b/radicle/src/identity/doc.rs index 706ea375..ff8d6d57 100644 --- a/radicle/src/identity/doc.rs +++ b/radicle/src/identity/doc.rs @@ -167,10 +167,11 @@ impl Doc { pub fn init( doc: &[u8], remote: &RemoteId, + signatures: &[(&PublicKey, Signature)], repo: &git2::Repository, ) -> Result { 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 { signatures: &[(&PublicKey, Signature)], repo: &git2::Repository, ) -> Result { - 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 { tree: &git2::Tree, msg: &str, parents: &[&git2::Commit], + signatures: &[(&PublicKey, Signature)], repo: &git2::Repository, ) -> Result { 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()) } diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs index b4b466d4..58453202 100644 --- a/radicle/src/rad.rs +++ b/radicle/src/rad.rs @@ -65,7 +65,7 @@ pub fn init( 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)?; diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index 1988f124..94360fcb 100644 --- a/radicle/src/storage/git.rs +++ b/radicle/src/storage/git.rs @@ -213,15 +213,21 @@ impl Repository { } /// Create the repository's identity branch. - pub fn init( + pub fn init( doc: &Doc, 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)) }