jj_lib/signing.rs
1// Copyright 2023 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Generic APIs to work with cryptographic signatures created and verified by
16//! various backends.
17
18use std::fmt::Debug;
19use std::fmt::Display;
20use std::sync::Mutex;
21
22use clru::CLruCache;
23use thiserror::Error;
24
25use crate::backend::CommitId;
26use crate::config::ConfigGetError;
27use crate::gpg_signing::GpgBackend;
28use crate::gpg_signing::GpgsmBackend;
29use crate::settings::UserSettings;
30use crate::ssh_signing::SshBackend;
31use crate::store::COMMIT_CACHE_CAPACITY;
32#[cfg(feature = "testing")]
33use crate::test_signing_backend::TestSigningBackend;
34
35/// A status of the signature, part of the [Verification] type.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum SigStatus {
38 /// Valid signature that matches the data.
39 Good,
40 /// Valid signature that could not be verified (e.g. due to an unknown key).
41 Unknown,
42 /// Valid signature that does not match the signed data.
43 Bad,
44}
45
46impl Display for SigStatus {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 let s = match self {
49 SigStatus::Good => "good",
50 SigStatus::Unknown => "unknown",
51 SigStatus::Bad => "bad",
52 };
53 write!(f, "{s}")
54 }
55}
56
57/// The result of a signature verification.
58/// Key and display are optional additional info that backends can or can not
59/// provide to add additional information for the templater to potentially show.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct Verification {
62 /// The status of the signature.
63 pub status: SigStatus,
64 /// The key id representation, if available. For GPG, this will be the key
65 /// fingerprint.
66 pub key: Option<String>,
67 /// A display string, if available. For GPG, this will be formatted primary
68 /// user ID.
69 pub display: Option<String>,
70}
71
72impl Verification {
73 /// A shortcut to create an `Unknown` verification with no additional
74 /// metadata.
75 pub fn unknown() -> Self {
76 Self {
77 status: SigStatus::Unknown,
78 key: None,
79 display: None,
80 }
81 }
82
83 /// Create a new verification
84 pub fn new(status: SigStatus, key: Option<String>, display: Option<String>) -> Self {
85 Self {
86 status,
87 key,
88 display,
89 }
90 }
91}
92
93/// The backend for signing and verifying cryptographic signatures.
94///
95/// This allows using different signers, such as GPG or SSH, or different
96/// versions of them.
97pub trait SigningBackend: Debug + Send + Sync {
98 /// Name of the backend, used in the config and for display.
99 fn name(&self) -> &str;
100
101 /// Check if the signature can be read and verified by this backend.
102 ///
103 /// Should check the signature format, usually just looks at the prefix.
104 fn can_read(&self, signature: &[u8]) -> bool;
105
106 /// Create a signature for arbitrary data.
107 ///
108 /// The `key` parameter is what `jj sign` receives as key argument, or what
109 /// is configured in the `signing.key` config.
110 fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult<Vec<u8>>;
111
112 /// Verify a signature. Should be reflexive with `sign`:
113 /// ```rust,ignore
114 /// verify(data, sign(data)?)?.status == SigStatus::Good
115 /// ```
116 fn verify(&self, data: &[u8], signature: &[u8]) -> SignResult<Verification>;
117}
118
119/// An error type for the signing/verifying operations
120#[derive(Debug, Error)]
121pub enum SignError {
122 /// The verification failed because the signature *format* was invalid.
123 #[error("Invalid signature")]
124 InvalidSignatureFormat,
125 /// A generic error from the backend impl.
126 #[error("Signing error")]
127 Backend(#[source] Box<dyn std::error::Error + Send + Sync>),
128}
129
130/// A result type for the signing/verifying operations
131pub type SignResult<T> = Result<T, SignError>;
132
133/// An error type for the signing backend initialization.
134#[derive(Debug, Error)]
135pub enum SignInitError {
136 /// If the backend name specified in the config is not known.
137 #[error("Unknown signing backend configured: {0}")]
138 UnknownBackend(String),
139 /// Failed to load backend configuration.
140 #[error("Failed to configure signing backend")]
141 BackendConfig(#[source] ConfigGetError),
142}
143
144/// A enum that describes if a created/rewritten commit should be signed or not.
145#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
146#[serde(rename_all = "kebab-case")]
147pub enum SignBehavior {
148 /// Drop existing signatures.
149 /// This is what jj did before signing support or does now when a signing
150 /// backend is not configured.
151 Drop,
152 /// Only sign commits that were authored by self and already signed,
153 /// "preserving" the signature across rewrites.
154 /// This is what jj does when a signing backend is configured.
155 Keep,
156 /// Sign/re-sign commits that were authored by self and drop them for
157 /// others. This is what jj does when configured to always sign.
158 Own,
159 /// Always sign commits, regardless of who authored or signed them before.
160 /// This is what jj does on `jj sign -f`.
161 Force,
162}
163
164/// Wraps low-level signing backends and adds caching, similar to `Store`.
165#[derive(Debug)]
166pub struct Signer {
167 /// The backend that is used for signing commits.
168 /// Optional because signing might not be configured.
169 main_backend: Option<Box<dyn SigningBackend>>,
170 /// All known backends without the main one - used for verification.
171 /// Main backend is also used for verification, but it's not in this list
172 /// for ownership reasons.
173 backends: Vec<Box<dyn SigningBackend>>,
174 cache: Mutex<CLruCache<CommitId, Verification>>,
175}
176
177impl Signer {
178 /// Creates a signer based on user settings. Uses all known backends, and
179 /// chooses one of them to be used for signing depending on the config.
180 pub fn from_settings(settings: &UserSettings) -> Result<Self, SignInitError> {
181 let mut backends: Vec<Box<dyn SigningBackend>> = vec![
182 Box::new(GpgBackend::from_settings(settings).map_err(SignInitError::BackendConfig)?),
183 Box::new(GpgsmBackend::from_settings(settings).map_err(SignInitError::BackendConfig)?),
184 Box::new(SshBackend::from_settings(settings).map_err(SignInitError::BackendConfig)?),
185 #[cfg(feature = "testing")]
186 Box::new(TestSigningBackend),
187 ];
188
189 let main_backend = settings
190 .signing_backend()
191 .map_err(SignInitError::BackendConfig)?
192 .map(|backend| {
193 backends
194 .iter()
195 .position(|b| b.name() == backend)
196 .map(|i| backends.remove(i))
197 .ok_or(SignInitError::UnknownBackend(backend))
198 })
199 .transpose()?;
200
201 Ok(Self::new(main_backend, backends))
202 }
203
204 /// Creates a signer with the given backends.
205 pub fn new(
206 main_backend: Option<Box<dyn SigningBackend>>,
207 other_backends: Vec<Box<dyn SigningBackend>>,
208 ) -> Self {
209 Self {
210 main_backend,
211 backends: other_backends,
212 cache: Mutex::new(CLruCache::new(COMMIT_CACHE_CAPACITY.try_into().unwrap())),
213 }
214 }
215
216 /// Checks if the signer can sign, i.e. if a main backend is configured.
217 pub fn can_sign(&self) -> bool {
218 self.main_backend.is_some()
219 }
220
221 /// This is just a pass-through to the main backend that unconditionally
222 /// creates a signature.
223 pub fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult<Vec<u8>> {
224 self.main_backend
225 .as_ref()
226 .expect("tried to sign without checking can_sign first")
227 .sign(data, key)
228 }
229
230 /// Looks for backend that can verify the signature and returns the result
231 /// of its verification.
232 pub fn verify(
233 &self,
234 commit_id: &CommitId,
235 data: &[u8],
236 signature: &[u8],
237 ) -> SignResult<Verification> {
238 let cached = self.cache.lock().unwrap().get(commit_id).cloned();
239 if let Some(check) = cached {
240 return Ok(check);
241 }
242
243 let verification = self
244 .main_backend
245 .iter()
246 .chain(self.backends.iter())
247 .filter(|b| b.can_read(signature))
248 // skip unknown and invalid sigs to allow other backends that can read to try
249 // for example, we might have gpg and sq, both of which could read a PGP signature
250 .find_map(|backend| match backend.verify(data, signature) {
251 Ok(check) if check.status == SigStatus::Unknown => None,
252 Err(SignError::InvalidSignatureFormat) => None,
253 e => Some(e),
254 })
255 .transpose()?;
256
257 if let Some(verification) = verification {
258 // a key might get imported before next call?.
259 // realistically this is unlikely, but technically
260 // it's correct to not cache unknowns here
261 if verification.status != SigStatus::Unknown {
262 self.cache
263 .lock()
264 .unwrap()
265 .put(commit_id.clone(), verification.clone());
266 }
267 Ok(verification)
268 } else {
269 // now here it's correct to cache unknowns, as we don't
270 // have a backend that knows how to handle this signature
271 //
272 // not sure about how much of an optimization this is
273 self.cache
274 .lock()
275 .unwrap()
276 .put(commit_id.clone(), Verification::unknown());
277 Ok(Verification::unknown())
278 }
279 }
280}