Skip to main content

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