radicle_node/
fingerprint.rs1use thiserror::Error;
23
24use radicle::crypto;
25use radicle::crypto::Signer as _;
26use radicle::profile::Home;
27
28#[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 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 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 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
93fn 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 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 assert_eq!(fp.verify(&other_secret), FingerprintVerification::Mismatch);
128 }
129}