jj_core/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::num::NonZeroUsize;
21use std::sync::Mutex;
22
23use clru::CLruCache;
24use thiserror::Error;
25
26use crate::backend::CommitId;
27
28// TODO: This is a duplication of `jj_lib::store::COMMIT_CACHE_CAPACITY`. Use
29// the respective constant when we lower `Store`.
30const COMMIT_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(100).unwrap();
31
32/// A status of the signature, part of the [Verification] type.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum SigStatus {
35 /// Valid signature that matches the data.
36 Good,
37 /// Valid signature that could not be verified (e.g. due to an unknown key).
38 Unknown,
39 /// Valid signature that does not match the signed data.
40 Bad,
41}
42
43impl Display for SigStatus {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 let s = match self {
46 Self::Good => "good",
47 Self::Unknown => "unknown",
48 Self::Bad => "bad",
49 };
50 write!(f, "{s}")
51 }
52}
53
54/// The result of a signature verification.
55/// Key and display are optional additional info that backends can or can not
56/// provide to add additional information for the templater to potentially show.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct Verification {
59 /// The status of the signature.
60 pub status: SigStatus,
61 /// The key id representation, if available. For GPG, this will be the key
62 /// fingerprint.
63 pub key: Option<String>,
64 /// A display string, if available. For GPG, this will be formatted primary
65 /// user ID.
66 pub display: Option<String>,
67}
68
69impl Verification {
70 /// A shortcut to create an `Unknown` verification with no additional
71 /// metadata.
72 pub fn unknown() -> Self {
73 Self {
74 status: SigStatus::Unknown,
75 key: None,
76 display: None,
77 }
78 }
79
80 /// Create a new verification
81 pub fn new(status: SigStatus, key: Option<String>, display: Option<String>) -> Self {
82 Self {
83 status,
84 key,
85 display,
86 }
87 }
88}
89
90/// The backend for signing and verifying cryptographic signatures.
91///
92/// This allows using different signers, such as GPG or SSH, or different
93/// versions of them.
94pub trait SigningBackend: Debug + Send + Sync {
95 /// Name of the backend, used in the config and for display.
96 fn name(&self) -> &str;
97
98 /// Check if the signature can be read and verified by this backend.
99 ///
100 /// Should check the signature format, usually just looks at the prefix.
101 fn can_read(&self, signature: &[u8]) -> bool;
102
103 /// Create a signature for arbitrary data.
104 ///
105 /// The `key` parameter is what `jj sign` receives as key argument, or what
106 /// is configured in the `signing.key` config.
107 fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult<Vec<u8>>;
108
109 /// Verify a signature. Should be reflexive with `sign`:
110 /// ```rust,ignore
111 /// verify(data, sign(data)?)?.status == SigStatus::Good
112 /// ```
113 fn verify(&self, data: &[u8], signature: &[u8]) -> SignResult<Verification>;
114}
115
116/// An error type for the signing/verifying operations
117#[derive(Debug, Error)]
118pub enum SignError {
119 /// The verification failed because the signature *format* was invalid.
120 #[error("Invalid signature")]
121 InvalidSignatureFormat,
122 /// A generic error from the backend impl.
123 #[error("Signing error")]
124 Backend(#[source] Box<dyn std::error::Error + Send + Sync>),
125}
126
127/// A result type for the signing/verifying operations
128pub type SignResult<T> = Result<T, SignError>;
129
130/// Wraps low-level signing backends and adds caching, similar to `Store`.
131#[derive(Debug)]
132pub struct Signer {
133 /// The backend that is used for signing commits.
134 /// Optional because signing might not be configured.
135 main_backend: Option<Box<dyn SigningBackend>>,
136 /// All known backends without the main one - used for verification.
137 /// Main backend is also used for verification, but it's not in this list
138 /// for ownership reasons.
139 backends: Vec<Box<dyn SigningBackend>>,
140 cache: Mutex<CLruCache<CommitId, Verification>>,
141}
142
143impl Signer {
144 /// Creates a signer with the given backends.
145 pub fn new(
146 main_backend: Option<Box<dyn SigningBackend>>,
147 other_backends: Vec<Box<dyn SigningBackend>>,
148 ) -> Self {
149 Self {
150 main_backend,
151 backends: other_backends,
152 cache: Mutex::new(CLruCache::new(COMMIT_CACHE_CAPACITY)),
153 }
154 }
155
156 /// Checks if the signer can sign, i.e. if a main backend is configured.
157 pub fn can_sign(&self) -> bool {
158 self.main_backend.is_some()
159 }
160
161 /// This is just a pass-through to the main backend that unconditionally
162 /// creates a signature.
163 pub fn sign(&self, data: &[u8], key: Option<&str>) -> SignResult<Vec<u8>> {
164 self.main_backend
165 .as_ref()
166 .expect("tried to sign without checking can_sign first")
167 .sign(data, key)
168 }
169
170 /// Looks for backend that can verify the signature and returns the result
171 /// of its verification.
172 pub fn verify(
173 &self,
174 commit_id: &CommitId,
175 data: &[u8],
176 signature: &[u8],
177 ) -> SignResult<Verification> {
178 let cached = self.cache.lock().unwrap().get(commit_id).cloned();
179 if let Some(check) = cached {
180 return Ok(check);
181 }
182
183 let verification = self
184 .main_backend
185 .iter()
186 .chain(self.backends.iter())
187 .filter(|b| b.can_read(signature))
188 // skip unknown and invalid sigs to allow other backends that can read to try
189 // for example, we might have gpg and sq, both of which could read a PGP signature
190 .find_map(|backend| match backend.verify(data, signature) {
191 Ok(check) if check.status == SigStatus::Unknown => None,
192 Err(SignError::InvalidSignatureFormat) => None,
193 e => Some(e),
194 })
195 .transpose()?;
196
197 if let Some(verification) = verification {
198 // a key might get imported before next call?.
199 // realistically this is unlikely, but technically
200 // it's correct to not cache unknowns here
201 if verification.status != SigStatus::Unknown {
202 self.cache
203 .lock()
204 .unwrap()
205 .put(commit_id.clone(), verification.clone());
206 }
207 Ok(verification)
208 } else {
209 // now here it's correct to cache unknowns, as we don't
210 // have a backend that knows how to handle this signature
211 //
212 // not sure about how much of an optimization this is
213 self.cache
214 .lock()
215 .unwrap()
216 .put(commit_id.clone(), Verification::unknown());
217 Ok(Verification::unknown())
218 }
219 }
220}