Skip to main content

okbot_peer/
key.rs

1//! Ed25519 keys: a bot's own key (`state/peer.key`) and the public keys of members and teams.
2
3use std::fmt;
4use std::io::Write as _;
5use std::os::unix::fs::{OpenOptionsExt as _, PermissionsExt as _};
6use std::path::Path;
7use std::str::FromStr;
8use std::sync::Arc;
9
10use base64::Engine as _;
11use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
12use ring::signature::{ED25519, Ed25519KeyPair, KeyPair as _, UnparsedPublicKey};
13
14use crate::Error;
15
16/// The private key file in a bot's `state/`.
17pub const KEY_FILE: &str = "peer.key";
18/// The public key file next to it, read by other bots of the machine.
19pub const PUBLIC_FILE: &str = "peer.pub";
20
21const PREFIX: &str = "ed25519:";
22
23/// A public key, written `ed25519:<base64url>`.
24#[derive(Clone, PartialEq, Eq, Hash)]
25pub struct PublicKey([u8; 32]);
26
27impl PublicKey {
28    /// Whether `signature` is this key's signature of `message`.
29    pub fn verify(&self, message: &[u8], signature: &[u8]) -> bool {
30        UnparsedPublicKey::new(&ED25519, &self.0)
31            .verify(message, signature)
32            .is_ok()
33    }
34
35    /// A short form for people to compare (the first 8 bytes, hex).
36    pub fn fingerprint(&self) -> String {
37        hex::encode(&self.0[..8])
38    }
39}
40
41impl fmt::Display for PublicKey {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(f, "{PREFIX}{}", B64.encode(self.0))
44    }
45}
46
47impl fmt::Debug for PublicKey {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        fmt::Display::fmt(self, f)
50    }
51}
52
53impl FromStr for PublicKey {
54    type Err = Error;
55
56    fn from_str(s: &str) -> Result<Self, Self::Err> {
57        let bad = || Error::Key(format!("`{s}` is not an `ed25519:<base64url>` public key"));
58        let bytes = B64
59            .decode(s.trim().strip_prefix(PREFIX).ok_or_else(bad)?)
60            .map_err(|_| bad())?;
61        Ok(Self(bytes.try_into().map_err(|_| bad())?))
62    }
63}
64
65impl serde::Serialize for PublicKey {
66    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
67        s.collect_str(self)
68    }
69}
70
71impl<'de> serde::Deserialize<'de> for PublicKey {
72    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
73        let text = String::deserialize(d)?;
74        text.parse().map_err(serde::de::Error::custom)
75    }
76}
77
78/// A private signing key (a bot's, or a team owner's).
79#[derive(Clone)]
80pub struct SigningKey(Arc<Ed25519KeyPair>);
81
82impl fmt::Debug for SigningKey {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "SigningKey({})", self.public())
85    }
86}
87
88impl SigningKey {
89    /// A new random key and its PKCS#8 bytes (what is saved).
90    pub fn generate() -> Result<(Self, Vec<u8>), Error> {
91        let rng = ring::rand::SystemRandom::new();
92        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng)
93            .map_err(|_| Error::Key("cannot generate a key".into()))?;
94        let bytes = pkcs8.as_ref().to_vec();
95        Ok((Self::from_pkcs8(&bytes)?, bytes))
96    }
97
98    /// A key from PKCS#8 bytes.
99    pub fn from_pkcs8(bytes: &[u8]) -> Result<Self, Error> {
100        Ed25519KeyPair::from_pkcs8_maybe_unchecked(bytes)
101            .map(|k| Self(Arc::new(k)))
102            .map_err(|_| Error::Key("the key file is not an Ed25519 PKCS#8 key".into()))
103    }
104
105    /// Reads the key at `path`, or creates it (mode 0600) when there is none.
106    pub fn load_or_create(path: &Path) -> Result<Self, Error> {
107        match std::fs::read(path) {
108            Ok(bytes) => Self::from_pkcs8(&bytes),
109            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
110                let (key, bytes) = Self::generate()?;
111                write_new_private(path, &bytes)?;
112                tracing::info!(path = %path.display(), "created a peer key");
113                Ok(key)
114            }
115            Err(e) => Err(Error::Io(path.display().to_string(), e)),
116        }
117    }
118
119    /// Reads a bot's key from its `state/` directory (creating it on first use) and writes its
120    /// public key next to it, for the other bots of the machine.
121    pub fn for_bot(state_dir: &Path) -> Result<Self, Error> {
122        std::fs::create_dir_all(state_dir)
123            .map_err(|e| Error::Io(state_dir.display().to_string(), e))?;
124        let key = Self::load_or_create(&state_dir.join(KEY_FILE))?;
125        let public = state_dir.join(PUBLIC_FILE);
126        let text = format!("{}\n", key.public());
127        if std::fs::read_to_string(&public).ok().as_deref() != Some(text.as_str()) {
128            std::fs::write(&public, text)
129                .map_err(|e| Error::Io(public.display().to_string(), e))?;
130        }
131        Ok(key)
132    }
133
134    /// The public key.
135    pub fn public(&self) -> PublicKey {
136        let bytes: [u8; 32] = self
137            .0
138            .public_key()
139            .as_ref()
140            .try_into()
141            .expect("Ed25519 public keys are 32 bytes");
142        PublicKey(bytes)
143    }
144
145    /// Signs `message`.
146    pub fn sign(&self, message: &[u8]) -> Vec<u8> {
147        self.0.sign(message).as_ref().to_vec()
148    }
149}
150
151/// Reads the public key a bot directory publishes (`<dir>/state/peer.pub`).
152pub fn bot_public_key(bot_dir: &Path) -> Result<PublicKey, Error> {
153    let path = bot_dir.join("state").join(PUBLIC_FILE);
154    std::fs::read_to_string(&path)
155        .map_err(|e| Error::Io(path.display().to_string(), e))?
156        .parse()
157}
158
159/// Writes `bytes` to a new file only its owner can read.
160pub fn write_new_private(path: &Path, bytes: &[u8]) -> Result<(), Error> {
161    let io = |e| Error::Io(path.display().to_string(), e);
162    if let Some(parent) = path.parent() {
163        std::fs::create_dir_all(parent).map_err(io)?;
164    }
165    let mut file = std::fs::OpenOptions::new()
166        .write(true)
167        .create_new(true)
168        .mode(0o600)
169        .open(path)
170        .map_err(io)?;
171    file.write_all(bytes).map_err(io)?;
172    file.set_permissions(std::fs::Permissions::from_mode(0o600))
173        .map_err(io)
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn sign_and_verify() {
182        let (key, _) = SigningKey::generate().unwrap();
183        let signature = key.sign(b"hello");
184        assert!(key.public().verify(b"hello", &signature));
185        assert!(!key.public().verify(b"hullo", &signature));
186        let (other, _) = SigningKey::generate().unwrap();
187        assert!(!other.public().verify(b"hello", &signature));
188    }
189
190    #[test]
191    fn public_key_text_round_trips() {
192        let (key, _) = SigningKey::generate().unwrap();
193        let text = key.public().to_string();
194        assert!(text.starts_with("ed25519:"));
195        assert_eq!(text.parse::<PublicKey>().unwrap(), key.public());
196        assert!("ed25519:abc".parse::<PublicKey>().is_err());
197        assert!("rsa:abc".parse::<PublicKey>().is_err());
198    }
199
200    #[test]
201    fn bot_key_is_created_once_and_private() {
202        let dir = tempfile::tempdir().unwrap();
203        let state = dir.path().join("state");
204        let first = SigningKey::for_bot(&state).unwrap();
205        let again = SigningKey::for_bot(&state).unwrap();
206        assert_eq!(first.public(), again.public());
207        let mode = std::fs::metadata(state.join(KEY_FILE))
208            .unwrap()
209            .permissions()
210            .mode();
211        assert_eq!(mode & 0o777, 0o600);
212        assert_eq!(bot_public_key(dir.path()).unwrap(), first.public());
213    }
214}