Skip to main content

dspk_stream/transport/
auth.rs

1//! Who may connect to a server: client certificates the server issues itself.
2//!
3//! A server keeps an [`Authority`] in a folder of its own (mode 0700): a certificate authority
4//! (CA) key (mode 0600) and certificate, and a registry of the clients it enrolled
5//! (`clients.json`: name, permission, certificate fingerprint, issued, revoked). Enrolling a
6//! client issues it a certificate and key signed by the CA, written with the CA certificate into
7//! one file for that client ([`Enrollment`], mode 0600). The client presents its certificate in
8//! the TLS handshake (mutual TLS) and trusts the server through the same CA, so nothing else has
9//! to be handed out.
10//!
11//! The handshake only accepts certificates signed by the CA; the registry then decides: a
12//! certificate whose fingerprint is not enrolled, or is revoked, is refused. The registry is read
13//! at every connection, so revoking takes effect for the next connection without a restart.
14//! Each client has a [`Permission`]: `read` (recordings, views) or `control` (also configure and
15//! run jobs).
16
17use 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
29/// The CA certificate (PEM), what clients trust.
30pub const CA_CERT_FILE: &str = "ca.pem";
31/// The CA key (PEM, mode 0600): whoever has it can issue clients.
32pub const CA_KEY_FILE: &str = "ca.key.pem";
33/// The enrolled clients.
34pub const CLIENTS_FILE: &str = "clients.json";
35/// Years a client certificate is valid; then it is enrolled again.
36pub const CLIENT_YEARS: i32 = 2;
37/// Years the CA is valid.
38pub const CA_YEARS: i32 = 20;
39/// Years a server certificate is valid (it is issued again at every start).
40pub const SERVER_YEARS: i32 = 1;
41/// Longest client name.
42pub const MAX_NAME_CHARS: usize = 64;
43/// The name of a client without a certificate (only with anonymous access allowed).
44pub 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/// What a client may do.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "lowercase")]
52pub enum Permission {
53    /// Recordings and views.
54    Read,
55    /// Also configure, start and stop jobs, and write.
56    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/// The client on the other end of a connection.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct Peer {
71    pub name: String,
72    pub permission: Permission,
73}
74
75impl Peer {
76    /// A client without a certificate: read only.
77    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/// One enrolled client.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct ClientRecord {
89    pub name: String,
90    pub permission: Permission,
91    /// SHA-256 of its certificate (DER), hex.
92    pub fingerprint: String,
93    pub issued_unix: u64,
94    /// Its certificate is valid until the start of this year.
95    pub expires_year: i32,
96    /// When it was revoked; `None` while active.
97    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
111/// A client's certificate, key and the CA, as one PEM file to give to that client.
112pub struct Enrollment {
113    pub record: ClientRecord,
114    pub pem: String,
115}
116
117impl Enrollment {
118    /// Writes the file (mode 0600); refuses to overwrite one that exists.
119    pub fn write(&self, path: &Path) -> StreamResult<()> {
120        write_private(path, self.pem.as_bytes(), true)
121    }
122}
123
124/// A client's identity read from its enrollment file: what it presents, and the CA it trusts.
125#[derive(Clone)]
126pub struct ClientIdentity {
127    /// Its certificate (DER).
128    pub certificate: Vec<u8>,
129    /// Its private key (PKCS #8, DER).
130    pub private_key: Vec<u8>,
131    /// The certificate authority of the server(s) that enrolled it (DER).
132    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        // Never print the key
138        f.debug_struct("ClientIdentity").field("fingerprint", &fingerprint(&self.certificate)).finish_non_exhaustive()
139    }
140}
141
142impl ClientIdentity {
143    /// Reads an enrollment file: its first certificate is the client's, the others the CA's.
144    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
161/// A server's certificate authority and its registry of clients, in `dir`.
162pub 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    /// Opens the authority in `dir`, creating it (a new CA) when the folder holds none. A folder
177    /// with only one of the CA's two files is an error, never overwritten.
178    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    /// The CA certificate (DER): what clients trust.
209    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    /// A new server certificate for `names` (host names or addresses clients use), signed by the
218    /// CA, and its key; the chain ends with the CA certificate.
219    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    /// Issues `name` a client certificate with `permission` and records it. A name already
233    /// enrolled and not revoked is refused (revoke it first).
234    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(&registry)?;
252        let pem = format!("{}{}{}", cert.pem(), key.serialize_pem(), self.ca_pem);
253        Ok(Enrollment { record, pem })
254    }
255
256    /// Revokes `name`'s certificate: refused from its next connection on.
257    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(&registry)?;
265        Ok(revoked)
266    }
267
268    /// Every client ever enrolled, revoked ones included.
269    pub fn clients(&self) -> StreamResult<Vec<ClientRecord>> {
270        Ok(self.registry()?.clients)
271    }
272
273    /// Who presents `certificate` (DER): an active enrolled client, else `None`.
274    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    /// Writes the registry whole and renames it into place, so a reader never sees half a file.
289    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
299/// The CA's parameters. Issuing rebuilds them from here, so they must not depend on anything
300/// but constants (the issued certificates name this issuer).
301fn 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
315/// Letters, digits, `.`, `_`, `-`; 1 to [`MAX_NAME_CHARS`] characters (names become file names).
316fn 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
321/// SHA-256 of `der`, lowercase hex.
322pub 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
330/// The current year, close enough for validity periods (they start a year early and end at a
331/// year's start, so a day's error never makes a certificate not yet valid).
332fn 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
340/// Writes `bytes` to a new file readable only by its owner (0600 on Unix); `create_new` refuses
341/// an existing file.
342fn 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
361/// Sets `path`'s permission bits (Unix; elsewhere nothing).
362fn 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        // The same authority after a restart: same CA, same clients
394        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        // Half an authority is refused, not replaced
427        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}