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}