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