signatory/key/store/
fs.rs

1//! Filesystem-backed keystore
2
3use crate::{Error, KeyHandle, KeyInfo, KeyName, KeyRing, LoadPkcs8, Result};
4use pkcs8::der::pem::PemLabel;
5use std::{
6    fs,
7    path::{Path, PathBuf},
8};
9use zeroize::Zeroizing;
10
11#[cfg(unix)]
12use std::{fs::Permissions, os::unix::fs::PermissionsExt};
13
14/// Required filesystem mode for keystore directories (Unix-only)
15#[cfg(unix)]
16const REQUIRED_DIR_MODE: u32 = 0o700;
17
18/// PEM encapsulation boundary for private keys.
19const PRIVATE_KEY_BOUNDARY: &str = "-----BEGIN PRIVATE KEY-----";
20
21/// PEM encapsulation boundary for encrypted private keys.
22const ENCRYPTED_PRIVATE_KEY_BOUNDARY: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
23
24/// Filesystem-backed keystore.
25pub struct FsKeyStore {
26    path: PathBuf,
27}
28
29impl FsKeyStore {
30    /// Attempt to open a filesystem-backed keystore at the given path,
31    /// creating it if it doesn't already exist.
32    pub fn create_or_open(dir_path: &Path) -> Result<Self> {
33        Self::open(dir_path).or_else(|_| Self::create(dir_path))
34    }
35
36    /// Create a filesystem-backed keystore at the given path, making a new
37    /// directory and setting its file permissions.
38    pub fn create(dir_path: &Path) -> Result<Self> {
39        fs::create_dir_all(dir_path)?;
40
41        #[cfg(unix)]
42        fs::set_permissions(dir_path, Permissions::from_mode(REQUIRED_DIR_MODE))?;
43
44        Self::open(dir_path)
45    }
46
47    /// Initialize filesystem-backed keystore, opening the directory at the
48    /// provided path and checking that it has the correct filesystem
49    /// permissions.
50    pub fn open(dir_path: &Path) -> Result<Self> {
51        let path = dir_path.canonicalize()?;
52        let st = path.metadata()?;
53
54        if !st.is_dir() {
55            return Err(Error::NotADirectory);
56        }
57
58        #[cfg(unix)]
59        if st.permissions().mode() & 0o777 != REQUIRED_DIR_MODE {
60            return Err(Error::Permissions);
61        }
62
63        Ok(Self { path })
64    }
65
66    /// Get information about a key with the given name.
67    pub fn info(&self, name: &KeyName) -> Result<KeyInfo> {
68        let pem_data = Zeroizing::new(fs::read_to_string(self.key_path(name))?);
69
70        let encrypted = if pem_data.starts_with(ENCRYPTED_PRIVATE_KEY_BOUNDARY) {
71            true
72        } else if pem_data.starts_with(PRIVATE_KEY_BOUNDARY) {
73            false
74        } else {
75            return Err(pkcs8::Error::KeyMalformed.into());
76        };
77
78        let algorithm = if encrypted {
79            None
80        } else {
81            let (label, der) = pkcs8::SecretDocument::from_pem(&pem_data)?;
82            pkcs8::PrivateKeyInfo::validate_pem_label(label)?;
83            der.decode_msg::<pkcs8::PrivateKeyInfo<'_>>()?
84                .algorithm
85                .try_into()
86                .ok()
87        };
88
89        Ok(KeyInfo {
90            name: name.clone(),
91            algorithm,
92            encrypted,
93        })
94    }
95
96    /// Import a key with a given name into the provided keyring.
97    pub fn import(&self, name: &KeyName, key_ring: &mut KeyRing) -> Result<KeyHandle> {
98        key_ring.load_pkcs8(self.load(name)?.decode_msg()?)
99    }
100
101    /// Load a PKCS#8 key from the keystore.
102    pub fn load(&self, name: &KeyName) -> Result<pkcs8::SecretDocument> {
103        let (label, doc) = pkcs8::SecretDocument::read_pem_file(self.key_path(name))?;
104        pkcs8::PrivateKeyInfo::validate_pem_label(&label)?;
105        Ok(doc)
106    }
107
108    /// Import a PKCS#8 key into the keystore.
109    pub fn store(&self, name: &KeyName, der: &pkcs8::SecretDocument) -> Result<()> {
110        der.write_pem_file(
111            self.key_path(name),
112            pkcs8::PrivateKeyInfo::PEM_LABEL,
113            Default::default(),
114        )?;
115        Ok(())
116    }
117
118    /// Delete a PKCS#8 key from the keystore.
119    pub fn delete(&self, name: &KeyName) -> Result<()> {
120        fs::remove_file(self.key_path(name))?;
121
122        Ok(())
123    }
124
125    /// Compute the path for a key with a given name.
126    fn key_path(&self, name: &KeyName) -> PathBuf {
127        let mut path = self.path.join(name);
128        path.set_extension("pem");
129        path
130    }
131}
132
133#[cfg(test)]
134#[allow(unused_imports)] // TODO(tarcieri): always use imports
135mod tests {
136    use super::FsKeyStore;
137    use crate::{Algorithm, GeneratePkcs8};
138
139    #[cfg(feature = "secp256k1")]
140    use crate::ecdsa::secp256k1;
141
142    pub const EXAMPLE_KEY: &str = "example-key";
143
144    pub struct FsStoreHandle {
145        pub keystore: FsKeyStore,
146        pub dir: tempfile::TempDir,
147    }
148
149    /// Create a keystore containing one key named `example_key` with the given content
150    #[allow(dead_code)]
151    fn create_example_keystore(example_key: &pkcs8::SecretDocument) -> FsStoreHandle {
152        let dir = tempfile::tempdir().unwrap();
153        let keystore = FsKeyStore::create_or_open(&dir.path().join("keys")).unwrap();
154
155        keystore
156            .store(&EXAMPLE_KEY.parse().unwrap(), example_key)
157            .unwrap();
158
159        FsStoreHandle { keystore, dir }
160    }
161
162    #[cfg(feature = "secp256k1")]
163    #[test]
164    fn import_and_delete_key() {
165        let key_name = EXAMPLE_KEY.parse().unwrap();
166        let example_key = secp256k1::SigningKey::generate_pkcs8();
167        let ks = create_example_keystore(&example_key);
168
169        let example_key2 = ks.keystore.load(&key_name).unwrap();
170        assert_eq!(example_key.as_bytes(), example_key2.as_bytes());
171
172        ks.keystore.delete(&key_name).unwrap();
173    }
174
175    #[cfg(feature = "secp256k1")]
176    #[test]
177    fn get_key_info() {
178        let key_name = EXAMPLE_KEY.parse().unwrap();
179        let example_key = secp256k1::SigningKey::generate_pkcs8();
180        let ks = create_example_keystore(&example_key);
181
182        let key_info = ks.keystore.info(&key_name).unwrap();
183        assert_eq!(key_info.name, key_name);
184        assert_eq!(key_info.algorithm, Some(Algorithm::EcdsaSecp256k1));
185        assert!(!key_info.encrypted);
186    }
187}