Skip to content

Commit

Permalink
sign: Implement a test signing backend and add a few basic tests
Browse files Browse the repository at this point in the history
  • Loading branch information
necauqua committed Nov 27, 2023
1 parent 83a80f2 commit ac4c1cd
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 3 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

180 changes: 180 additions & 0 deletions lib/tests/test_signing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
use jj_lib::backend::{MillisSinceEpoch, Signature, Timestamp};
use jj_lib::repo::{MutableRepo, Repo};
use jj_lib::settings::UserSettings;
use jj_lib::signing::{SigStatus, SignBehavior, Signer, Verification};
use test_case::test_case;
use testutils::test_signing_backend::TestSigningBackend;
use testutils::{create_random_commit, write_random_commit, TestRepoBackend, TestWorkspace};

fn user_settings(sign_all: bool) -> UserSettings {
let config = testutils::base_config()
.add_source(config::File::from_str(
&format!(
r#"
signing.key = "impeccable"
signing.sign-all = {sign_all}
"#
),
config::FileFormat::Toml,
))
.build()
.unwrap();
UserSettings::from_config(config)
}

fn someone_else() -> Signature {
Signature {
name: "Someone Else".to_string(),
email: "[email protected]".to_string(),
timestamp: Timestamp {
timestamp: MillisSinceEpoch(0),
tz_offset: 0,
},
}
}

fn good_verification() -> Option<Verification> {
Some(Verification {
status: SigStatus::Good,
key: Some("impeccable".to_owned()),
display: None,
})
}

#[test_case(TestRepoBackend::Local ; "local backend")]
#[test_case(TestRepoBackend::Git ; "git backend")]
fn manual(backend: TestRepoBackend) {
let settings = user_settings(true);

let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, &|_| {
Signer::new(Some(Box::new(TestSigningBackend)), vec![])
});

let repo = &test_workspace.repo;

let settings = settings.clone();
let repo = repo.clone();
let mut tx = repo.start_transaction(&settings, "test");
let commit1 = create_random_commit(tx.mut_repo(), &settings)
.set_sign_behavior(SignBehavior::Own)
.write()
.unwrap();
let commit2 = create_random_commit(tx.mut_repo(), &settings)
.set_sign_behavior(SignBehavior::Own)
.set_author(someone_else())
.write()
.unwrap();
tx.commit();

let commit1 = repo.store().get_commit(commit1.id()).unwrap();
assert_eq!(commit1.verification(), good_verification());

let commit2 = repo.store().get_commit(commit2.id()).unwrap();
assert_eq!(commit2.verification(), None);
}

#[test_case(TestRepoBackend::Local ; "local backend")]
#[test_case(TestRepoBackend::Git ; "git backend")]
fn keep_on_rewrite(backend: TestRepoBackend) {
let settings = user_settings(true);

let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, &|_| {
Signer::new(Some(Box::new(TestSigningBackend)), vec![])
});

let repo = &test_workspace.repo;

let settings = settings.clone();
let repo = repo.clone();
let mut tx = repo.start_transaction(&settings, "test");
let commit = create_random_commit(tx.mut_repo(), &settings)
.set_sign_behavior(SignBehavior::Own)
.write()
.unwrap();
tx.commit();

let mut tx = repo.start_transaction(&settings, "test");
let mut_repo = tx.mut_repo() as &mut MutableRepo;
let rewritten = mut_repo.rewrite_commit(&settings, &commit).write().unwrap();

let commit = repo.store().get_commit(rewritten.id()).unwrap();
assert_eq!(commit.verification(), good_verification());
}

#[test_case(TestRepoBackend::Local ; "local backend")]
#[test_case(TestRepoBackend::Git ; "git backend")]
fn manual_drop_on_rewrite(backend: TestRepoBackend) {
let settings = user_settings(true);

let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, &|_| {
Signer::new(Some(Box::new(TestSigningBackend)), vec![])
});

let repo = &test_workspace.repo;

let settings = settings.clone();
let repo = repo.clone();
let mut tx = repo.start_transaction(&settings, "test");
let commit = create_random_commit(tx.mut_repo(), &settings)
.set_sign_behavior(SignBehavior::Own)
.write()
.unwrap();
tx.commit();

let mut tx = repo.start_transaction(&settings, "test");
let mut_repo = tx.mut_repo() as &mut MutableRepo;
let rewritten = mut_repo
.rewrite_commit(&settings, &commit)
.set_sign_behavior(SignBehavior::Drop)
.write()
.unwrap();

let commit = repo.store().get_commit(rewritten.id()).unwrap();
assert_eq!(commit.verification(), None);
}

#[test_case(TestRepoBackend::Local ; "local backend")]
#[test_case(TestRepoBackend::Git ; "git backend")]
fn forced(backend: TestRepoBackend) {
let settings = user_settings(true);

let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, &|_| {
Signer::new(Some(Box::new(TestSigningBackend)), vec![])
});

let repo = &test_workspace.repo;

let settings = settings.clone();
let repo = repo.clone();
let mut tx = repo.start_transaction(&settings, "test");
let commit = create_random_commit(tx.mut_repo(), &settings)
.set_sign_behavior(SignBehavior::Force)
.set_author(someone_else())
.write()
.unwrap();
tx.commit();

let commit = repo.store().get_commit(commit.id()).unwrap();
assert_eq!(commit.verification(), good_verification());
}

#[test_case(TestRepoBackend::Local ; "local backend")]
#[test_case(TestRepoBackend::Git ; "git backend")]
fn configured(backend: TestRepoBackend) {
let settings = user_settings(true);

let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, &|_| {
Signer::new(Some(Box::new(TestSigningBackend)), vec![])
});

let repo = &test_workspace.repo;

let settings = settings.clone();
let repo = repo.clone();
let mut tx = repo.start_transaction(&settings, "test");
let commit = write_random_commit(tx.mut_repo(), &settings);
tx.commit();

let commit = repo.store().get_commit(commit.id()).unwrap();
assert_eq!(commit.verification(), good_verification());
}
1 change: 1 addition & 0 deletions lib/testutils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ readme = { workspace = true }
async-trait = { workspace = true }
config = { workspace = true }
git2 = { workspace = true }
hex = { workspace = true }
itertools = { workspace = true }
jj-lib = { workspace = true }
rand = { workspace = true }
Expand Down
28 changes: 25 additions & 3 deletions lib/testutils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ use jj_lib::commit_builder::CommitBuilder;
use jj_lib::git_backend::GitBackend;
use jj_lib::local_backend::LocalBackend;
use jj_lib::merged_tree::MergedTree;
use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo, RepoLoader, StoreFactories};
use jj_lib::repo::{
MutableRepo, ReadonlyRepo, Repo, RepoLoader, SignerInitializer, StoreFactories,
};
use jj_lib::repo_path::RepoPath;
use jj_lib::rewrite::RebasedDescendant;
use jj_lib::settings::{SignSettings, UserSettings};
Expand All @@ -43,6 +45,7 @@ use tempfile::TempDir;
use crate::test_backend::TestBackend;

pub mod test_backend;
pub mod test_signing_backend;

pub fn hermetic_libgit2() {
// libgit2 respects init.defaultBranch (and possibly other config
Expand Down Expand Up @@ -175,16 +178,27 @@ impl TestWorkspace {
Self::init_with_backend(settings, TestRepoBackend::Test)
}

pub fn init_with_backend(settings: &UserSettings, backend: TestRepoBackend) -> Self {
pub fn init_with_backend_and_signer(
settings: &UserSettings,
backend: TestRepoBackend,
signer_init: &SignerInitializer,
) -> Self {
let temp_dir = new_temp_dir();

let workspace_root = temp_dir.path().join("repo");
fs::create_dir(&workspace_root).unwrap();

let (workspace, repo) = Workspace::init_with_backend(
let (workspace, repo) = Workspace::init_with_factories(
settings,
&workspace_root,
&move |settings, store_path| backend.init_backend(settings, store_path),
signer_init,
ReadonlyRepo::default_op_store_initializer(),
ReadonlyRepo::default_op_heads_store_initializer(),
ReadonlyRepo::default_index_store_initializer(),
ReadonlyRepo::default_submodule_store_initializer(),
jj_lib::workspace::default_working_copy_initializer(),
jj_lib::op_store::WorkspaceId::default(),
)
.unwrap();

Expand All @@ -196,6 +210,14 @@ impl TestWorkspace {
}
}

pub fn init_with_backend(settings: &UserSettings, backend: TestRepoBackend) -> Self {
Self::init_with_backend_and_signer(
settings,
backend,
ReadonlyRepo::default_signer_initializer(),
)
}

pub fn root_dir(&self) -> PathBuf {
self.temp_dir.path().join("repo").join("..")
}
Expand Down
50 changes: 50 additions & 0 deletions lib/testutils/src/test_signing_backend.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
use hex::ToHex;
use jj_lib::content_hash::blake2b_hash;
use jj_lib::signing::*;

#[derive(Debug)]
pub struct TestSigningBackend;

const PREFIX: &str = "--- JJ-TEST-SIGNATURE ---\nKEY: ";

impl SigningBackend for TestSigningBackend {
fn is_of_this_type(&self, signature: &[u8]) -> bool {
signature.starts_with(PREFIX.as_bytes())
}

fn sign(&self, data: &[u8], key: Option<&str>) -> Result<Vec<u8>, SignError> {
let key = key.unwrap_or_default();
let mut body = Vec::with_capacity(data.len() + key.len());
body.extend_from_slice(key.as_bytes());
body.extend_from_slice(data);

let hash: String = blake2b_hash(&body).encode_hex();

Ok(format!("{PREFIX}{key}\n{hash}").into_bytes())
}

fn verify(&self, data: &[u8], signature: &[u8]) -> Result<Verification, SignError> {
let Some(key) = signature
.strip_prefix(PREFIX.as_bytes())
.and_then(|s| s.splitn(2, |&b| b == b'\n').next())
else {
return Ok(Verification::invalid());
};
let key = (!key.is_empty()).then_some(std::str::from_utf8(key).unwrap().to_owned());

let sig = self.sign(data, key.as_deref())?;
if sig == signature {
Ok(Verification {
status: SigStatus::Good,
key,
display: None,
})
} else {
Ok(Verification {
status: SigStatus::Bad,
key,
display: None,
})
}
}
}

0 comments on commit ac4c1cd

Please sign in to comment.