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