forked from mirrors/jj
sign: Implement a test signing backend and add a few basic tests
This commit is contained in:
parent
d7229a3f90
commit
eb1c0ab4a2
10 changed files with 272 additions and 11 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2795,6 +2795,7 @@ dependencies = [
|
|||
"async-trait",
|
||||
"config",
|
||||
"git2",
|
||||
"hex",
|
||||
"itertools 0.11.0",
|
||||
"jj-lib",
|
||||
"rand",
|
||||
|
|
|
@ -27,7 +27,8 @@ use jj_lib::git_backend::GitBackend;
|
|||
use jj_lib::repo::StoreFactories;
|
||||
use jj_lib::repo_path::RepoPath;
|
||||
use jj_lib::settings::UserSettings;
|
||||
use jj_lib::workspace::Workspace;
|
||||
use jj_lib::signing::Signer;
|
||||
use jj_lib::workspace::{Workspace, WorkspaceInitError};
|
||||
|
||||
#[derive(clap::Parser, Clone, Debug)]
|
||||
enum CustomCommands {
|
||||
|
@ -59,6 +60,8 @@ fn run_custom_command(
|
|||
command_helper.settings(),
|
||||
wc_path,
|
||||
&|settings, store_path| Ok(Box::new(JitBackend::init(settings, store_path)?)),
|
||||
Signer::from_settings(command_helper.settings())
|
||||
.map_err(WorkspaceInitError::SignInit)?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -28,12 +28,15 @@ use jj_lib::op_store::{OperationId, WorkspaceId};
|
|||
use jj_lib::repo::ReadonlyRepo;
|
||||
use jj_lib::repo_path::RepoPathBuf;
|
||||
use jj_lib::settings::UserSettings;
|
||||
use jj_lib::signing::Signer;
|
||||
use jj_lib::store::Store;
|
||||
use jj_lib::working_copy::{
|
||||
CheckoutError, CheckoutStats, LockedWorkingCopy, ResetError, SnapshotError, SnapshotOptions,
|
||||
WorkingCopy, WorkingCopyStateError,
|
||||
};
|
||||
use jj_lib::workspace::{default_working_copy_factories, WorkingCopyInitializer, Workspace};
|
||||
use jj_lib::workspace::{
|
||||
default_working_copy_factories, WorkingCopyInitializer, Workspace, WorkspaceInitError,
|
||||
};
|
||||
|
||||
#[derive(clap::Parser, Clone, Debug)]
|
||||
enum CustomCommands {
|
||||
|
@ -58,6 +61,8 @@ fn run_custom_command(
|
|||
command_helper.settings(),
|
||||
wc_path,
|
||||
&backend_initializer,
|
||||
Signer::from_settings(command_helper.settings())
|
||||
.map_err(WorkspaceInitError::SignInit)?,
|
||||
&ReadonlyRepo::default_op_store_initializer(),
|
||||
&ReadonlyRepo::default_op_heads_store_initializer(),
|
||||
&ReadonlyRepo::default_index_store_initializer(),
|
||||
|
|
|
@ -119,8 +119,6 @@ pub enum RepoInitError {
|
|||
Backend(#[from] BackendInitError),
|
||||
#[error(transparent)]
|
||||
Path(#[from] PathError),
|
||||
#[error(transparent)]
|
||||
SignInit(#[from] SignInitError),
|
||||
}
|
||||
|
||||
impl ReadonlyRepo {
|
||||
|
@ -143,10 +141,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: Signer,
|
||||
op_store_initializer: &OpStoreInitializer,
|
||||
op_heads_store_initializer: &OpHeadsStoreInitializer,
|
||||
index_store_initializer: &IndexStoreInitializer,
|
||||
|
@ -159,7 +159,6 @@ 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 signer = Signer::from_settings(user_settings)?;
|
||||
let store = Store::new(backend, signer, user_settings.use_tree_conflict_format());
|
||||
let repo_settings = user_settings.with_repo(&repo_path).unwrap();
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ use crate::repo::{
|
|||
StoreFactories, StoreLoadError, SubmoduleStoreInitializer,
|
||||
};
|
||||
use crate::settings::UserSettings;
|
||||
use crate::signing::SignInitError;
|
||||
use crate::signing::{SignInitError, Signer};
|
||||
use crate::store::Store;
|
||||
use crate::working_copy::{
|
||||
CheckoutError, CheckoutStats, LockedWorkingCopy, WorkingCopy, WorkingCopyStateError,
|
||||
|
@ -149,7 +149,8 @@ impl Workspace {
|
|||
) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
|
||||
let backend_initializer: &'static BackendInitializer =
|
||||
&|_settings, store_path| Ok(Box::new(LocalBackend::init(store_path)));
|
||||
Self::init_with_backend(user_settings, workspace_root, backend_initializer)
|
||||
let signer = Signer::from_settings(user_settings)?;
|
||||
Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer)
|
||||
}
|
||||
|
||||
/// Initializes a workspace with a new Git backend and bare Git repo in
|
||||
|
@ -160,7 +161,8 @@ impl Workspace {
|
|||
) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
|
||||
let backend_initializer: &'static BackendInitializer =
|
||||
&|settings, store_path| Ok(Box::new(GitBackend::init_internal(settings, store_path)?));
|
||||
Self::init_with_backend(user_settings, workspace_root, backend_initializer)
|
||||
let signer = Signer::from_settings(user_settings)?;
|
||||
Self::init_with_backend(user_settings, workspace_root, backend_initializer, signer)
|
||||
}
|
||||
|
||||
/// Initializes a workspace with a new Git backend and Git repo that shares
|
||||
|
@ -189,7 +191,8 @@ impl Workspace {
|
|||
Ok(Box::new(backend))
|
||||
}
|
||||
};
|
||||
Self::init_with_backend(user_settings, workspace_root, &backend_initializer)
|
||||
let signer = Signer::from_settings(user_settings)?;
|
||||
Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer)
|
||||
}
|
||||
|
||||
/// Initializes a workspace with an existing Git repo at the specified path.
|
||||
|
@ -221,7 +224,8 @@ impl Workspace {
|
|||
Ok(Box::new(backend))
|
||||
}
|
||||
};
|
||||
Self::init_with_backend(user_settings, workspace_root, &backend_initializer)
|
||||
let signer = Signer::from_settings(user_settings)?;
|
||||
Self::init_with_backend(user_settings, workspace_root, &backend_initializer, signer)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -229,6 +233,7 @@ impl Workspace {
|
|||
user_settings: &UserSettings,
|
||||
workspace_root: &Path,
|
||||
backend_initializer: &BackendInitializer,
|
||||
signer: Signer,
|
||||
op_store_initializer: &OpStoreInitializer,
|
||||
op_heads_store_initializer: &OpHeadsStoreInitializer,
|
||||
index_store_initializer: &IndexStoreInitializer,
|
||||
|
@ -244,6 +249,7 @@ impl Workspace {
|
|||
user_settings,
|
||||
&repo_dir,
|
||||
backend_initializer,
|
||||
signer,
|
||||
op_store_initializer,
|
||||
op_heads_store_initializer,
|
||||
index_store_initializer,
|
||||
|
@ -252,7 +258,6 @@ impl Workspace {
|
|||
.map_err(|repo_init_err| match repo_init_err {
|
||||
RepoInitError::Backend(err) => WorkspaceInitError::Backend(err),
|
||||
RepoInitError::Path(err) => WorkspaceInitError::Path(err),
|
||||
RepoInitError::SignInit(err) => WorkspaceInitError::SignInit(err),
|
||||
})?;
|
||||
let (working_copy, repo) = init_working_copy(
|
||||
user_settings,
|
||||
|
@ -276,11 +281,13 @@ impl Workspace {
|
|||
user_settings: &UserSettings,
|
||||
workspace_root: &Path,
|
||||
backend_initializer: &BackendInitializer,
|
||||
signer: Signer,
|
||||
) -> Result<(Self, Arc<ReadonlyRepo>), WorkspaceInitError> {
|
||||
Self::init_with_factories(
|
||||
user_settings,
|
||||
workspace_root,
|
||||
backend_initializer,
|
||||
signer,
|
||||
ReadonlyRepo::default_op_store_initializer(),
|
||||
ReadonlyRepo::default_op_heads_store_initializer(),
|
||||
ReadonlyRepo::default_index_store_initializer(),
|
||||
|
|
|
@ -35,6 +35,7 @@ use jj_lib::op_store::{BranchTarget, RefTarget, RemoteRef, RemoteRefState};
|
|||
use jj_lib::refs::BranchPushUpdate;
|
||||
use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo};
|
||||
use jj_lib::settings::{GitSettings, UserSettings};
|
||||
use jj_lib::signing::Signer;
|
||||
use jj_lib::str_util::StringPattern;
|
||||
use jj_lib::workspace::Workspace;
|
||||
use maplit::{btreemap, hashset};
|
||||
|
@ -1125,6 +1126,7 @@ impl GitRepoData {
|
|||
&git_repo_dir,
|
||||
)?))
|
||||
},
|
||||
Signer::from_settings(&settings).unwrap(),
|
||||
ReadonlyRepo::default_op_store_initializer(),
|
||||
ReadonlyRepo::default_op_heads_store_initializer(),
|
||||
ReadonlyRepo::default_index_store_initializer(),
|
||||
|
@ -1990,6 +1992,7 @@ fn test_init() {
|
|||
&git_repo_dir,
|
||||
)?))
|
||||
},
|
||||
Signer::from_settings(&settings).unwrap(),
|
||||
ReadonlyRepo::default_op_store_initializer(),
|
||||
ReadonlyRepo::default_op_heads_store_initializer(),
|
||||
ReadonlyRepo::default_index_store_initializer(),
|
||||
|
@ -2315,6 +2318,7 @@ fn set_up_push_repos(settings: &UserSettings, temp_dir: &TempDir) -> PushTestSet
|
|||
&clone_repo_dir,
|
||||
)?))
|
||||
},
|
||||
Signer::from_settings(settings).unwrap(),
|
||||
ReadonlyRepo::default_op_store_initializer(),
|
||||
ReadonlyRepo::default_op_heads_store_initializer(),
|
||||
ReadonlyRepo::default_index_store_initializer(),
|
||||
|
|
171
lib/tests/test_signing.rs
Normal file
171
lib/tests/test_signing.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use jj_lib::backend::{MillisSinceEpoch, Signature, Timestamp};
|
||||
use jj_lib::repo::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: "someone-else@example.com".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 signer = Signer::new(Some(Box::new(TestSigningBackend)), vec![]);
|
||||
let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, signer);
|
||||
|
||||
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().unwrap(), good_verification());
|
||||
|
||||
let commit2 = repo.store().get_commit(commit2.id()).unwrap();
|
||||
assert_eq!(commit2.verification().unwrap(), None);
|
||||
}
|
||||
|
||||
#[test_case(TestRepoBackend::Git ; "git backend")]
|
||||
fn keep_on_rewrite(backend: TestRepoBackend) {
|
||||
let settings = user_settings(true);
|
||||
|
||||
let signer = Signer::new(Some(Box::new(TestSigningBackend)), vec![]);
|
||||
let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, signer);
|
||||
|
||||
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();
|
||||
let rewritten = mut_repo.rewrite_commit(&settings, &commit).write().unwrap();
|
||||
|
||||
let commit = repo.store().get_commit(rewritten.id()).unwrap();
|
||||
assert_eq!(commit.verification().unwrap(), good_verification());
|
||||
}
|
||||
|
||||
#[test_case(TestRepoBackend::Git ; "git backend")]
|
||||
fn manual_drop_on_rewrite(backend: TestRepoBackend) {
|
||||
let settings = user_settings(true);
|
||||
|
||||
let signer = Signer::new(Some(Box::new(TestSigningBackend)), vec![]);
|
||||
let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, signer);
|
||||
|
||||
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();
|
||||
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().unwrap(), None);
|
||||
}
|
||||
|
||||
#[test_case(TestRepoBackend::Git ; "git backend")]
|
||||
fn forced(backend: TestRepoBackend) {
|
||||
let settings = user_settings(true);
|
||||
|
||||
let signer = Signer::new(Some(Box::new(TestSigningBackend)), vec![]);
|
||||
let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, signer);
|
||||
|
||||
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().unwrap(), good_verification());
|
||||
}
|
||||
|
||||
#[test_case(TestRepoBackend::Git ; "git backend")]
|
||||
fn configured(backend: TestRepoBackend) {
|
||||
let settings = user_settings(true);
|
||||
|
||||
let signer = Signer::new(Some(Box::new(TestSigningBackend)), vec![]);
|
||||
let test_workspace = TestWorkspace::init_with_backend_and_signer(&settings, backend, signer);
|
||||
|
||||
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().unwrap(), good_verification());
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -32,6 +32,7 @@ use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo, RepoLoader, StoreFactories};
|
|||
use jj_lib::repo_path::{RepoPath, RepoPathBuf};
|
||||
use jj_lib::rewrite::RebasedDescendant;
|
||||
use jj_lib::settings::UserSettings;
|
||||
use jj_lib::signing::Signer;
|
||||
use jj_lib::store::Store;
|
||||
use jj_lib::transaction::Transaction;
|
||||
use jj_lib::tree::Tree;
|
||||
|
@ -43,6 +44,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
|
||||
|
@ -139,6 +141,7 @@ impl TestRepo {
|
|||
&settings,
|
||||
&repo_dir,
|
||||
&move |settings, store_path| backend.init_backend(settings, store_path),
|
||||
Signer::from_settings(&settings).unwrap(),
|
||||
ReadonlyRepo::default_op_store_initializer(),
|
||||
ReadonlyRepo::default_op_heads_store_initializer(),
|
||||
ReadonlyRepo::default_index_store_initializer(),
|
||||
|
@ -175,6 +178,18 @@ impl TestWorkspace {
|
|||
}
|
||||
|
||||
pub fn init_with_backend(settings: &UserSettings, backend: TestRepoBackend) -> Self {
|
||||
Self::init_with_backend_and_signer(
|
||||
settings,
|
||||
backend,
|
||||
Signer::from_settings(settings).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn init_with_backend_and_signer(
|
||||
settings: &UserSettings,
|
||||
backend: TestRepoBackend,
|
||||
signer: Signer,
|
||||
) -> Self {
|
||||
let temp_dir = new_temp_dir();
|
||||
|
||||
let workspace_root = temp_dir.path().join("repo");
|
||||
|
@ -184,6 +199,7 @@ impl TestWorkspace {
|
|||
settings,
|
||||
&workspace_root,
|
||||
&move |settings, store_path| backend.init_backend(settings, store_path),
|
||||
signer,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
54
lib/testutils/src/test_signing_backend.rs
Normal file
54
lib/testutils/src/test_signing_backend.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use hex::ToHex;
|
||||
use jj_lib::content_hash::blake2b_hash;
|
||||
use jj_lib::signing::{SigStatus, SignError, SignResult, SigningBackend, Verification};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestSigningBackend;
|
||||
|
||||
const PREFIX: &str = "--- JJ-TEST-SIGNATURE ---\nKEY: ";
|
||||
|
||||
impl SigningBackend for TestSigningBackend {
|
||||
fn name(&self) -> &str {
|
||||
"test"
|
||||
}
|
||||
|
||||
fn can_read(&self, signature: &[u8]) -> bool {
|
||||
signature.starts_with(PREFIX.as_bytes())
|
||||
}
|
||||
|
||||
fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult<Vec<u8>> {
|
||||
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]) -> SignResult<Verification> {
|
||||
let Some(key) = signature
|
||||
.strip_prefix(PREFIX.as_bytes())
|
||||
.and_then(|s| s.splitn(2, |&b| b == b'\n').next())
|
||||
else {
|
||||
return Err(SignError::InvalidSignatureFormat);
|
||||
};
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue