tor_keymgr/keystore/ctor/
service.rs

1//! Read-only C Tor service key store implementation
2//!
3//! See [`CTorServiceKeystore`] for more details.
4
5use crate::keystore::ctor::err::{CTorKeystoreError, MalformedServiceKeyError};
6use crate::keystore::ctor::CTorKeystore;
7use crate::keystore::fs_utils::{checked_op, FilesystemAction, FilesystemError};
8use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore, KeystoreId};
9use crate::{CTorPath, CTorServicePath, KeyPath, Result};
10
11use fs_mistrust::Mistrust;
12use tor_basic_utils::PathExt as _;
13use tor_error::internal;
14use tor_key_forge::{KeyType, KeystoreItemType};
15use tor_llcrypto::pk::ed25519;
16use tor_persist::hsnickname::HsNickname;
17
18use std::path::{Path, PathBuf};
19use std::result::Result as StdResult;
20use std::sync::Arc;
21
22/// A read-only C Tor service keystore.
23///
24/// This keystore provides read-only access to the hidden service keys
25/// rooted at a given `HiddenServiceDirectory` directory
26/// (see `HiddenServiceDirectory` in `tor(1)`).
27///
28/// This keystore can be used to read the `HiddenServiceDirectory/private_key`
29/// and `HiddenServiceDirectory/public_key` C Tor keys, specified by
30/// [`CTorServicePath::PrivateKey`] (with [`KeyType::Ed25519ExpandedKeypair`])
31/// and [`CTorServicePath::PublicKey`] (with [`KeyType::Ed25519PublicKey`]),
32/// respectively. Any other files stored in `HiddenServiceDirectory` will be ignored.
33///
34/// The only supported [`Keystore`] operations are [`contains`](Keystore::contains),
35/// [`get`](Keystore::get), and [`list`](Keystore::list). All other keystore operations
36/// will return an error.
37///
38/// This keystore implementation uses the [`CTorPath`] of the requested [`KeySpecifier`]
39/// and the [`KeystoreItemType`] to identify the appropriate key.
40/// If the requested `CTorPath` is not [`Service`](CTorPath::Service),
41/// or if the [`HsNickname`] specified in the `CTorPath` does not match the nickname of this store,
42/// the key will be declared not found.
43/// If the requested `CTorPath` is [`Service`](CTorPath::Service),
44/// but the `ItemType` and [`CTorServicePath`] are mismatched,
45/// an error is returned.
46pub struct CTorServiceKeystore {
47    /// The underlying keystore
48    keystore: CTorKeystore,
49    /// The nickname of the service this keystore is meant for
50    nickname: HsNickname,
51}
52
53impl CTorServiceKeystore {
54    /// Create a new `CTorServiceKeystore`
55    /// rooted at the specified `keystore_dir` directory.
56    ///
57    /// This function returns an error if `keystore_dir` is not a directory,
58    /// or if it does not conform to the requirements of the specified `Mistrust`.
59    pub fn from_path_and_mistrust(
60        keystore_dir: impl AsRef<Path>,
61        mistrust: &Mistrust,
62        id: KeystoreId,
63        nickname: HsNickname,
64    ) -> Result<Self> {
65        let keystore = CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id)?;
66
67        Ok(Self { keystore, nickname })
68    }
69}
70
71/// Extract the key path (relative to the keystore root) from the specified result `res`,
72/// or return an error.
73///
74/// If `res` is `None`, return `ret`.
75macro_rules! rel_path_if_supported {
76    ($self:expr, $spec:expr, $ret:expr, $item_type:expr) => {{
77        use KeystoreItemType::*;
78
79        // If the key specifier doesn't have a CTorPath,
80        // we can't possibly handle this key.
81        let Some(ctor_path) = $spec.ctor_path() else {
82            return $ret;
83        };
84
85        // This keystore only deals with service keys...
86        let CTorPath::Service { path, nickname } = ctor_path else {
87            return $ret;
88        };
89
90        // ...more specifically, it has the service keys of a *particular* service
91        // (identified by nickname).
92        if nickname != $self.nickname {
93            return $ret;
94        };
95
96        let relpath = $self.keystore.rel_path(PathBuf::from(path.to_string()));
97        match ($item_type, &path) {
98            (Key(KeyType::Ed25519ExpandedKeypair), CTorServicePath::PrivateKey)
99            | (Key(KeyType::Ed25519PublicKey), CTorServicePath::PublicKey) => Ok(()),
100            _ => Err(CTorKeystoreError::InvalidKeystoreItemType {
101                item_type: $item_type.clone(),
102                item: format!("key {}", relpath.rel_path_unchecked().display_lossy()),
103            }),
104        }?;
105
106        relpath
107    }};
108}
109
110impl Keystore for CTorServiceKeystore {
111    fn id(&self) -> &KeystoreId {
112        &self.keystore.id
113    }
114
115    fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
116        let path = rel_path_if_supported!(self, key_spec, Ok(false), item_type);
117
118        let meta = match checked_op!(metadata, path) {
119            Ok(meta) => meta,
120            Err(fs_mistrust::Error::NotFound(_)) => return Ok(false),
121            Err(e) => {
122                return Err(FilesystemError::FsMistrust {
123                    action: FilesystemAction::Read,
124                    path: path.rel_path_unchecked().into(),
125                    err: e.into(),
126                })
127                .map_err(|e| CTorKeystoreError::Filesystem(e).into());
128            }
129        };
130
131        // The path exists, now check that it's actually a file and not a directory or symlink.
132        if meta.is_file() {
133            Ok(true)
134        } else {
135            Err(
136                CTorKeystoreError::Filesystem(FilesystemError::NotARegularFile(
137                    path.rel_path_unchecked().into(),
138                ))
139                .into(),
140            )
141        }
142    }
143
144    fn get(
145        &self,
146        key_spec: &dyn KeySpecifier,
147        item_type: &KeystoreItemType,
148    ) -> Result<Option<ErasedKey>> {
149        use KeystoreItemType::*;
150
151        let path = rel_path_if_supported!(self, key_spec, Ok(None), item_type);
152
153        let key = match checked_op!(read, path) {
154            Err(fs_mistrust::Error::NotFound(_)) => return Ok(None),
155            res => res
156                .map_err(|err| FilesystemError::FsMistrust {
157                    action: FilesystemAction::Read,
158                    path: path.rel_path_unchecked().into(),
159                    err: err.into(),
160                })
161                .map_err(CTorKeystoreError::Filesystem)?,
162        };
163
164        let parse_err = |err: MalformedServiceKeyError| CTorKeystoreError::MalformedKey {
165            path: path.rel_path_unchecked().into(),
166            err: err.into(),
167        };
168
169        let parsed_key: ErasedKey = match item_type {
170            Key(KeyType::Ed25519ExpandedKeypair) => parse_ed25519_keypair(&key)
171                .map_err(parse_err)
172                .map(Box::new)?,
173            Key(KeyType::Ed25519PublicKey) => parse_ed25519_public(&key)
174                .map_err(parse_err)
175                .map(Box::new)?,
176            _ => {
177                return Err(
178                    internal!("item type was not validated by rel_path_if_supported?!").into(),
179                );
180            }
181        };
182
183        Ok(Some(parsed_key))
184    }
185
186    fn insert(
187        &self,
188        _key: &dyn EncodableItem,
189        _key_spec: &dyn KeySpecifier,
190        _item_type: &KeystoreItemType,
191    ) -> Result<()> {
192        Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
193    }
194
195    fn remove(
196        &self,
197        _key_spec: &dyn KeySpecifier,
198        _item_type: &KeystoreItemType,
199    ) -> Result<Option<()>> {
200        Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
201    }
202
203    fn list(&self) -> Result<Vec<(KeyPath, KeystoreItemType)>> {
204        use crate::CTorServicePath::*;
205        use itertools::Itertools;
206
207        // This keystore can contain at most 2 keys (the public and private
208        // keys of the service)
209        let all_keys = [
210            (
211                CTorPath::Service {
212                    nickname: self.nickname.clone(),
213                    path: PublicKey,
214                },
215                KeyType::Ed25519PublicKey.into(),
216            ),
217            (
218                CTorPath::Service {
219                    nickname: self.nickname.clone(),
220                    path: PrivateKey,
221                },
222                KeyType::Ed25519ExpandedKeypair.into(),
223            ),
224        ];
225
226        all_keys
227            .into_iter()
228            .map(|(path, key_type)| {
229                self.contains(&path, &key_type)
230                    .map(|res: bool| (path, key_type, res))
231            })
232            .filter_map_ok(|(path, key_type, res)| res.then_some((path.into(), key_type)))
233            .collect()
234    }
235}
236
237/// Helper for parsing C Tor's ed25519 key format.
238macro_rules! parse_ed25519 {
239    ($key:expr, $parse_fn:expr, $tag:expr, $key_len:expr) => {{
240        let expected_len = $tag.len() + $key_len;
241
242        if $key.len() != expected_len {
243            return Err(MalformedServiceKeyError::InvalidKeyLen {
244                len: $key.len(),
245                expected_len,
246            });
247        }
248
249        let (tag, key) = $key.split_at($tag.len());
250
251        if tag != $tag {
252            return Err(MalformedServiceKeyError::InvalidTag {
253                tag: tag.to_vec(),
254                expected_tag: $tag.into(),
255            });
256        }
257
258        ($parse_fn)(key)
259    }};
260}
261
262/// Helper for parsing C Tor's ed25519 public key format.
263fn parse_ed25519_public(key: &[u8]) -> StdResult<ed25519::PublicKey, MalformedServiceKeyError> {
264    /// The tag C Tor ed25519 public keys are expected to begin with.
265    const PUBKEY_TAG: &[u8] = b"== ed25519v1-public: type0 ==\0\0\0";
266    /// The size of an ed25519 public key.
267    const PUBKEY_LEN: usize = 32;
268
269    parse_ed25519!(
270        key,
271        |key| ed25519::PublicKey::try_from(key)
272            .map_err(|e| MalformedServiceKeyError::from(Arc::new(e))),
273        PUBKEY_TAG,
274        PUBKEY_LEN
275    )
276}
277
278/// Helper for parsing C Tor's ed25519 keypair format.
279fn parse_ed25519_keypair(
280    key: &[u8],
281) -> StdResult<ed25519::ExpandedKeypair, MalformedServiceKeyError> {
282    /// The tag C Tor ed25519 keypairs are expected to begin with.
283    const KEYPAIR_TAG: &[u8] = b"== ed25519v1-secret: type0 ==\0\0\0";
284    /// The size of an ed25519 keypair.
285    const KEYPAIR_LEN: usize = 64;
286
287    parse_ed25519!(
288        key,
289        |key: &[u8]| {
290            let key: [u8; 64] = key
291                .try_into()
292                .map_err(|_| internal!("bad length on expanded ed25519 secret key "))?;
293            ed25519::ExpandedKeypair::from_secret_key_bytes(key)
294                .ok_or(MalformedServiceKeyError::Ed25519Keypair)
295        },
296        KEYPAIR_TAG,
297        KEYPAIR_LEN
298    )
299}
300
301#[cfg(test)]
302mod tests {
303    // @@ begin test lint list maintained by maint/add_warning @@
304    #![allow(clippy::bool_assert_comparison)]
305    #![allow(clippy::clone_on_copy)]
306    #![allow(clippy::dbg_macro)]
307    #![allow(clippy::mixed_attributes_style)]
308    #![allow(clippy::print_stderr)]
309    #![allow(clippy::print_stdout)]
310    #![allow(clippy::single_char_pattern)]
311    #![allow(clippy::unwrap_used)]
312    #![allow(clippy::unchecked_duration_subtraction)]
313    #![allow(clippy::useless_vec)]
314    #![allow(clippy::needless_pass_by_value)]
315    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
316
317    use super::*;
318    use std::fs;
319    use std::str::FromStr as _;
320    use tempfile::{tempdir, TempDir};
321
322    use crate::test_utils::{assert_found, DummyKey, TestCTorSpecifier};
323    use crate::CTorServicePath;
324
325    const PUBKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_public_key");
326    const PRIVKEY: &[u8] = include_bytes!("../../../testdata/tor-service/hs_ed25519_secret_key");
327
328    #[cfg(unix)]
329    use std::os::unix::fs::PermissionsExt;
330
331    fn init_keystore(id: &str, nickname: &str) -> (CTorServiceKeystore, TempDir) {
332        let keystore_dir = tempdir().unwrap();
333
334        #[cfg(unix)]
335        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();
336
337        let id = KeystoreId::from_str(id).unwrap();
338        let nickname = HsNickname::from_str(nickname).unwrap();
339        let keystore = CTorServiceKeystore::from_path_and_mistrust(
340            &keystore_dir,
341            &Mistrust::default(),
342            id,
343            nickname,
344        )
345        .unwrap();
346
347        const KEYS: &[(&str, &[u8])] = &[
348            ("hs_ed25519_public_key", PUBKEY),
349            ("hs_ed25519_secret_key", PRIVKEY),
350        ];
351
352        for (name, key) in KEYS {
353            fs::write(keystore_dir.path().join(name), key).unwrap();
354        }
355
356        (keystore, keystore_dir)
357    }
358
359    #[test]
360    fn get() {
361        let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
362
363        let unk_nickname = HsNickname::new("acutus-cepa".into()).unwrap();
364        let path = CTorPath::Service {
365            nickname: unk_nickname.clone(),
366            path: CTorServicePath::PublicKey,
367        };
368
369        // Not found!
370        assert_found!(
371            keystore,
372            &TestCTorSpecifier(path.clone()),
373            &KeyType::Ed25519PublicKey,
374            false
375        );
376
377        // But if we use the right nickname (i.e. the one matching the keystore's nickname),
378        // the key is found.
379        let path = CTorPath::Service {
380            nickname: keystore.nickname.clone(),
381            path: CTorServicePath::PublicKey,
382        };
383        assert_found!(
384            keystore,
385            &TestCTorSpecifier(path.clone()),
386            &KeyType::Ed25519PublicKey,
387            true
388        );
389
390        let path = CTorPath::Service {
391            nickname: keystore.nickname.clone(),
392            path: CTorServicePath::PrivateKey,
393        };
394        assert_found!(
395            keystore,
396            &TestCTorSpecifier(path.clone()),
397            &KeyType::Ed25519ExpandedKeypair,
398            true
399        );
400    }
401
402    #[test]
403    fn unsupported_operation() {
404        let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
405        let path = CTorPath::Service {
406            nickname: keystore.nickname.clone(),
407            path: CTorServicePath::PublicKey,
408        };
409
410        let err = keystore
411            .remove(
412                &TestCTorSpecifier(path.clone()),
413                &KeyType::Ed25519PublicKey.into(),
414            )
415            .unwrap_err();
416
417        assert_eq!(err.to_string(), "Operation not supported: remove");
418
419        let err = keystore
420            .insert(
421                &DummyKey,
422                &TestCTorSpecifier(path.clone()),
423                &KeyType::Ed25519PublicKey.into(),
424            )
425            .unwrap_err();
426
427        assert_eq!(err.to_string(), "Operation not supported: insert");
428    }
429
430    #[test]
431    fn wrong_keytype() {
432        let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
433
434        let path = CTorPath::Service {
435            nickname: keystore.nickname.clone(),
436            path: CTorServicePath::PublicKey,
437        };
438
439        let err = keystore
440            .get(
441                &TestCTorSpecifier(path.clone()),
442                &KeyType::X25519StaticKeypair.into(),
443            )
444            .map(|_| ())
445            .unwrap_err();
446
447        assert_eq!(
448            err.to_string(),
449            "Invalid item type X25519StaticKeypair for key hs_ed25519_public_key"
450        );
451    }
452
453    #[test]
454    fn list() {
455        let (keystore, _keystore_dir) = init_keystore("foo", "allium-cepa");
456        let keys: Vec<_> = keystore.list().unwrap();
457
458        assert_eq!(keys.len(), 2);
459
460        assert!(keys
461            .iter()
462            .any(|(_, key_type)| *key_type == KeyType::Ed25519ExpandedKeypair.into()));
463
464        assert!(keys
465            .iter()
466            .any(|(_, key_type)| *key_type == KeyType::Ed25519PublicKey.into()));
467    }
468}