Skip to main content

identity_kit/keys/
software.rs

1//! `SoftwareKeyStore`: private keys in a JWK set file.
2
3use core::fmt;
4use std::fs::{self, OpenOptions};
5use std::io::{self, Write as _};
6use std::path::{Path, PathBuf};
7use std::sync::{PoisonError, RwLock};
8
9use indexmap::IndexMap;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use super::local::{self, Entry};
14use super::{KeyId, KeySpec, KeyStore, KeyStoreCapabilities, KeyStoreError};
15use crate::jose::{Jwk, JwsAlgorithm};
16
17/// A JWK set (RFC 7517 §5) of private keys, the on-disk format of the store.
18#[derive(Serialize, Deserialize)]
19struct JwkSet {
20    keys: Vec<Jwk>,
21    #[serde(flatten)]
22    additional: IndexMap<String, Value>,
23}
24
25/// How to open a [`SoftwareKeyStore`].
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
27#[non_exhaustive]
28pub struct SoftwareKeyStoreOptions {
29    /// Open a file that other users can read or write.
30    ///
31    /// By default a key store file with group or world permission bits is refused with
32    /// [`KeyStoreError::InsecurePermissions`]. The check runs on Unix only.
33    pub allow_insecure_permissions: bool,
34    /// Create an empty key store if the file does not exist.
35    pub create_if_missing: bool,
36}
37
38impl SoftwareKeyStoreOptions {
39    /// The defaults: refuse insecure permissions, fail on a missing file.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Sets [`allow_insecure_permissions`](Self::allow_insecure_permissions).
45    #[must_use]
46    pub fn allow_insecure_permissions(mut self, allow: bool) -> Self {
47        self.allow_insecure_permissions = allow;
48        self
49    }
50
51    /// Sets [`create_if_missing`](Self::create_if_missing).
52    #[must_use]
53    pub fn create_if_missing(mut self, create: bool) -> Self {
54        self.create_if_missing = create;
55        self
56    }
57}
58
59/// A [`KeyStore`] over a file holding a JWK set of private keys.
60///
61/// The file is `{"keys": [...]}` (RFC 7517 §5) where every key is a private JWK with a `kid`,
62/// the key id, and an `alg`, the one algorithm it signs with. Signs EdDSA, ES256 and (feature
63/// `secp256k1`) ES256K; generates and deletes. RSA keys in the file are refused, since the crate
64/// never signs with RSA (`docs/adr/0001-rsa-verification-only.md`).
65///
66/// The whole file is read when the store is opened and every key is checked then: a key whose
67/// public members do not match its private scalar, whose `alg` does not fit, or whose `kid` is
68/// taken by another key fails [`SoftwareKeyStore::open`] with [`KeyStoreError::InvalidKeySet`].
69/// Private material is zeroized when the store is dropped.
70///
71/// Writes (`generate`, `delete`, [`import`](Self::import)) replace the file atomically: the new
72/// set is written to a temporary file in the same directory, created with mode `0600` on Unix,
73/// synced and renamed over the old one. A store opened on a file that other users can read is
74/// refused unless [`SoftwareKeyStoreOptions::allow_insecure_permissions`] is set. Nothing locks
75/// the file against other processes; one store per file.
76///
77/// The key id of a generated key is the spec's label when set and the RFC 7638 thumbprint of
78/// the public key otherwise. `xtask keys generate` provisions a file from the command line.
79///
80/// # Examples
81///
82/// ```
83/// # tokio::runtime::Builder::new_current_thread().build()?.block_on(async {
84/// use identity_kit::jose::{JwsAlgorithm, KeyType};
85/// use identity_kit::keys::{KeySpec, KeyStore, SoftwareKeyStore, SoftwareKeyStoreOptions};
86///
87/// let dir = tempfile::tempdir()?;
88/// let path = dir.path().join("keys.json");
89/// let store = SoftwareKeyStore::create(&path)?;
90/// let key = store
91///     .generate(KeySpec::new(KeyType::P256, JwsAlgorithm::ES256).with_label("issuer"))
92///     .await?;
93/// assert_eq!(key.as_str(), "issuer");
94///
95/// // The key is on disk and usable by the next store opened on the file.
96/// let reopened = SoftwareKeyStore::open(&path)?;
97/// assert!(reopened.exists(&key).await?);
98/// assert_eq!(reopened.sign(&key, JwsAlgorithm::ES256, b"payload").await?.len(), 64);
99/// # Ok::<(), Box<dyn std::error::Error>>(())
100/// # })?;
101/// # Ok::<(), Box<dyn std::error::Error>>(())
102/// ```
103pub struct SoftwareKeyStore {
104    path: PathBuf,
105    keys: RwLock<IndexMap<KeyId, Entry>>,
106    additional: IndexMap<String, Value>,
107}
108
109impl SoftwareKeyStore {
110    /// Opens the key set file at `path` with the default options.
111    ///
112    /// # Errors
113    ///
114    /// [`KeyStoreError::Io`] when the file cannot be read, [`KeyStoreError::InsecurePermissions`]
115    /// when other users can read it and [`KeyStoreError::InvalidKeySet`] when it is not a JWK set
116    /// of usable private keys.
117    pub fn open(path: impl Into<PathBuf>) -> Result<Self, KeyStoreError> {
118        Self::open_with(path, SoftwareKeyStoreOptions::default())
119    }
120
121    /// Opens the key set file at `path` as `options` say.
122    ///
123    /// # Errors
124    ///
125    /// As [`SoftwareKeyStore::open`].
126    #[expect(
127        clippy::needless_pass_by_value,
128        reason = "options are taken by value, as builders take them, so that they can grow"
129    )]
130    pub fn open_with(
131        path: impl Into<PathBuf>,
132        options: SoftwareKeyStoreOptions,
133    ) -> Result<Self, KeyStoreError> {
134        let path = path.into();
135        if options.create_if_missing && !path.exists() {
136            return Self::create(path);
137        }
138        let data = fs::read(&path).map_err(|source| KeyStoreError::Io {
139            path: path.clone(),
140            source,
141        })?;
142        if !options.allow_insecure_permissions {
143            check_permissions(&path)?;
144        }
145        let set: JwkSet =
146            serde_json::from_slice(&data).map_err(|e| KeyStoreError::InvalidKeySet {
147                path: path.clone(),
148                reason: "not a JWK set".to_owned(),
149                source: Some(Box::new(e)),
150            })?;
151        let mut keys = IndexMap::with_capacity(set.keys.len());
152        for jwk in set.keys {
153            let (id, entry) =
154                Entry::from_jwk(jwk).map_err(|unusable| unusable.for_key_set(path.clone()))?;
155            if keys.insert(id.clone(), entry).is_some() {
156                return Err(KeyStoreError::InvalidKeySet {
157                    path,
158                    reason: format!("key `{id}` is defined twice"),
159                    source: None,
160                });
161            }
162        }
163        Ok(Self {
164            path,
165            keys: RwLock::new(keys),
166            additional: set.additional,
167        })
168    }
169
170    /// Creates an empty key set file at `path`, with mode `0600` on Unix.
171    ///
172    /// # Errors
173    ///
174    /// [`KeyStoreError::Io`] when the file exists already or cannot be written.
175    pub fn create(path: impl Into<PathBuf>) -> Result<Self, KeyStoreError> {
176        let path = path.into();
177        if path.exists() {
178            return Err(KeyStoreError::Io {
179                path,
180                source: io::Error::new(io::ErrorKind::AlreadyExists, "file exists"),
181            });
182        }
183        let store = Self {
184            path,
185            keys: RwLock::new(IndexMap::new()),
186            additional: IndexMap::new(),
187        };
188        store.save(&IndexMap::new())?;
189        Ok(store)
190    }
191
192    /// The key set file.
193    pub fn path(&self) -> &Path {
194        &self.path
195    }
196
197    /// The ids of the keys in the store, in file order.
198    pub fn key_ids(&self) -> Vec<KeyId> {
199        self.keys
200            .read()
201            .unwrap_or_else(PoisonError::into_inner)
202            .keys()
203            .cloned()
204            .collect()
205    }
206
207    /// Adds an existing private JWK to the store and writes the file.
208    ///
209    /// The key id is the `kid` when set and the RFC 7638 thumbprint of the public key otherwise;
210    /// `alg` must fit the key when set and defaults to the key type's algorithm.
211    ///
212    /// # Errors
213    ///
214    /// [`KeyStoreError::InvalidKey`] when the JWK is not a usable private key of a supported
215    /// type, [`KeyStoreError::AlreadyExists`] when its id is taken and [`KeyStoreError::Io`] when
216    /// the file cannot be written.
217    pub fn import(&self, jwk: Jwk) -> Result<KeyId, KeyStoreError> {
218        let key = KeyId::new(jwk.kid().unwrap_or_default());
219        let (id, entry) = Entry::from_jwk(jwk).map_err(|unusable| unusable.for_key(key))?;
220        self.insert(id, entry)
221    }
222
223    /// Inserts `entry` under `id` and writes the file, rolling back on failure.
224    fn insert(&self, id: KeyId, entry: Entry) -> Result<KeyId, KeyStoreError> {
225        let mut keys = self.keys.write().unwrap_or_else(PoisonError::into_inner);
226        if keys.contains_key(&id) {
227            return Err(KeyStoreError::AlreadyExists(id));
228        }
229        keys.insert(id.clone(), entry);
230        if let Err(e) = self.save(&keys) {
231            keys.shift_remove(&id);
232            return Err(e);
233        }
234        Ok(id)
235    }
236
237    /// Writes `keys` to the file atomically.
238    fn save(&self, keys: &IndexMap<KeyId, Entry>) -> Result<(), KeyStoreError> {
239        let set = JwkSet {
240            keys: keys.values().map(|entry| entry.jwk.clone()).collect(),
241            additional: self.additional.clone(),
242        };
243        // A `Jwk` serializes to a JSON object; this cannot fail.
244        let mut json = serde_json::to_vec_pretty(&set).map_err(|e| KeyStoreError::Io {
245            path: self.path.clone(),
246            source: io::Error::other(e),
247        })?;
248        json.push(b'\n');
249        write_atomically(&self.path, &json).map_err(|source| KeyStoreError::Io {
250            path: self.path.clone(),
251            source,
252        })
253    }
254}
255
256impl fmt::Debug for SoftwareKeyStore {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        f.debug_struct("SoftwareKeyStore")
259            .field("path", &self.path)
260            .field("keys", &self.key_ids())
261            .finish_non_exhaustive()
262    }
263}
264
265#[async_trait::async_trait]
266impl KeyStore for SoftwareKeyStore {
267    fn capabilities(&self) -> KeyStoreCapabilities {
268        KeyStoreCapabilities::new(local::ALGORITHMS.iter().copied())
269            .with_generate()
270            .with_delete()
271    }
272
273    async fn public_jwk(&self, key: &KeyId) -> Result<Jwk, KeyStoreError> {
274        self.keys
275            .read()
276            .unwrap_or_else(PoisonError::into_inner)
277            .get(key)
278            .map(Entry::public_jwk)
279            .ok_or_else(|| KeyStoreError::NotFound(key.clone()))
280    }
281
282    async fn sign(
283        &self,
284        key: &KeyId,
285        alg: JwsAlgorithm,
286        data: &[u8],
287    ) -> Result<Vec<u8>, KeyStoreError> {
288        self.keys
289            .read()
290            .unwrap_or_else(PoisonError::into_inner)
291            .get(key)
292            .ok_or_else(|| KeyStoreError::NotFound(key.clone()))?
293            .sign(key, alg, data)
294    }
295
296    async fn exists(&self, key: &KeyId) -> Result<bool, KeyStoreError> {
297        Ok(self
298            .keys
299            .read()
300            .unwrap_or_else(PoisonError::into_inner)
301            .contains_key(key))
302    }
303
304    async fn generate(&self, spec: KeySpec) -> Result<KeyId, KeyStoreError> {
305        let (id, key) = local::generate(&spec, &mut local::os_random)?;
306        let entry = Entry::new(&id, key, spec.alg);
307        self.insert(id, entry)
308    }
309
310    async fn delete(&self, key: &KeyId) -> Result<(), KeyStoreError> {
311        let mut keys = self.keys.write().unwrap_or_else(PoisonError::into_inner);
312        let (index, id, entry) = keys
313            .shift_remove_full(key)
314            .ok_or_else(|| KeyStoreError::NotFound(key.clone()))?;
315        if let Err(e) = self.save(&keys) {
316            keys.shift_insert(index, id, entry);
317            return Err(e);
318        }
319        Ok(())
320    }
321}
322
323/// Refuses a file with group or world permission bits. Unix only.
324#[cfg(unix)]
325fn check_permissions(path: &Path) -> Result<(), KeyStoreError> {
326    use std::os::unix::fs::PermissionsExt as _;
327    let mode = fs::metadata(path)
328        .map_err(|source| KeyStoreError::Io {
329            path: path.to_owned(),
330            source,
331        })?
332        .permissions()
333        .mode()
334        & 0o7777;
335    if mode & 0o077 != 0 {
336        return Err(KeyStoreError::InsecurePermissions {
337            path: path.to_owned(),
338            mode,
339        });
340    }
341    Ok(())
342}
343
344#[cfg(not(unix))]
345fn check_permissions(_path: &Path) -> Result<(), KeyStoreError> {
346    Ok(())
347}
348
349/// Writes `data` to a private temporary file next to `path`, syncs it and renames it over
350/// `path`, so that a reader sees either the old or the new content and never a partial file.
351fn write_atomically(path: &Path, data: &[u8]) -> io::Result<()> {
352    let dir = match path.parent() {
353        Some(dir) if !dir.as_os_str().is_empty() => dir,
354        _ => Path::new("."),
355    };
356    let file_name = path
357        .file_name()
358        .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?;
359    let mut nonce = [0u8; 8];
360    getrandom::fill(&mut nonce).map_err(io::Error::other)?;
361    let mut tmp_name = std::ffi::OsString::from(".");
362    tmp_name.push(file_name);
363    tmp_name.push(format!(".{:016x}.tmp", u64::from_be_bytes(nonce)));
364    let tmp = dir.join(tmp_name);
365
366    let mut options = OpenOptions::new();
367    options.write(true).create_new(true);
368    #[cfg(unix)]
369    {
370        use std::os::unix::fs::OpenOptionsExt as _;
371        options.mode(0o600);
372    }
373    let result = options.open(&tmp).and_then(|mut file| {
374        file.write_all(data)?;
375        file.sync_all()?;
376        drop(file);
377        fs::rename(&tmp, path)
378    });
379    if result.is_err() {
380        let _ = fs::remove_file(&tmp);
381    }
382    result
383}