Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle-node src fingerprint.rs
//! Fingerprint the public key corresponding to the secret key used by
//! `radicle-node`.
//!
//! This allows users to configure the path to the secret key
//! freely, while ensuring that the key is not changed.
//!
//! In order to achieve this, the fingerprint of the public key
//! derived from the secret key is stored in the Radicle home
//! in a file (usually at `.radicle/node/fingerprint`).
//! When the node starts up and this file does not exist, it is assumed that
//! this is the first time the node is started, and the fingerprint is
//! initialized from the secret key in the keystore.
//! On subsequent startups, the fingerprint of the public key
//! derived from the secret key in the keystore is compared to the
//! fingerprint stored on disk, and if they do not match, the node
//! refuses to start (this last part is implemented in `main.rs`).
//!
//! If the user deletes the fingerprint file, the node will not be able
//! to detect a possible change of the secret key. The consequences of
//! doing this are unclear.

use thiserror::Error;

use radicle::crypto;
use radicle::profile::Home;

/// Fingerprint of a public key.
#[derive(Debug, PartialEq)]
pub struct Fingerprint(String);

impl std::fmt::Display for Fingerprint {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum FingerprintVerification {
    Match,
    Mismatch,
}

#[derive(Error, Debug)]
pub enum Error {
    #[error(transparent)]
    Io(#[from] std::io::Error),

    #[error("fingerprint file is not valid UTF-8: {0}")]
    Utf8(#[from] std::str::Utf8Error),
}

impl Fingerprint {
    /// Return fingerprint of the node, if it exists.
    pub fn read(home: &Home) -> Result<Option<Fingerprint>, Error> {
        match std::fs::read(path(home)) {
            Ok(contents) => Ok(Some(Fingerprint(
                String::from(std::str::from_utf8(contents.as_ref())?)
                    .trim_end()
                    .to_string(),
            ))),
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
            Err(err) => Err(Error::Io(err)),
        }
    }

    /// Initialize the fingerprint of the node with given public key.
    pub fn init(
        home: &Home,
        secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
    ) -> Result<(), Error> {
        let public_key = secret_key.deref().public_key().into();
        let mut file = std::fs::OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(path(home))?;
        {
            use std::io::Write as _;
            file.write_all(crypto::ssh::fmt::fingerprint(&public_key).as_ref())?;
        }

        Ok(())
    }

    /// Verify that the fingerprint of given public key matches self.
    pub fn verify(
        &self,
        secret_key: &impl std::ops::Deref<Target = crypto::SecretKey>,
    ) -> FingerprintVerification {
        let public_key = secret_key.deref().public_key().into();
        if crypto::ssh::fmt::fingerprint(&public_key) == self.0 {
            FingerprintVerification::Match
        } else {
            FingerprintVerification::Mismatch
        }
    }
}

/// Return the location of the node fingerprint.
fn path(home: &Home) -> std::path::PathBuf {
    home.node().join("fingerprint")
}

#[cfg(test)]
mod tests {
    use super::*;

    use crypto::ssh::Keystore;

    #[test]
    fn matching() {
        let tmp = tempfile::tempdir().unwrap();
        let home = Home::new(tmp.path()).unwrap();

        let store = Keystore::new(&home.keys());
        store.init("test 1", None, crypto::Seed::default()).unwrap();
        let secret = store.secret_key(None).unwrap().unwrap();

        assert_eq!(Fingerprint::read(&home).unwrap(), None);
        Fingerprint::init(&home, &secret).unwrap();

        let fp = Fingerprint::read(&home).unwrap().unwrap();
        assert_eq!(fp.verify(&secret), FingerprintVerification::Match);

        // Generate a new keypair, which does not match the fingerprint.
        // This simulates the user modifying `~/.radicle/keys`.
        std::fs::remove_dir_all(home.keys()).unwrap();
        store.init("test 1", None, crypto::Seed::default()).unwrap();
        let other_secret = store.secret_key(None).unwrap().unwrap();

        assert_ne!(secret, other_secret);
        // Note that `fp` has not changed since it was initialized from `secret`.
        assert_eq!(fp.verify(&other_secret), FingerprintVerification::Mismatch);
    }
}