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