radicle_node/
fingerprint.rs

1//! Fingerprint the public key corresponding to the secret key used by
2//! `radicle-node`.
3//!
4//! This allows users to configure the path to the secret key
5//! freely, while ensuring that the key is not changed.
6//!
7//! In order to achieve this, the fingerprint of the public key
8//! derived from the secret key is stored in the Radicle home
9//! in a file (usually at `.radicle/node/fingerprint`).
10//! When the node starts up and this file does not exist, it is assumed that
11//! this is the first time the node is started, and the fingerprint is
12//! initialized from the secret key in the keystore.
13//! On subsequent startups, the fingerprint of the public key
14//! derived from the secret key in the keystore is compared to the
15//! fingerprint stored on disk, and if they do not match, the node
16//! refuses to start (this last part is implemented in `main.rs`).
17//!
18//! If the user deletes the fingerprint file, the node will not be able
19//! to detect a possible change of the secret key. The consequences of
20//! doing this are unclear.
21
22use thiserror::Error;
23
24use radicle::crypto;
25use radicle::profile::Home;
26
27/// Fingerprint of a public key.
28#[derive(Debug, PartialEq)]
29pub struct Fingerprint(String);
30
31impl std::fmt::Display for Fingerprint {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        write!(f, "{}", self.0)
34    }
35}
36
37#[derive(Debug, PartialEq, Eq)]
38pub enum FingerprintVerification {
39    Match,
40    Mismatch,
41}
42
43#[derive(Error, Debug)]
44pub enum Error {
45    #[error(transparent)]
46    Io(#[from] std::io::Error),
47
48    #[error("fingerprint file is not valid UTF-8: {0}")]
49    Utf8(#[from] std::str::Utf8Error),
50}
51
52impl Fingerprint {
53    /// Return fingerprint of the node, if it exists.
54    pub fn read(home: &Home) -> Result<Option<Fingerprint>, Error> {
55        match std::fs::read(path(home)) {
56            Ok(contents) => Ok(Some(Fingerprint(
57                String::from(std::str::from_utf8(contents.as_ref())?)
58                    .trim_end()
59                    .to_string(),
60            ))),
61            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
62            Err(err) => Err(Error::Io(err)),
63        }
64    }
65
66    /// Initialize the fingerprint of the node with given public key.
67    pub fn init(
68        home: &Home,
69        secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
70    ) -> Result<(), Error> {
71        let public_key = crypto::PublicKey(secret_key.deref().public_key());
72        let mut file = std::fs::OpenOptions::new()
73            .create_new(true)
74            .write(true)
75            .open(path(home))?;
76        {
77            use std::io::Write as _;
78            file.write_all(crypto::ssh::fmt::fingerprint(&public_key).as_ref())?;
79        }
80
81        Ok(())
82    }
83
84    /// Verify that the fingerprint of given public key matches self.
85    pub fn verify(
86        &self,
87        secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
88    ) -> FingerprintVerification {
89        let public_key = crypto::PublicKey(secret_key.deref().public_key());
90        if crypto::ssh::fmt::fingerprint(&public_key) == self.0 {
91            FingerprintVerification::Match
92        } else {
93            FingerprintVerification::Mismatch
94        }
95    }
96}
97
98/// Return the location of the node fingerprint.
99fn path(home: &Home) -> std::path::PathBuf {
100    home.node().join("fingerprint")
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    use crypto::ssh::Keystore;
108
109    #[test]
110    fn matching() {
111        let tmp = tempfile::tempdir().unwrap();
112        let home = Home::new(tmp.path()).unwrap();
113
114        let store = Keystore::new(&home.keys());
115        store.init("test 1", None, crypto::Seed::default()).unwrap();
116        let secret = store.secret_key(None).unwrap().unwrap();
117
118        assert_eq!(Fingerprint::read(&home).unwrap(), None);
119        Fingerprint::init(&home, &secret).unwrap();
120
121        let fp = Fingerprint::read(&home).unwrap().unwrap();
122        assert_eq!(fp.verify(&secret), FingerprintVerification::Match);
123
124        // Generate a new keypair, which does not match the fingerprint.
125        // This simulates the user modifying `~/.radicle/keys`.
126        std::fs::remove_dir_all(home.keys()).unwrap();
127        store.init("test 1", None, crypto::Seed::default()).unwrap();
128        let other_secret = store.secret_key(None).unwrap().unwrap();
129
130        assert_ne!(secret, other_secret);
131        // Note that `fp` has not changed since it was initialiazed from `secret`.
132        assert_eq!(fp.verify(&other_secret), FingerprintVerification::Mismatch);
133    }
134}