1use 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
16pub const KEY_FILE: &str = "peer.key";
18pub const PUBLIC_FILE: &str = "peer.pub";
20
21const PREFIX: &str = "ed25519:";
22
23#[derive(Clone, PartialEq, Eq, Hash)]
25pub struct PublicKey([u8; 32]);
26
27impl PublicKey {
28 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 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#[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 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 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 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 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 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 pub fn sign(&self, message: &[u8]) -> Vec<u8> {
147 self.0.sign(message).as_ref().to_vec()
148 }
149}
150
151pub 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
159pub 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}