From d2f423521ec76406944ad83098ec33afe20c692b Mon Sep 17 00:00:00 2001 From: Kim Altintop Date: Mon, 9 Jan 2023 13:18:33 +0100 Subject: This is it Squashed commit of all the exploration history. Development starts here. Signed-off-by: Kim Altintop --- src/cmd/id/sign.rs | 221 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 src/cmd/id/sign.rs (limited to 'src/cmd/id/sign.rs') diff --git a/src/cmd/id/sign.rs b/src/cmd/id/sign.rs new file mode 100644 index 0000000..b63ef94 --- /dev/null +++ b/src/cmd/id/sign.rs @@ -0,0 +1,221 @@ +// Copyright © 2022 Kim Altintop +// SPDX-License-Identifier: GPL-2.0-only WITH openvpn-openssl-exception + +use std::collections::BTreeMap; + +use anyhow::{ + anyhow, + bail, + ensure, + Context as _, +}; + +use super::{ + edit, + Common, +}; +use crate::{ + cfg, + cmd::{ + self, + args::Refname, + id::META_FILE_ID, + ui::{ + self, + edit_commit_message, + info, + }, + FromGit as _, + GitIdentity, + }, + git::{ + self, + if_not_found_none, + refs, + }, + metadata, +}; + +#[derive(Debug, clap::Args)] +pub struct Sign { + #[clap(flatten)] + common: Common, + /// Commit to this branch if the signature threshold is met + #[clap(short = 'b', long, value_parser, value_name = "REF")] + commit_to: Refname, + /// Check out the committed changes + /// + /// Only has an effect if the repository is non-bare. + #[clap(long, value_parser)] + checkout: bool, + /// Don't commit anything to disk + #[clap(long, value_parser)] + dry_run: bool, + /// Commit message for this edit + /// + /// Like git, $EDITOR will be invoked if not specified. + #[clap(short, long, value_parser)] + message: Option, +} + +#[derive(serde::Serialize)] +pub struct Output { + #[serde(rename = "ref")] + refname: Refname, + #[serde(with = "crate::git::serde::oid")] + commit: git2::Oid, +} + +pub fn sign(args: Sign) -> cmd::Result { + let (repo, refname) = args.common.resolve()?; + let mut tx = refs::Transaction::new(&repo)?; + let _tip = tx.lock_ref(refname.clone())?; + + let GitIdentity { + signed: + metadata::Signed { + signed: proposed, + signatures: proposed_signatures, + }, + .. + } = metadata::Identity::from_tip(&repo, &refname)?; + let prev_hash: git2::Oid = proposed + .prev + .as_ref() + .ok_or_else(|| anyhow!("cannot sign a genesis revision"))? + .into(); + let (parent, target_ref) = if refname == args.commit_to { + // Signing in-place is only legal if the proposed update already + // meets the signature threshold + let _ = proposed + .verify(&proposed_signatures, cmd::find_parent(&repo)) + .context("proposed update does not meet the signature threshold")?; + (proposed.clone(), repo.find_reference(&args.commit_to)?) + } else { + let target_ref = if_not_found_none(repo.find_reference(&args.commit_to))?; + match target_ref { + // If the target ref exists, it must yield a verified id.json whose + // blob hash equals the 'prev' hash of the proposed update + Some(tgt) => { + let parent_commit = tgt.peel_to_commit()?; + let GitIdentity { + hash: parent_hash, + signed: + metadata::Signed { + signed: parent, + signatures: parent_signatures, + }, + } = metadata::Identity::from_commit(&repo, &parent_commit).with_context(|| { + format!("failed to load {} from {}", META_FILE_ID, &args.commit_to) + })?; + let _ = parent + .verify(&parent_signatures, cmd::find_parent(&repo)) + .with_context(|| format!("target {} could not be verified", &args.commit_to))?; + ensure!( + parent_hash == prev_hash, + "parent hash (.prev) doesn't match" + ); + + (parent, tgt) + }, + + // If the target ref is unborn, the proposed's parent commit must + // yield a verified id.json, as we will create the target from + // HEAD^1 + None => { + let parent_commit = repo + .find_reference(&refname)? + .peel_to_commit()? + .parents() + .next() + .ok_or_else(|| anyhow!("cannot sign an initial commit"))?; + let GitIdentity { + hash: parent_hash, + signed: + metadata::Signed { + signed: parent, + signatures: parent_signatures, + }, + } = metadata::Identity::from_commit(&repo, &parent_commit)?; + let _ = parent + .verify(&parent_signatures, cmd::find_parent(&repo)) + .with_context(|| { + format!( + "parent commit {} of {} could not be verified", + parent_commit.id(), + refname + ) + })?; + ensure!( + parent_hash == prev_hash, + "parent hash (.prev) doesn't match" + ); + + let tgt = repo.reference( + &args.commit_to, + parent_commit.id(), + false, + &format!("branch: Created from {}^1", refname), + )?; + + (parent, tgt) + }, + } + }; + let commit_to = tx.lock_ref(args.commit_to)?; + + let canonical = proposed.canonicalise()?; + let mut signer = cfg::signer(&repo.config()?, ui::askpass)?; + let mut signatures = BTreeMap::new(); + let keyid = metadata::KeyId::from(signer.ident()); + if !parent.keys.contains_key(&keyid) && !proposed.keys.contains_key(&keyid) { + bail!("key {} is not eligible to sign the document", keyid); + } + if proposed_signatures.contains_key(&keyid) { + bail!("proposed update is already signed with key {}", keyid); + } + + let signature = signer.sign(&canonical)?; + signatures.insert(keyid, metadata::Signature::from(signature)); + signatures.extend(proposed_signatures); + + let _ = proposed + .verify(&signatures, cmd::find_parent(&repo)) + .context("proposal could not be verified after signing")?; + + let signed = metadata::Signed { + signed: metadata::Metadata::identity(proposed), + signatures, + }; + + let parent_commit = target_ref.peel_to_commit()?; + let parent_tree = parent_commit.tree()?; + let on_head = !repo.is_bare() && git2::Branch::wrap(target_ref).is_head(); + + let tree = if on_head { + edit::write_tree(&repo, &signed) + } else { + edit::write_tree_bare(&repo, &signed, Some(&parent_tree)) + }?; + let msg = args + .message + .map(Ok) + .unwrap_or_else(|| edit_commit_message(&repo, commit_to.name(), &parent_tree, &tree))?; + let commit = git::commit_signed(&mut signer, &repo, msg, &tree, &[&parent_commit])?; + commit_to.set_target(commit, "it: identity signoff"); + + tx.commit()?; + + if on_head { + repo.checkout_tree( + tree.as_object(), + Some(git2::build::CheckoutBuilder::new().safe()), + )?; + info!("Checked out tree {}", tree.id()); + } + + Ok(Output { + refname: commit_to.into(), + commit, + }) +} -- cgit v1.2.3