diff --git a/cli/examples/custom-working-copy/main.rs b/cli/examples/custom-working-copy/main.rs index 68c2f193786..2c53d85490d 100644 --- a/cli/examples/custom-working-copy/main.rs +++ b/cli/examples/custom-working-copy/main.rs @@ -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(), diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index 14e71edf97b..ea0885ccca0 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -355,6 +355,22 @@ "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" + }, + "backends": { + "type": "object", + "description": "Tables of options to pass to specific signing backends", + "properties": {}, + "additionalProperties": true + } + } } } } \ No newline at end of file diff --git a/lib/src/backend.rs b/lib/src/backend.rs index 8cd563867a2..7632f0a3dbd 100644 --- a/lib/src/backend.rs +++ b/lib/src/backend.rs @@ -27,6 +27,7 @@ use thiserror::Error; use crate::content_hash::ContentHash; use crate::merge::Merge; use crate::repo_path::{RepoPath, RepoPathComponent}; +use crate::signing::SignError; pub trait ObjectId { fn new(value: Vec) -> Self; @@ -147,7 +148,7 @@ content_hash! { } } -pub type SigningFn = Box BackendResult>>; +pub type SigningFn = Box Result, 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. diff --git a/lib/src/commit.rs b/lib/src/commit.rs index b5c250e8588..0010ed0ea6a 100644 --- a/lib/src/commit.rs +++ b/lib/src/commit.rs @@ -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)] @@ -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 { + 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. diff --git a/lib/src/commit_builder.rs b/lib/src/commit_builder.rs index 6bb10270521..0ac2a839d2f 100644 --- a/lib/src/commit_builder.rs +++ b/lib/src/commit_builder.rs @@ -20,6 +20,7 @@ use crate::backend::{self, BackendResult, ChangeId, CommitId, MergedTreeId, Sign use crate::commit::Commit; use crate::repo::{MutableRepo, Repo}; use crate::settings::{JJRng, UserSettings}; +use crate::signing::{SignBehavior, SignConfig}; #[must_use] pub struct CommitBuilder<'repo> { @@ -27,6 +28,8 @@ pub struct CommitBuilder<'repo> { rng: Arc, commit: backend::Commit, rewrite_source: Option, + user_email: String, + sign_behavior: SignBehavior, } impl CommitBuilder<'_> { @@ -55,6 +58,8 @@ impl CommitBuilder<'_> { rng, commit, rewrite_source: None, + user_email: settings.user_email(), + sign_behavior: SignBehavior::Drop, } } @@ -83,6 +88,8 @@ impl CommitBuilder<'_> { commit, rng: settings.get_rng(), rewrite_source: Some(predecessor.clone()), + user_email: settings.user_email(), + sign_behavior: SignBehavior::Drop, } } @@ -157,6 +164,15 @@ impl CommitBuilder<'_> { self } + pub fn sign_behavior(&self) -> SignBehavior { + self.sign_behavior + } + + pub fn set_sign_behavior(mut self, sign_behavior: SignBehavior) -> Self { + self.sign_behavior = sign_behavior; + self + } + pub fn write(self) -> BackendResult { let mut rewrite_source_id = None; if let Some(rewrite_source) = self.rewrite_source { @@ -164,7 +180,11 @@ impl CommitBuilder<'_> { rewrite_source_id.replace(rewrite_source.id().clone()); } } - let commit = self.mut_repo.write_commit(self.commit)?; + let sign_config = SignConfig { + behavior: self.sign_behavior, + user_email: &self.user_email, + }; + let commit = self.mut_repo.write_commit(self.commit, sign_config)?; if let Some(rewrite_source_id) = rewrite_source_id { self.mut_repo .record_rewritten_commit(rewrite_source_id, commit.id().clone()) diff --git a/lib/src/git_backend.rs b/lib/src/git_backend.rs index a992f136fd1..bb77e0ab424 100644 --- a/lib/src/git_backend.rs +++ b/lib/src/git_backend.rs @@ -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())); diff --git a/lib/src/lib.rs b/lib/src/lib.rs index f0ac9314a4c..cf649466e5b 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -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; diff --git a/lib/src/local_backend.rs b/lib/src/local_backend.rs index 8f01e84c9c8..287a6071025 100644 --- a/lib/src/local_backend.rs +++ b/lib/src/local_backend.rs @@ -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 }); } diff --git a/lib/src/repo.rs b/lib/src/repo.rs index f3391f4e75c..632461c2f44 100644 --- a/lib/src/repo.rs +++ b/lib/src/repo.rs @@ -52,6 +52,7 @@ use crate::refs::{ use crate::revset::{self, ChangeIdIndex, Revset, RevsetExpression}; use crate::rewrite::{DescendantRebaser, RebaseOptions}; use crate::settings::{RepoSettings, UserSettings}; +use crate::signing::{SignConfig, Signer, SigningBackend}; use crate::simple_op_heads_store::SimpleOpHeadsStore; use crate::simple_op_store::SimpleOpStore; use crate::store::Store; @@ -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)) } @@ -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, @@ -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"); @@ -346,6 +362,7 @@ pub type OpStoreInitializer = dyn Fn(&UserSettings, &Path) -> Box; pub type OpHeadsStoreInitializer = dyn Fn(&UserSettings, &Path) -> Box; pub type IndexStoreInitializer = dyn Fn(&UserSettings, &Path) -> Box; pub type SubmoduleStoreInitializer = dyn Fn(&UserSettings, &Path) -> Box; +pub type SignerInitializer = dyn Fn(&UserSettings) -> Signer; type BackendFactory = Box Result, BackendLoadError>>; @@ -353,9 +370,11 @@ type OpStoreFactory = Box Box>; type OpHeadsStoreFactory = Box Box>; type IndexStoreFactory = Box Box>; type SubmoduleStoreFactory = Box Box>; +type SigningBackendFactory = Box Box>; pub struct StoreFactories { backend_factories: HashMap, + signing_backend_factories: HashMap, op_store_factories: HashMap, op_heads_store_factories: HashMap, index_store_factories: HashMap, @@ -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(), @@ -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, @@ -462,6 +487,23 @@ impl StoreFactories { Ok(backend_factory(settings, store_path)?) } + pub fn load_signer(&self, settings: &UserSettings) -> Result { + 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); } @@ -608,6 +650,7 @@ impl RepoLoader { ) -> Result { 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(); @@ -792,8 +835,12 @@ impl MutableRepo { CommitBuilder::for_rewrite_from(self, settings, predecessor) } - pub fn write_commit(&mut self, commit: backend::Commit) -> BackendResult { - let commit = self.store().write_commit(commit)?; + pub fn write_commit( + &mut self, + commit: backend::Commit, + sign_config: SignConfig, + ) -> BackendResult { + let commit = self.store().write_commit(commit, sign_config)?; self.add_head(&commit); Ok(commit) } diff --git a/lib/src/settings.rs b/lib/src/settings.rs index a56bc83f9ae..a5f3f9a86b7 100644 --- a/lib/src/settings.rs +++ b/lib/src/settings.rs @@ -215,6 +215,10 @@ impl UserSettings { e @ Err(_) => e, } } + + pub fn signing_backend(&self) -> Option { + self.config.get_string("signing.backend").ok() + } } /// This Rng uses interior mutability to allow generating random values using an diff --git a/lib/src/signing.rs b/lib/src/signing.rs new file mode 100644 index 00000000000..cfb9f5373a1 --- /dev/null +++ b/lib/src/signing.rs @@ -0,0 +1,199 @@ +#![allow(missing_docs)] + +use std::collections::HashMap; +use std::fmt::Debug; +use std::sync::RwLock; + +use thiserror::Error; + +use crate::backend::{Commit, CommitId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SigStatus { + /// Valid signature that matches the data. + Good, + /// Valid signature that could not be verified (e.g. due to an unknown key). + Unknown, + /// Valid signature that does not match the signed data. + Bad, + /// Invalid signature. + Invalid, +} + +#[derive(Debug, Clone)] +pub struct Verification { + /// The status of the signature. + pub status: SigStatus, + /// The key representation, if available. For GPG, this will be the key + /// fingerprint. + pub key: Option, + /// A display string, if available. For GPG, this will be formatted primary + /// user ID. + pub display: Option, +} + +impl Verification { + pub fn unknown() -> Self { + Self { + status: SigStatus::Unknown, + key: None, + display: None, + } + } +} + +/// The backend for signing and verifying cryptographic signatures. +/// +/// This allows using different signers, such as GPG or SSH, or different +/// versions of them. +/// +/// The instance of the backend carries the principal that is used to make +/// signatures, e.g. for GPG it's a key id (and a GPG-managed key itself). +pub trait SigningBackend: Debug + Send + Sync { + /// Update the principal that the backend should use. + /// + /// The parameter is what `jj sign` receives as key argument, None if it + /// doesn't receive any, usually implying some sort of default. + fn update_key(&self, key: Option); + + /// Check if the signature was created by this backend implementation. + /// + /// Should check the signature format, usually just looks at the prefix. + fn is_of_this_type(&self, signature: &[u8]) -> bool; + + /// Create a signature for arbitrary data. + fn sign(&self, data: &[u8]) -> Result, SignError>; + + /// Verify a signature. Should be reflexive with `sign`: + /// ```rust,ignore + /// verify(data, sign(data)?)?.status == SigStatus::Good + /// ``` + fn verify(&self, data: &[u8], signature: &[u8]) -> Result; +} + +#[derive(Debug, Error)] +pub enum SignError { + #[error("Would override a foreign commit signature")] + CannotRewrite, + #[error("Signing error: {0}")] + Other(#[from] Box), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum SignBehavior { + /// Drop existing signatures. + /// This is what jj did before signing support or does now when a signing + /// backend is not configured. + #[default] + Drop, + /// Only sign commits that were authored by self and already signed, + /// "preserving" the signature across rewrites. + /// This is what jj does when a signing backend is configured. + Keep, + /// Sign/re-sign commits that were authored by self. Foreign signatures are + /// overwritten on authored commits and dropped on others. + /// This is what jj does when configured to always sign. + Own, + /// Always sign commits, regardless of who authored or signed them before. + /// This is what jj does on `jj sign -f` or `jj --force-sign `. + Force, +} + +#[derive(Debug, Clone, Default)] +pub struct SignConfig<'a> { + pub behavior: SignBehavior, + pub user_email: &'a str, +} + +impl SignConfig<'_> { + 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, + } + } +} + +/// Wraps low-level signing backends and adds caching, similar to `Store`. +#[derive(Debug, Default)] +pub struct Signer { + main_backend: Option>, + other_backends: Vec>, + cache: RwLock>, +} + +impl Signer { + pub fn new( + main_backend: Option>, + other_backends: Vec>, + ) -> Self { + Self { + main_backend, + other_backends, + cache: Default::default(), + } + } + + pub fn can_sign(&self) -> bool { + self.main_backend.is_some() + } + + /// This is just a pass-through to the main backend that unconditionally + /// creates a signature. + pub fn sign(&self, data: &[u8]) -> Result, SignError> { + self.main_backend + .as_ref() + .expect("tried to sign without checking can_sign first") + .sign(data) + } + + /// Looks for backend that can verify the signature and returns the result + /// of its verification. + pub fn verify(&self, commit_id: &CommitId, data: &[u8], signature: &[u8]) -> Verification { + let cached = self + .cache + .read() + .unwrap() + .get(commit_id) + .cloned(); + if let Some(check) = cached { + return check; + } + if let Some(backend) = self + .main_backend + .iter() + .chain(self.other_backends.iter()) + .find(|b| b.is_of_this_type(signature)) + { + let Ok(check) = backend.verify(data, signature) else { + return Verification::unknown(); + }; + + // a key might get imported before next call?. + // realistically this is unlikely, but technically + // it's correct to not cache unknowns here + if check.status != SigStatus::Unknown { + self.cache + .write() + .unwrap() + .insert(commit_id.clone(), check.clone()); + } + check + } else { + // now here it's correct to cache unknowns, as we don't + // have a backend that knows how to handle this signature + // + // not sure about how much of an optimization this is + self.cache + .write() + .unwrap() + .insert(commit_id.clone(), Verification::unknown()); + Verification::unknown() + } + } +} diff --git a/lib/src/store.rs b/lib/src/store.rs index 69d1f73dd60..cc44e97215e 100644 --- a/lib/src/store.rs +++ b/lib/src/store.rs @@ -22,14 +22,15 @@ use std::sync::{Arc, RwLock}; use pollster::FutureExt; -use crate::backend; use crate::backend::{ - Backend, BackendResult, ChangeId, CommitId, ConflictId, FileId, MergedTreeId, SymlinkId, TreeId, + self, Backend, BackendResult, ChangeId, CommitId, ConflictId, FileId, MergedTreeId, SigningFn, + SymlinkId, TreeId, }; use crate::commit::Commit; use crate::merge::{Merge, MergedTreeValue}; use crate::merged_tree::MergedTree; use crate::repo_path::RepoPath; +use crate::signing::{SignConfig, Signer}; use crate::tree::Tree; use crate::tree_builder::TreeBuilder; @@ -37,6 +38,7 @@ use crate::tree_builder::TreeBuilder; /// adds caching. pub struct Store { backend: Box, + signer: Signer, commit_cache: RwLock>>, tree_cache: RwLock>>, use_tree_conflict_format: bool, @@ -51,9 +53,14 @@ impl Debug for Store { } impl Store { - pub fn new(backend: Box, use_tree_conflict_format: bool) -> Arc { + pub fn new( + backend: Box, + signer: Signer, + use_tree_conflict_format: bool, + ) -> Arc { Arc::new(Store { backend, + signer, commit_cache: Default::default(), tree_cache: Default::default(), use_tree_conflict_format, @@ -64,6 +71,10 @@ impl Store { self.backend.as_any() } + pub fn signer(&self) -> &Signer { + &self.signer + } + /// Whether new tree should be written using the tree-level format. pub fn use_tree_conflict_format(&self) -> bool { self.use_tree_conflict_format @@ -124,9 +135,19 @@ impl Store { Ok(data) } - pub fn write_commit(self: &Arc, commit: backend::Commit) -> BackendResult { + pub fn write_commit( + self: &Arc, + commit: backend::Commit, + sign_config: SignConfig, + ) -> BackendResult { assert!(!commit.parents.is_empty()); - let (commit_id, commit) = self.backend.write_commit(commit, None)?; + + let signing_fn = (self.signer.can_sign() && sign_config.should_sign(&commit)).then(|| { + let store = self.clone(); + Box::new(move |data: &_| store.signer.sign(data)) as SigningFn + }); + + let (commit_id, commit) = self.backend.write_commit(commit, signing_fn)?; let data = Arc::new(commit); { let mut write_locked_cache = self.commit_cache.write().unwrap(); diff --git a/lib/src/workspace.rs b/lib/src/workspace.rs index 36b11bfed6e..3d6294660e9 100644 --- a/lib/src/workspace.rs +++ b/lib/src/workspace.rs @@ -33,7 +33,7 @@ use crate::op_store::{OperationId, WorkspaceId}; use crate::repo::{ read_store_type_compat, BackendInitializer, CheckOutCommitError, IndexStoreInitializer, OpHeadsStoreInitializer, OpStoreInitializer, ReadonlyRepo, Repo, RepoInitError, RepoLoader, - StoreFactories, StoreLoadError, SubmoduleStoreInitializer, + SignerInitializer, StoreFactories, StoreLoadError, SubmoduleStoreInitializer, }; use crate::settings::UserSettings; use crate::store::Store; @@ -226,6 +226,7 @@ impl Workspace { user_settings: &UserSettings, workspace_root: &Path, backend_initializer: &BackendInitializer, + signer_initializer: &SignerInitializer, op_store_initializer: &OpStoreInitializer, op_heads_store_initializer: &OpHeadsStoreInitializer, index_store_initializer: &IndexStoreInitializer, @@ -241,6 +242,7 @@ impl Workspace { user_settings, &repo_dir, backend_initializer, + signer_initializer, op_store_initializer, op_heads_store_initializer, index_store_initializer, @@ -277,6 +279,7 @@ impl Workspace { user_settings, workspace_root, backend_initializer, + ReadonlyRepo::default_signer_initializer(), ReadonlyRepo::default_op_store_initializer(), ReadonlyRepo::default_op_heads_store_initializer(), ReadonlyRepo::default_index_store_initializer(), diff --git a/lib/tests/test_git.rs b/lib/tests/test_git.rs index 1885fb9737c..5d3243ba941 100644 --- a/lib/tests/test_git.rs +++ b/lib/tests/test_git.rs @@ -1125,6 +1125,7 @@ impl GitRepoData { &git_repo_dir, )?)) }, + ReadonlyRepo::default_signer_initializer(), ReadonlyRepo::default_op_store_initializer(), ReadonlyRepo::default_op_heads_store_initializer(), ReadonlyRepo::default_index_store_initializer(), @@ -1990,6 +1991,7 @@ fn test_init() { &git_repo_dir, )?)) }, + ReadonlyRepo::default_signer_initializer(), ReadonlyRepo::default_op_store_initializer(), ReadonlyRepo::default_op_heads_store_initializer(), ReadonlyRepo::default_index_store_initializer(), @@ -2315,6 +2317,7 @@ fn set_up_push_repos(settings: &UserSettings, temp_dir: &TempDir) -> PushTestSet &clone_repo_dir, )?)) }, + ReadonlyRepo::default_signer_initializer(), ReadonlyRepo::default_op_store_initializer(), ReadonlyRepo::default_op_heads_store_initializer(), ReadonlyRepo::default_index_store_initializer(), diff --git a/lib/testutils/src/lib.rs b/lib/testutils/src/lib.rs index b7438b4cd92..1280497298e 100644 --- a/lib/testutils/src/lib.rs +++ b/lib/testutils/src/lib.rs @@ -32,6 +32,7 @@ use jj_lib::repo::{MutableRepo, ReadonlyRepo, Repo, RepoLoader, StoreFactories}; use jj_lib::repo_path::RepoPath; use jj_lib::rewrite::RebasedDescendant; use jj_lib::settings::UserSettings; +use jj_lib::signing::SignConfig; use jj_lib::store::Store; use jj_lib::transaction::Transaction; use jj_lib::tree::Tree; @@ -139,6 +140,7 @@ impl TestRepo { &settings, &repo_dir, &move |settings, store_path| backend.init_backend(settings, store_path), + ReadonlyRepo::default_signer_initializer(), ReadonlyRepo::default_op_store_initializer(), ReadonlyRepo::default_op_heads_store_initializer(), ReadonlyRepo::default_index_store_initializer(), @@ -336,7 +338,7 @@ pub fn commit_with_tree(store: &Arc, tree_id: MergedTreeId) -> Commit { committer: signature, secure_sig: None, }; - store.write_commit(commit).unwrap() + store.write_commit(commit, SignConfig::default()).unwrap() } pub fn dump_tree(store: &Arc, tree_id: &MergedTreeId) -> String { diff --git a/lib/testutils/src/test_backend.rs b/lib/testutils/src/test_backend.rs index f4e1c62a1e9..aa2d2d5d55e 100644 --- a/lib/testutils/src/test_backend.rs +++ b/lib/testutils/src/test_backend.rs @@ -282,7 +282,7 @@ impl Backend for TestBackend { if let Some(sign) = &mut sign_with { let data = format!("{contents:?}").into_bytes(); - let sig = sign(&data)?; + let sig = sign(&data).map_err(|err| BackendError::Other(Box::new(err)))?; contents.secure_sig = Some(SecureSig { data, sig }); }