1use std::io::Write as _;
18use std::path::{Path, PathBuf};
19use std::time::{SystemTime, UNIX_EPOCH};
20
21use rcgen::{date_time_ymd, BasicConstraints, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair, KeyUsagePurpose};
22use rustls::pki_types::pem::PemObject;
23use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
24use serde::{Deserialize, Serialize};
25use sha2::{Digest, Sha256};
26
27use crate::error::{StreamError, StreamResult};
28
29pub const CA_CERT_FILE: &str = "ca.pem";
31pub const CA_KEY_FILE: &str = "ca.key.pem";
33pub const CLIENTS_FILE: &str = "clients.json";
35pub const CLIENT_YEARS: i32 = 2;
37pub const CA_YEARS: i32 = 20;
39pub const SERVER_YEARS: i32 = 1;
41pub const MAX_NAME_CHARS: usize = 64;
43pub const ANONYMOUS: &str = "anonymous";
45const CA_NAME: &str = "dspk serve authority";
46const SERVER_NAME: &str = "dspk serve";
47const SECS_PER_YEAR: u64 = 31_556_952;
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum Permission {
53 Read,
55 Control,
57}
58
59impl std::fmt::Display for Permission {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.write_str(match self {
62 Self::Read => "read",
63 Self::Control => "control",
64 })
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct Peer {
71 pub name: String,
72 pub permission: Permission,
73}
74
75impl Peer {
76 pub fn anonymous() -> Self {
78 Self { name: ANONYMOUS.into(), permission: Permission::Read }
79 }
80
81 pub fn can_control(&self) -> bool {
82 self.permission == Permission::Control
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct ClientRecord {
89 pub name: String,
90 pub permission: Permission,
91 pub fingerprint: String,
93 pub issued_unix: u64,
94 pub expires_year: i32,
96 pub revoked_unix: Option<u64>,
98}
99
100impl ClientRecord {
101 pub fn active(&self) -> bool {
102 self.revoked_unix.is_none()
103 }
104}
105
106#[derive(Debug, Default, Serialize, Deserialize)]
107struct Registry {
108 clients: Vec<ClientRecord>,
109}
110
111pub struct Enrollment {
113 pub record: ClientRecord,
114 pub pem: String,
115}
116
117impl Enrollment {
118 pub fn write(&self, path: &Path) -> StreamResult<()> {
120 write_private(path, self.pem.as_bytes(), true)
121 }
122}
123
124#[derive(Clone)]
126pub struct ClientIdentity {
127 pub certificate: Vec<u8>,
129 pub private_key: Vec<u8>,
131 pub authorities: Vec<Vec<u8>>,
133}
134
135impl std::fmt::Debug for ClientIdentity {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 f.debug_struct("ClientIdentity").field("fingerprint", &fingerprint(&self.certificate)).finish_non_exhaustive()
139 }
140}
141
142impl ClientIdentity {
143 pub fn from_file(path: &Path) -> StreamResult<Self> {
145 let bytes = std::fs::read(path)?;
146 Self::from_pem(&bytes)
147 }
148
149 pub fn from_pem(bytes: &[u8]) -> StreamResult<Self> {
150 let certs: Vec<Vec<u8>> = CertificateDer::pem_slice_iter(bytes).filter_map(Result::ok).map(|c| c.to_vec()).collect();
151 let (certificate, authorities) = certs.split_first().ok_or_else(|| StreamError::Tls("no certificate in the identity file".into()))?;
152 if authorities.is_empty() {
153 return Err(StreamError::Tls("no authority (CA) certificate in the identity file".into()));
154 }
155 let key = PrivateKeyDer::from_pem_slice(bytes).map_err(|e| StreamError::Tls(format!("no private key in the identity file: {e}")))?;
156 let PrivateKeyDer::Pkcs8(key) = key else { return Err(StreamError::Tls("the identity's key is not PKCS #8".into())) };
157 Ok(Self { certificate: certificate.clone(), private_key: key.secret_pkcs8_der().to_vec(), authorities: authorities.to_vec() })
158 }
159}
160
161pub struct Authority {
163 dir: PathBuf,
164 ca_pem: String,
165 ca_der: CertificateDer<'static>,
166 ca_key: KeyPair,
167}
168
169impl std::fmt::Debug for Authority {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct("Authority").field("dir", &self.dir).finish_non_exhaustive()
172 }
173}
174
175impl Authority {
176 pub fn open_or_create(dir: &Path) -> StreamResult<Self> {
179 std::fs::create_dir_all(dir)?;
180 restrict(dir, 0o700)?;
181 let (cert_path, key_path) = (dir.join(CA_CERT_FILE), dir.join(CA_KEY_FILE));
182 match (cert_path.exists(), key_path.exists()) {
183 (true, true) => {
184 let ca_pem = std::fs::read_to_string(&cert_path)?;
185 let ca_key = KeyPair::from_pem(&std::fs::read_to_string(&key_path)?).map_err(tls)?;
186 let ca_der = CertificateDer::from_pem_slice(ca_pem.as_bytes()).map_err(|e| StreamError::Tls(format!("{}: {e}", cert_path.display())))?.into_owned();
187 Ok(Self { dir: dir.to_path_buf(), ca_pem, ca_der, ca_key })
188 }
189 (false, false) => {
190 let ca_key = KeyPair::generate().map_err(tls)?;
191 let mut params = ca_params();
192 let year = this_year();
193 (params.not_before, params.not_after) = (date_time_ymd(year - 1, 1, 1), date_time_ymd(year + CA_YEARS, 1, 1));
194 let cert = params.self_signed(&ca_key).map_err(tls)?;
195 write_private(&key_path, ca_key.serialize_pem().as_bytes(), true)?;
196 let ca_pem = cert.pem();
197 std::fs::write(&cert_path, &ca_pem)?;
198 Ok(Self { dir: dir.to_path_buf(), ca_pem, ca_der: cert.der().clone(), ca_key })
199 }
200 _ => Err(StreamError::Tls(format!("{} holds only one of {CA_CERT_FILE} and {CA_KEY_FILE}: restore the other or remove both", dir.display()))),
201 }
202 }
203
204 pub fn dir(&self) -> &Path {
205 &self.dir
206 }
207
208 pub fn ca_certificate(&self) -> &[u8] {
210 &self.ca_der
211 }
212
213 fn issuer(&self) -> Issuer<'static, &KeyPair> {
214 Issuer::new(ca_params(), &self.ca_key)
215 }
216
217 pub fn server_certificate(&self, names: &[String]) -> StreamResult<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
220 let key = KeyPair::generate().map_err(tls)?;
221 let mut params = CertificateParams::new(names.to_vec()).map_err(tls)?;
222 params.distinguished_name = common_name(SERVER_NAME);
223 params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
224 params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
225 let year = this_year();
226 (params.not_before, params.not_after) = (date_time_ymd(year - 1, 1, 1), date_time_ymd(year + SERVER_YEARS + 1, 1, 1));
227 let cert = params.signed_by(&key, &self.issuer()).map_err(tls)?;
228 let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.serialize_der()));
229 Ok((vec![cert.der().clone(), self.ca_der.clone()], key))
230 }
231
232 pub fn enroll(&self, name: &str, permission: Permission) -> StreamResult<Enrollment> {
235 check_name(name)?;
236 let mut registry = self.registry()?;
237 if registry.clients.iter().any(|c| c.name == name && c.active()) {
238 return Err(StreamError::Tls(format!("{name} is already enrolled: revoke it first to issue a new certificate")));
239 }
240 let key = KeyPair::generate().map_err(tls)?;
241 let mut params = CertificateParams::new(Vec::<String>::new()).map_err(tls)?;
242 params.distinguished_name = common_name(name);
243 params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
244 params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
245 let year = this_year();
246 let expires_year = year + CLIENT_YEARS + 1;
247 (params.not_before, params.not_after) = (date_time_ymd(year - 1, 1, 1), date_time_ymd(expires_year, 1, 1));
248 let cert = params.signed_by(&key, &self.issuer()).map_err(tls)?;
249 let record = ClientRecord { name: name.into(), permission, fingerprint: fingerprint(cert.der()), issued_unix: now_unix(), expires_year, revoked_unix: None };
250 registry.clients.push(record.clone());
251 self.save(®istry)?;
252 let pem = format!("{}{}{}", cert.pem(), key.serialize_pem(), self.ca_pem);
253 Ok(Enrollment { record, pem })
254 }
255
256 pub fn revoke(&self, name: &str) -> StreamResult<ClientRecord> {
258 let mut registry = self.registry()?;
259 let Some(record) = registry.clients.iter_mut().find(|c| c.name == name && c.active()) else {
260 return Err(StreamError::Tls(format!("no active client named {name}")));
261 };
262 record.revoked_unix = Some(now_unix());
263 let revoked = record.clone();
264 self.save(®istry)?;
265 Ok(revoked)
266 }
267
268 pub fn clients(&self) -> StreamResult<Vec<ClientRecord>> {
270 Ok(self.registry()?.clients)
271 }
272
273 pub fn authorize(&self, certificate: &[u8]) -> Option<Peer> {
275 let fp = fingerprint(certificate);
276 let registry = self.registry().map_err(|e| tracing::warn!("reading the client registry: {e}")).ok()?;
277 registry.clients.into_iter().find(|c| c.fingerprint == fp && c.active()).map(|c| Peer { name: c.name, permission: c.permission })
278 }
279
280 fn registry(&self) -> StreamResult<Registry> {
281 match std::fs::read(self.dir.join(CLIENTS_FILE)) {
282 Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| StreamError::Tls(format!("{CLIENTS_FILE}: {e}"))),
283 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Registry::default()),
284 Err(e) => Err(e.into()),
285 }
286 }
287
288 fn save(&self, registry: &Registry) -> StreamResult<()> {
290 let json = serde_json::to_vec_pretty(registry).map_err(|e| StreamError::Tls(e.to_string()))?;
291 let tmp = self.dir.join(format!("{CLIENTS_FILE}.tmp"));
292 let _ = std::fs::remove_file(&tmp);
293 write_private(&tmp, &json, true)?;
294 std::fs::rename(&tmp, self.dir.join(CLIENTS_FILE))?;
295 Ok(())
296 }
297}
298
299fn ca_params() -> CertificateParams {
302 let mut p = CertificateParams::default();
303 p.distinguished_name = common_name(CA_NAME);
304 p.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
305 p.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign, KeyUsagePurpose::DigitalSignature];
306 p
307}
308
309fn common_name(name: &str) -> DistinguishedName {
310 let mut dn = DistinguishedName::new();
311 dn.push(DnType::CommonName, name);
312 dn
313}
314
315fn check_name(name: &str) -> StreamResult<()> {
317 let ok = !name.is_empty() && name.len() <= MAX_NAME_CHARS && name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')) && !name.starts_with('.');
318 if ok { Ok(()) } else { Err(StreamError::Tls(format!("client name {name:?}: use 1–{MAX_NAME_CHARS} letters, digits, '.', '_' or '-', not starting with '.'"))) }
319}
320
321pub fn fingerprint(der: &[u8]) -> String {
323 Sha256::digest(der).iter().map(|b| format!("{b:02x}")).collect()
324}
325
326fn now_unix() -> u64 {
327 SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs())
328}
329
330fn this_year() -> i32 {
333 1970 + (now_unix() / SECS_PER_YEAR) as i32
334}
335
336fn tls(e: impl std::fmt::Display) -> StreamError {
337 StreamError::Tls(e.to_string())
338}
339
340fn write_private(path: &Path, bytes: &[u8], create_new: bool) -> StreamResult<()> {
343 let mut options = std::fs::OpenOptions::new();
344 options.write(true);
345 if create_new {
346 options.create_new(true);
347 } else {
348 options.create(true).truncate(true);
349 }
350 #[cfg(unix)]
351 {
352 use std::os::unix::fs::OpenOptionsExt as _;
353 options.mode(0o600);
354 }
355 let mut file = options.open(path).map_err(|e| std::io::Error::new(e.kind(), format!("{}: {e}", path.display())))?;
356 file.write_all(bytes)?;
357 file.sync_all()?;
358 Ok(())
359}
360
361fn restrict(path: &Path, mode: u32) -> StreamResult<()> {
363 #[cfg(unix)]
364 {
365 use std::os::unix::fs::PermissionsExt as _;
366 std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))?;
367 }
368 #[cfg(not(unix))]
369 let _ = (path, mode);
370 Ok(())
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 fn scratch(name: &str) -> PathBuf {
378 let dir = std::env::temp_dir().join(format!("dspk-stream-auth-{name}-{}", std::process::id()));
379 let _ = std::fs::remove_dir_all(&dir);
380 dir
381 }
382
383 #[test]
384 fn enroll_authorize_revoke() {
385 let dir = scratch("cycle");
386 let auth = Authority::open_or_create(&dir).unwrap();
387 let alice = auth.enroll("alice", Permission::Control).unwrap();
388 let id = ClientIdentity::from_pem(alice.pem.as_bytes()).unwrap();
389 assert_eq!(id.authorities, vec![auth.ca_certificate().to_vec()]);
390 assert_eq!(auth.authorize(&id.certificate), Some(Peer { name: "alice".into(), permission: Permission::Control }));
391 assert!(auth.enroll("alice", Permission::Read).is_err(), "an active name is not issued twice");
392
393 let again = Authority::open_or_create(&dir).unwrap();
395 assert_eq!(again.ca_certificate(), auth.ca_certificate());
396 assert!(again.authorize(&id.certificate).is_some());
397
398 auth.revoke("alice").unwrap();
399 assert_eq!(again.authorize(&id.certificate), None, "revoked: refused, read fresh");
400 assert!(auth.enroll("alice", Permission::Read).is_ok(), "a revoked name can be enrolled again");
401 assert_eq!(auth.clients().unwrap().len(), 2);
402 let _ = std::fs::remove_dir_all(&dir);
403 }
404
405 #[test]
406 fn names_files_and_permissions() {
407 for bad in ["", ".hidden", "a/b", "../x", "a b", &"x".repeat(MAX_NAME_CHARS + 1)] {
408 assert!(check_name(bad).is_err(), "{bad:?}");
409 }
410 assert!(check_name("lab-pc_2.local").is_ok());
411 let dir = scratch("files");
412 let auth = Authority::open_or_create(&dir).unwrap();
413 let e = auth.enroll("bob", Permission::Read).unwrap();
414 let out = dir.join("bob.pem");
415 e.write(&out).unwrap();
416 assert!(e.write(&out).is_err(), "never overwritten");
417 #[cfg(unix)]
418 {
419 use std::os::unix::fs::PermissionsExt as _;
420 let mode = |p: &Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777;
421 assert_eq!(mode(&dir), 0o700);
422 assert_eq!(mode(&dir.join(CA_KEY_FILE)), 0o600);
423 assert_eq!(mode(&out), 0o600);
424 assert_eq!(mode(&dir.join(CLIENTS_FILE)), 0o600);
425 }
426 std::fs::remove_file(dir.join(CA_KEY_FILE)).unwrap();
428 assert!(Authority::open_or_create(&dir).is_err());
429 let _ = std::fs::remove_dir_all(&dir);
430 }
431}