1use std::fs;
4use std::io::Write;
5use std::path::Path;
6use std::sync::Arc;
7
8use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
9use zeroize::Zeroizing;
10
11use crate::canonical::random_bytes;
12use crate::error::{Error, Result};
13
14#[derive(Clone)]
17pub struct KeyPair {
18 sk: Arc<SigningKey>,
19}
20
21const OTHERS_ANY: u32 = 0o077;
23
24#[cfg(unix)]
28pub fn check_private(path: &Path, uid: u32) -> Result<()> {
29 use std::os::unix::fs::MetadataExt;
30 let meta = fs::symlink_metadata(path)?;
31 let shown = path.display();
32 if !meta.file_type().is_file() {
33 return Err(Error::Invalid(format!(
34 "key file {shown} is not a regular file (a link is not followed)"
35 )));
36 }
37 if meta.uid() != uid {
38 return Err(Error::Invalid(format!(
39 "key file {shown} belongs to uid {}, not to this process (uid {uid}); \
40 a key must be created by the account that uses it",
41 meta.uid()
42 )));
43 }
44 let mode = meta.mode() & 0o777;
45 if mode & OTHERS_ANY != 0 {
46 return Err(Error::Invalid(format!(
47 "key file {shown} has mode {mode:o}, so others may read it: run `chmod 600 {shown}` \
48 if nobody else could have, or make a new key"
49 )));
50 }
51 Ok(())
52}
53
54#[cfg(not(unix))]
55pub fn check_private(_: &Path, _: u32) -> Result<()> {
56 Ok(())
57}
58
59impl KeyPair {
60 pub fn generate() -> Self {
61 Self::from_secret(Zeroizing::new(random_bytes::<32>()))
62 }
63
64 pub fn from_secret(bytes: Zeroizing<[u8; 32]>) -> Self {
65 Self {
66 sk: Arc::new(SigningKey::from_bytes(&bytes)),
67 }
68 }
69
70 pub fn load_or_create(path: &Path, uid: u32) -> Result<Self> {
73 if path.exists() {
74 return Self::load(path, uid);
75 }
76 let key = Self::generate();
77 if let Some(dir) = path.parent() {
78 fs::create_dir_all(dir)?;
79 }
80 let mut opts = fs::OpenOptions::new();
81 opts.write(true).create_new(true);
82 #[cfg(unix)]
83 {
84 use std::os::unix::fs::OpenOptionsExt;
85 opts.mode(0o600);
86 }
87 let mut f = opts.open(path)?;
88 let text = Zeroizing::new(hex::encode(key.sk.to_bytes()));
89 f.write_all(text.as_bytes())?;
90 Ok(key)
91 }
92
93 pub fn load(path: &Path, uid: u32) -> Result<Self> {
96 check_private(path, uid)?;
97 Self::read(path)
98 }
99
100 #[cfg(unix)]
104 pub fn load_carried(path: &Path, uid: u32) -> Result<(Self, bool)> {
105 use std::os::unix::fs::MetadataExt;
106 let open = match check_private(path, uid) {
107 Ok(()) => false,
108 Err(e) => {
109 let meta = fs::symlink_metadata(path)?;
110 if !meta.file_type().is_file() || meta.uid() != uid {
111 return Err(e);
112 }
113 true
114 }
115 };
116 Ok((Self::read(path)?, open))
117 }
118
119 fn read(path: &Path) -> Result<Self> {
121 let text = Zeroizing::new(fs::read_to_string(path)?);
122 let bytes = Zeroizing::new(
123 hex::decode(text.trim()).map_err(|e| Error::Invalid(format!("key file: {e}")))?,
124 );
125 let mut arr = Zeroizing::new([0u8; 32]);
126 if bytes.len() != arr.len() {
127 return Err(Error::Invalid("key file: expected 32 bytes".into()));
128 }
129 arr.copy_from_slice(&bytes);
130 Ok(Self::from_secret(arr))
131 }
132
133 pub fn public_hex(&self) -> String {
134 hex::encode(self.sk.verifying_key().to_bytes())
135 }
136
137 pub fn sign_hex(&self, msg: &[u8]) -> String {
138 hex::encode(self.sk.sign(msg).to_bytes())
139 }
140}
141
142pub fn verify_hex(public_hex: &str, msg: &[u8], sig_hex: &str) -> bool {
143 let Ok(pk) = hex::decode(public_hex) else {
144 return false;
145 };
146 let Ok(pk): Result<[u8; 32], _> = pk.try_into() else {
147 return false;
148 };
149 let Ok(vk) = VerifyingKey::from_bytes(&pk) else {
150 return false;
151 };
152 let Ok(sig) = hex::decode(sig_hex) else {
153 return false;
154 };
155 let Ok(sig): Result<[u8; 64], _> = sig.try_into() else {
156 return false;
157 };
158 vk.verify_strict(msg, &Signature::from_bytes(&sig)).is_ok()
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn sign_and_verify() {
167 let k = KeyPair::generate();
168 let sig = k.sign_hex(b"hello");
169 assert!(verify_hex(&k.public_hex(), b"hello", &sig));
170 assert!(!verify_hex(&k.public_hex(), b"hellO", &sig));
171 assert!(!verify_hex(
172 &KeyPair::generate().public_hex(),
173 b"hello",
174 &sig
175 ));
176 }
177
178 #[test]
179 fn key_file_is_private_and_reloads() {
180 let dir = tempfile::tempdir().unwrap();
181 let p = dir.path().join("k/daemon.key");
182 let uid = owner(dir.path());
183 let a = KeyPair::load_or_create(&p, uid).unwrap();
184 let b = KeyPair::load_or_create(&p, uid).unwrap();
185 assert_eq!(a.public_hex(), b.public_hex());
186 #[cfg(unix)]
187 {
188 use std::os::unix::fs::PermissionsExt;
189 assert_eq!(
190 fs::metadata(&p).unwrap().permissions().mode() & 0o777,
191 0o600
192 );
193 }
194 }
195
196 #[cfg(unix)]
197 fn owner(p: &Path) -> u32 {
198 use std::os::unix::fs::MetadataExt;
199 fs::metadata(p).unwrap().uid()
200 }
201
202 #[cfg(unix)]
205 #[test]
206 fn a_key_file_must_be_private_to_its_user() {
207 use std::os::unix::fs::PermissionsExt;
208 let dir = tempfile::tempdir().unwrap();
209 let uid = owner(dir.path());
210 let p = dir.path().join("a.key");
211 let key = KeyPair::load_or_create(&p, uid).unwrap();
212 assert!(KeyPair::load(&p, uid).is_ok());
213
214 fs::set_permissions(&p, fs::Permissions::from_mode(0o644)).unwrap();
215 let e = KeyPair::load(&p, uid).err().unwrap().to_string();
216 assert!(e.contains("mode 644") && e.contains("chmod 600"), "{e}");
217 assert!(KeyPair::load_or_create(&p, uid).is_err());
218 let (carried, open) = KeyPair::load_carried(&p, uid).unwrap();
219 assert!(open);
220 assert_eq!(carried.public_hex(), key.public_hex());
221 fs::set_permissions(&p, fs::Permissions::from_mode(0o640)).unwrap();
222 assert!(KeyPair::load(&p, uid).is_err(), "group may read");
223 fs::set_permissions(&p, fs::Permissions::from_mode(0o602)).unwrap();
224 assert!(KeyPair::load(&p, uid).is_err(), "others may write");
225 fs::set_permissions(&p, fs::Permissions::from_mode(0o400)).unwrap();
226 assert!(KeyPair::load(&p, uid).is_ok(), "read-only to its owner");
227 let (_, open) = KeyPair::load_carried(&p, uid).unwrap();
228 assert!(!open);
229
230 let e = KeyPair::load(&p, uid + 1).err().unwrap().to_string();
231 assert!(e.contains(&format!("belongs to uid {uid}")), "{e}");
232 assert!(KeyPair::load_carried(&p, uid + 1).is_err());
233
234 let link = dir.path().join("link.key");
235 std::os::unix::fs::symlink(&p, &link).unwrap();
236 let e = KeyPair::load(&link, uid).err().unwrap().to_string();
237 assert!(e.contains("not a regular file"), "{e}");
238 assert!(KeyPair::load_carried(&link, uid).is_err());
239 }
240
241 #[test]
242 fn clones_share_one_secret() {
243 let a = KeyPair::generate();
244 let b = a.clone();
245 assert!(Arc::ptr_eq(&a.sk, &b.sk));
246 assert_eq!(a.sign_hex(b"x"), b.sign_hex(b"x"));
247 }
248
249 #[test]
250 fn a_key_file_holds_32_bytes_of_hex() {
251 let dir = tempfile::tempdir().unwrap();
252 let p = dir.path().join("short.key");
253 fs::write(&p, "abcd").unwrap();
254 assert!(
255 KeyPair::read(&p)
256 .err()
257 .unwrap()
258 .to_string()
259 .contains("32 bytes")
260 );
261 fs::write(&p, "zz").unwrap();
262 assert!(KeyPair::read(&p).is_err());
263 fs::write(&p, format!("{}\n", "11".repeat(32))).unwrap();
264 assert!(KeyPair::read(&p).is_ok());
265 }
266}