Skip to content

Commit

Permalink
sign: Define signing backend API and integrate it
Browse files Browse the repository at this point in the history
Finished everything except actual signing backend implementation(s) and
the UI.
  • Loading branch information
necauqua committed Nov 27, 2023
1 parent 55f7527 commit 83a80f2
Show file tree
Hide file tree
Showing 16 changed files with 405 additions and 19 deletions.
1 change: 1 addition & 0 deletions cli/examples/custom-working-copy/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ fn run_custom_command(
command_helper.settings(),
wc_path,
&backend_initializer,
&ReadonlyRepo::default_signer_initializer(),
&ReadonlyRepo::default_op_store_initializer(),
&ReadonlyRepo::default_op_heads_store_initializer(),
&ReadonlyRepo::default_index_store_initializer(),
Expand Down
25 changes: 25 additions & 0 deletions cli/src/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,31 @@
"default": "1MiB"
}
}
},
"signing": {
"type": "object",
"description": "Settings for verifying and creating cryptographic commit signatures",
"properties": {
"backend": {
"type": "string",
"description": "Which backend to use to create commit signatures"
},
"key": {
"type": "string",
"description": "The key parameter to pass to the signing backend. Overridden by `jj sign` parameter or by the global `--sign-with` option"
},
"sign-all": {
"type": "boolean",
"description": "Whether to sign all commits by default. Overridden by global `--no-sign` option",
"default": false
},
"backends": {
"type": "object",
"description": "Tables of options to pass to specific signing backends",
"properties": {},
"additionalProperties": true
}
}
}
}
}
3 changes: 2 additions & 1 deletion lib/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use thiserror::Error;
use crate::content_hash::ContentHash;
use crate::merge::Merge;
use crate::repo_path::{RepoPath, RepoPathComponent, RepoPathComponentBuf};
use crate::signing::SignError;

pub trait ObjectId {
fn new(value: Vec<u8>) -> Self;
Expand Down Expand Up @@ -147,7 +148,7 @@ content_hash! {
}
}

pub type SigningFn = Box<dyn FnMut(&[u8]) -> BackendResult<Vec<u8>>>;
pub type SigningFn = Box<dyn FnMut(&[u8]) -> Result<Vec<u8>, SignError>>;

/// Identifies a single legacy tree, which may have path-level conflicts, or a
/// merge of multiple trees, where the individual trees do not have conflicts.
Expand Down
14 changes: 14 additions & 0 deletions lib/src/commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use std::sync::Arc;
use crate::backend;
use crate::backend::{BackendError, ChangeId, CommitId, MergedTreeId, Signature};
use crate::merged_tree::MergedTree;
use crate::signing::Verification;
use crate::store::Store;

#[derive(Clone)]
Expand Down Expand Up @@ -146,6 +147,19 @@ impl Commit {
}
false
}

/// A quick way to just check if a signature is present.
pub fn is_signed(&self) -> bool {
self.data.secure_sig.is_some()
}

/// A slow (but cached) way to get the full verification.
pub fn verification(&self) -> Option<Verification> {
self.data
.secure_sig
.as_ref()
.map(|sig| self.store.signer().verify(&self.id, &sig.data, &sig.sig))
}
}

/// Wrapper to sort `Commit` by committer timestamp.
Expand Down
25 changes: 23 additions & 2 deletions lib/src/commit_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ use std::sync::Arc;
use crate::backend::{self, BackendResult, ChangeId, CommitId, MergedTreeId, Signature};
use crate::commit::Commit;
use crate::repo::{MutableRepo, Repo};
use crate::settings::{JJRng, UserSettings};
use crate::settings::{JJRng, SignSettings, UserSettings};
use crate::signing::SignBehavior;

#[must_use]
pub struct CommitBuilder<'repo> {
mut_repo: &'repo mut MutableRepo,
rng: Arc<JJRng>,
commit: backend::Commit,
rewrite_source: Option<Commit>,
sign_settings: SignSettings,
}

impl CommitBuilder<'_> {
Expand Down Expand Up @@ -55,6 +57,7 @@ impl CommitBuilder<'_> {
rng,
commit,
rewrite_source: None,
sign_settings: settings.sign_settings(),
}
}

Expand All @@ -66,6 +69,7 @@ impl CommitBuilder<'_> {
let mut commit = predecessor.store_commit().clone();
commit.predecessors = vec![predecessor.id().clone()];
commit.committer = settings.signature();
commit.secure_sig = None;
// If the user had not configured a name and email before but now they have,
// update the author fields with the new information.
if commit.author.name.is_empty()
Expand All @@ -83,6 +87,7 @@ impl CommitBuilder<'_> {
commit,
rng: settings.get_rng(),
rewrite_source: Some(predecessor.clone()),
sign_settings: settings.sign_settings(),
}
}

Expand Down Expand Up @@ -157,14 +162,30 @@ impl CommitBuilder<'_> {
self
}

pub fn sign_settings(&self) -> &SignSettings {
&self.sign_settings
}

pub fn set_sign_behavior(mut self, sign_behavior: SignBehavior) -> Self {
self.sign_settings.behavior = sign_behavior;
self
}

pub fn set_sign_key(mut self, sign_key: Option<String>) -> Self {
self.sign_settings.key = sign_key;
self
}

pub fn write(self) -> BackendResult<Commit> {
let mut rewrite_source_id = None;
if let Some(rewrite_source) = self.rewrite_source {
if *rewrite_source.change_id() == self.commit.change_id {
rewrite_source_id.replace(rewrite_source.id().clone());
}
}
let commit = self.mut_repo.write_commit(self.commit)?;
let commit = self
.mut_repo
.write_commit(self.commit, self.sign_settings)?;
if let Some(rewrite_source_id) = rewrite_source_id {
self.mut_repo
.record_rewritten_commit(rewrite_source_id, commit.id().clone())
Expand Down
5 changes: 4 additions & 1 deletion lib/src/git_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,10 @@ impl Backend for GitBackend {
let mut data = Vec::with_capacity(512);
commit.write_to(&mut data).unwrap();

let sig = sign(&data)?;
let sig = sign(&data).map_err(|err| BackendError::WriteObject {
object_type: "commit",
source: Box::new(err),
})?;
commit
.extra_headers
.push(("gpgsig".into(), sig.clone().into()));
Expand Down
1 change: 1 addition & 0 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub mod revset;
pub mod revset_graph;
pub mod rewrite;
pub mod settings;
pub mod signing;
pub mod simple_op_heads_store;
pub mod simple_op_store;
pub mod stacked_table;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/local_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ impl Backend for LocalBackend {
let mut proto = commit_to_proto(&commit);
if let Some(mut sign) = sign_with {
let data = proto.encode_to_vec();
let sig = sign(&data)?;
let sig = sign(&data).map_err(to_other_err)?;
proto.secure_sig = Some(sig.clone());
commit.secure_sig = Some(SecureSig { data, sig });
}
Expand Down
55 changes: 51 additions & 4 deletions lib/src/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ use crate::refs::{
};
use crate::revset::{self, ChangeIdIndex, Revset, RevsetExpression};
use crate::rewrite::{DescendantRebaser, RebaseOptions};
use crate::settings::{RepoSettings, UserSettings};
use crate::settings::{RepoSettings, SignSettings, UserSettings};
use crate::signing::{Signer, SigningBackend};
use crate::simple_op_heads_store::SimpleOpHeadsStore;
use crate::simple_op_store::SimpleOpStore;
use crate::store::Store;
Expand Down Expand Up @@ -121,6 +122,18 @@ pub enum RepoInitError {
}

impl ReadonlyRepo {
pub fn default_signer_initializer() -> &'static SignerInitializer {
&|_settings| {
// let config = settings.signing_backend();
let /* mut */ main_backend = None;
let /* mut */ other_backends = Vec::new();

// todo add signing backend impls here

Signer::new(main_backend, other_backends)
}
}

pub fn default_op_store_initializer() -> &'static OpStoreInitializer {
&|_settings, store_path| Box::new(SimpleOpStore::init(store_path))
}
Expand All @@ -140,10 +153,12 @@ impl ReadonlyRepo {
&|_settings, store_path| Box::new(DefaultSubmoduleStore::init(store_path))
}

#[allow(clippy::too_many_arguments)]
pub fn init(
user_settings: &UserSettings,
repo_path: &Path,
backend_initializer: &BackendInitializer,
signer_initializer: &SignerInitializer,
op_store_initializer: &OpStoreInitializer,
op_heads_store_initializer: &OpHeadsStoreInitializer,
index_store_initializer: &IndexStoreInitializer,
Expand All @@ -156,7 +171,8 @@ impl ReadonlyRepo {
let backend = backend_initializer(user_settings, &store_path)?;
let backend_path = store_path.join("type");
fs::write(&backend_path, backend.name()).context(&backend_path)?;
let store = Store::new(backend, user_settings.use_tree_conflict_format());
let signer = signer_initializer(user_settings);
let store = Store::new(backend, signer, user_settings.use_tree_conflict_format());
let repo_settings = user_settings.with_repo(&repo_path).unwrap();

let op_store_path = repo_path.join("op_store");
Expand Down Expand Up @@ -346,16 +362,19 @@ pub type OpStoreInitializer = dyn Fn(&UserSettings, &Path) -> Box<dyn OpStore>;
pub type OpHeadsStoreInitializer = dyn Fn(&UserSettings, &Path) -> Box<dyn OpHeadsStore>;
pub type IndexStoreInitializer = dyn Fn(&UserSettings, &Path) -> Box<dyn IndexStore>;
pub type SubmoduleStoreInitializer = dyn Fn(&UserSettings, &Path) -> Box<dyn SubmoduleStore>;
pub type SignerInitializer = dyn Fn(&UserSettings) -> Signer;

type BackendFactory =
Box<dyn Fn(&UserSettings, &Path) -> Result<Box<dyn Backend>, BackendLoadError>>;
type OpStoreFactory = Box<dyn Fn(&UserSettings, &Path) -> Box<dyn OpStore>>;
type OpHeadsStoreFactory = Box<dyn Fn(&UserSettings, &Path) -> Box<dyn OpHeadsStore>>;
type IndexStoreFactory = Box<dyn Fn(&UserSettings, &Path) -> Box<dyn IndexStore>>;
type SubmoduleStoreFactory = Box<dyn Fn(&UserSettings, &Path) -> Box<dyn SubmoduleStore>>;
type SigningBackendFactory = Box<dyn Fn(&UserSettings) -> Box<dyn SigningBackend>>;

pub struct StoreFactories {
backend_factories: HashMap<String, BackendFactory>,
signing_backend_factories: HashMap<String, SigningBackendFactory>,
op_store_factories: HashMap<String, OpStoreFactory>,
op_heads_store_factories: HashMap<String, OpHeadsStoreFactory>,
index_store_factories: HashMap<String, IndexStoreFactory>,
Expand Down Expand Up @@ -424,6 +443,7 @@ impl StoreFactories {
pub fn empty() -> Self {
StoreFactories {
backend_factories: HashMap::new(),
signing_backend_factories: HashMap::new(),
op_store_factories: HashMap::new(),
op_heads_store_factories: HashMap::new(),
index_store_factories: HashMap::new(),
Expand All @@ -435,6 +455,11 @@ impl StoreFactories {
self.backend_factories.insert(name.to_string(), factory);
}

pub fn add_signing_backend(&mut self, name: &str, factory: SigningBackendFactory) {
self.signing_backend_factories
.insert(name.to_string(), factory);
}

pub fn load_backend(
&self,
settings: &UserSettings,
Expand Down Expand Up @@ -462,6 +487,23 @@ impl StoreFactories {
Ok(backend_factory(settings, store_path)?)
}

pub fn load_signer(&self, settings: &UserSettings) -> Result<Signer, StoreLoadError> {
let config = settings.signing_backend();

let mut main_backend = None;
let mut other_backends = Vec::new();

for (name, factory) in &self.signing_backend_factories {
if config.as_deref() == Some(name) {
main_backend = Some(factory(settings));
continue;
}
other_backends.push(factory(settings));
}

Ok(Signer::new(main_backend, other_backends))
}

pub fn add_op_store(&mut self, name: &str, factory: OpStoreFactory) {
self.op_store_factories.insert(name.to_string(), factory);
}
Expand Down Expand Up @@ -608,6 +650,7 @@ impl RepoLoader {
) -> Result<Self, StoreLoadError> {
let store = Store::new(
store_factories.load_backend(user_settings, &repo_path.join("store"))?,
store_factories.load_signer(user_settings)?,
user_settings.use_tree_conflict_format(),
);
let repo_settings = user_settings.with_repo(repo_path).unwrap();
Expand Down Expand Up @@ -792,8 +835,12 @@ impl MutableRepo {
CommitBuilder::for_rewrite_from(self, settings, predecessor)
}

pub fn write_commit(&mut self, commit: backend::Commit) -> BackendResult<Commit> {
let commit = self.store().write_commit(commit)?;
pub fn write_commit(
&mut self,
commit: backend::Commit,
sign_settings: SignSettings,
) -> BackendResult<Commit> {
let commit = self.store().write_commit(commit, sign_settings)?;
self.add_head(&commit);
Ok(commit)
}
Expand Down
46 changes: 45 additions & 1 deletion lib/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ use chrono::DateTime;
use rand::prelude::*;
use rand_chacha::ChaCha20Rng;

use crate::backend::{ChangeId, ObjectId, Signature, Timestamp};
use crate::backend::{ChangeId, Commit, ObjectId, Signature, Timestamp};
use crate::fmt_util::binary_prefix;
use crate::fsmonitor::FsmonitorKind;
use crate::signing::SignBehavior;

#[derive(Debug, Clone)]
pub struct UserSettings {
Expand Down Expand Up @@ -63,6 +64,39 @@ impl Default for GitSettings {
}
}

#[derive(Debug, Clone, Default)]
pub struct SignSettings {
pub behavior: SignBehavior,
pub user_email: String,
pub key: Option<String>,
}

impl SignSettings {
pub fn from_settings(settings: &UserSettings) -> Self {
Self {
behavior: SignBehavior::from_config(
settings
.config()
.get_bool("signing.sign-all")
.unwrap_or(false),
),
user_email: settings.user_email(),
key: settings.config().get_string("signing.key").ok(),
}
}

pub fn should_sign(&self, commit: &Commit) -> bool {
match self.behavior {
SignBehavior::Drop => false,
SignBehavior::Keep => {
commit.secure_sig.is_some() && commit.author.email == self.user_email
}
SignBehavior::Own => commit.author.email == self.user_email,
SignBehavior::Force => true,
}
}
}

fn get_timestamp_config(config: &config::Config, key: &str) -> Option<Timestamp> {
match config.get_string(key) {
Ok(timestamp_str) => match DateTime::parse_from_rfc3339(&timestamp_str) {
Expand Down Expand Up @@ -215,6 +249,16 @@ impl UserSettings {
e @ Err(_) => e,
}
}

// separate from sign_settings as those two are needed in pretty different
// places
pub fn signing_backend(&self) -> Option<String> {
self.config.get_string("signing.backend").ok()
}

pub fn sign_settings(&self) -> SignSettings {
SignSettings::from_settings(self)
}
}

/// This Rng uses interior mutability to allow generating random values using an
Expand Down
Loading

0 comments on commit 83a80f2

Please sign in to comment.