Skip to main content

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::crypto::Signer as _;
26use radicle::profile::Home;
27
28/// Fingerprint of a public key.
29#[derive(Debug, PartialEq)]
30pub struct Fingerprint(String);
31
32impl std::fmt::Display for Fingerprint {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        write!(f, "{}", self.0)
35    }
36}
37
38#[derive(Debug, PartialEq, Eq)]
39pub enum FingerprintVerification {
40    Match,
41    Mismatch,
42}
43
44#[derive(Error, Debug)]
45pub enum Error {
46    #[error(transparent)]
47    Io(#[from] std::io::Error),
48
49    #[error("fingerprint file is not valid UTF-8: {0}")]
50    Utf8(#[from] std::str::Utf8Error),
51}
52
53impl Fingerprint {
54    /// Return fingerprint of the node, if it exists.
55    pub fn read(home: &Home) -> Result<Option<Fingerprint>, Error> {
56        match std::fs::read(path(home)) {
57            Ok(contents) => Ok(Some(Fingerprint(
58                String::from(std::str::from_utf8(contents.as_ref())?)
59                    .trim_end()
60                    .to_string(),
61            ))),
62            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
63            Err(err) => Err(Error::Io(err)),
64        }
65    }
66
67    /// Initialize the fingerprint of the node with given public key.
68    pub fn init(home: &Home, secret_key: &crypto::SigningKey) -> Result<(), Error> {
69        let public_key = secret_key.public_key();
70        let mut file = std::fs::OpenOptions::new()
71            .create_new(true)
72            .write(true)
73            .open(path(home))?;
74        {
75            use std::io::Write as _;
76            file.write_all(crypto::ssh::fmt::fingerprint(public_key).as_ref())?;
77        }
78
79        Ok(())
80    }
81
82    /// Verify that the fingerprint of given public key matches self.
83    pub fn verify(&self, secret_key: &crypto::SigningKey) -> FingerprintVerification {
84        let public_key = secret_key.public_key();
85        if crypto::ssh::fmt::fingerprint(public_key) == self.0 {
86            FingerprintVerification::Match
87        } else {
88            FingerprintVerification::Mismatch
89        }
90    }
91}
92
93/// Return the location of the node fingerprint.
94fn path(home: &Home) -> std::path::PathBuf {
95    home.node().join("fingerprint")
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    use crypto::{Seed, ssh::Keystore};
103
104    #[test]
105    fn matching() {
106        let tmp = tempfile::tempdir().unwrap();
107        let home = Home::new(tmp.path()).unwrap();
108
109        let store = Keystore::new(&home.keys());
110        store.init("test 1", None, Seed::mock(1)).unwrap();
111        let secret = store.secret_key(None).unwrap().unwrap();
112
113        assert_eq!(Fingerprint::read(&home).unwrap(), None);
114        Fingerprint::init(&home, &secret).unwrap();
115
116        let fp = Fingerprint::read(&home).unwrap().unwrap();
117        assert_eq!(fp.verify(&secret), FingerprintVerification::Match);
118
119        // Generate a new keypair, which does not match the fingerprint.
120        // This simulates the user modifying `~/.radicle/keys`.
121        std::fs::remove_dir_all(home.keys()).unwrap();
122        store.init("test 1", None, Seed::mock(2)).unwrap();
123        let other_secret = store.secret_key(None).unwrap().unwrap();
124
125        assert_ne!(secret, other_secret);
126        // Note that `fp` has not changed since it was initialized from `secret`.
127        assert_eq!(fp.verify(&other_secret), FingerprintVerification::Mismatch);
128    }
129}