Skip to main content

mj_controller/server/api/
token.rs

1use super::*;
2
3/// Where the bearer token lives. It is a file rather than an environment
4/// variable so it survives daemon restarts and so deleting it is the explicit
5/// revoke gesture.
6pub fn api_token_path() -> PathBuf {
7    mj_core::config::data_dir().join(API_TOKEN_FILE)
8}
9
10/// Read the API bearer token, minting one on first use.
11///
12/// A missing file is ordinary first use. An unreadable or too-short one is
13/// replaced loudly: refusing to start the daemon over a damaged token file
14/// would be a worse answer than asking the caller to re-read the file.
15pub fn load_or_create_api_token(path: &std::path::Path) -> AnyResult<String> {
16    match std::fs::read_to_string(path) {
17        Ok(token) if token.trim().len() >= 32 => return Ok(token.trim().to_owned()),
18        Ok(token) => tracing::warn!(
19            path = %path.display(),
20            bytes = token.trim().len(),
21            "Mjolnir API token is too short; generating a new one revokes the old token"
22        ),
23        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
24        Err(error) => tracing::warn!(
25            path = %path.display(),
26            "could not read the Mjolnir API token ({error}); generating a new one revokes the old token"
27        ),
28    }
29    let mut bytes = [0_u8; API_TOKEN_BYTES];
30    getrandom::fill(&mut bytes)
31        .map_err(|error| anyhow::anyhow!("generate Mjolnir API token: {error}"))?;
32    let token = mj_core::hex::lower_hex(bytes);
33    mj_core::config::atomic_write(path, token.as_bytes())
34        .with_context(|| format!("persist Mjolnir API token {}", path.display()))?;
35    Ok(token)
36}
37
38/// The pin a local API client checks the viewer's certificate against:
39/// lowercase hex SHA-256 of the certificate's DER bytes.
40pub fn certificate_der_sha256(der: &[u8]) -> String {
41    use sha2::Digest;
42    mj_core::hex::lower_hex(sha2::Sha256::digest(der))
43}
44
45/// Pin the first (leaf) certificate of a PEM chain the viewer serves.
46///
47/// The daemon publishes this with the viewer URL so the CLI can trust exactly
48/// the certificate the operator configured, including a self-signed one that
49/// no CA chain, and no `CA:FALSE` rule, would accept.
50pub fn served_certificate_sha256(pem: &[u8]) -> AnyResult<String> {
51    use rustls::pki_types::CertificateDer;
52    use rustls::pki_types::pem::PemObject;
53    let leaf = CertificateDer::pem_slice_iter(pem)
54        .next()
55        .context("the certificate file holds no PEM certificate")?
56        .context("parse the PEM certificate")?;
57    Ok(certificate_der_sha256(&leaf))
58}
59
60// ---------------------------------------------------------------------------
61// Failures
62// ---------------------------------------------------------------------------