Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/cli/src/time_cli.rs

//! **`holger time-token`** (lane HOL-NCSC, NCSC P3): the signed offline time token and the
//! box's time floor (`server_lib::time_floor`).
//!
//! * `make` (the online side): a signed `holger.time-token/v1` bundle at the box's clock (or
//!   `--at`), signed by the three identities under `--signer-dir` (minted once);
//!   `--trust-policy-out` writes the pinned policy the airgap side verifies it with.
//! * `status --config C` (the airgap side): the floor, where it is kept and what set it.
//!
//! The token is carried in like any bundle: `holger bundle import --bundle <token> --policy
//! <its trust policy>`, the inbox, or the import doors.

use std::path::PathBuf;

use anyhow::{Context, Result};
use clap::{Args, Subcommand};

#[derive(Args)]
pub struct TimeTokenArgs {
    #[command(subcommand)]
    pub op: TimeTokenOp,
}

#[derive(Subcommand)]
pub enum TimeTokenOp {
    /// Make a signed time token (the online side).
    Make {
        /// The token bundle to write.
        #[arg(long)]
        out: PathBuf,
        /// The signer: three ed25519 identities, minted here on first use (keys 0600).
        #[arg(long)]
        signer_dir: PathBuf,
        /// The time it says (RFC 3339). Default: this machine's clock now.
        #[arg(long)]
        at: Option<String>,
        /// How long the token may be carried before it expires.
        #[arg(long, default_value_t = 7)]
        valid_days: i64,
        /// The token's supplier (its vendor).
        #[arg(long, default_value = "holger")]
        supplier: String,
        /// Write the pinned trust policy the airgap side verifies the token with.
        #[arg(long)]
        trust_policy_out: Option<PathBuf>,
    },
    /// The box's time floor.
    Status {
        #[arg(short, long)]
        config: PathBuf,
    },
}

pub fn run(a: TimeTokenArgs) -> Result<()> {
    match a.op {
        TimeTokenOp::Make {
            out,
            signer_dir,
            at,
            valid_days,
            supplier,
            trust_policy_out,
        } => {
            let at = match at {
                Some(t) => chrono::DateTime::parse_from_rfc3339(&t)
                    .with_context(|| format!("--at {t}: not an RFC 3339 time"))?
                    .with_timezone(&chrono::Utc),
                None => chrono::Utc::now(),
            };
            make(
                &out,
                &signer_dir,
                at,
                valid_days,
                &supplier,
                trust_policy_out,
            )
        }
        TimeTokenOp::Status { config } => {
            let holger = server_lib::read_ron_config(&config)
                .with_context(|| format!("load config {}", config.display()))?;
            let floor = server_lib::time_floor::TimeFloor::open(&holger.data_root())
                .map_err(|e| anyhow::anyhow!("{e}"))?;
            match floor.floor_ms() {
                0 => println!(
                    "TIME FLOOR none yet ({}): nothing trusted has been seen",
                    floor.path().display()
                ),
                ms => println!(
                    "TIME FLOOR {} ({}), set by {}",
                    chrono::DateTime::<chrono::Utc>::from_timestamp_millis(ms)
                        .map(|t| t.to_rfc3339())
                        .unwrap_or_default(),
                    floor.path().display(),
                    floor.source()
                ),
            }
            Ok(())
        }
    }
}

#[cfg(feature = "export-push")]
fn make(
    out: &std::path::Path,
    signer_dir: &std::path::Path,
    at: chrono::DateTime<chrono::Utc>,
    valid_days: i64,
    supplier: &str,
    trust_policy_out: Option<PathBuf>,
) -> Result<()> {
    anyhow::ensure!(valid_days > 0, "--valid-days is at least 1");
    let policy = server_lib::time_floor::make_token(
        out,
        signer_dir,
        at,
        chrono::Duration::days(valid_days),
        supplier,
    )?;
    println!(
        "TIME TOKEN {} says {} (valid {valid_days} d)",
        out.display(),
        at.to_rfc3339()
    );
    if let Some(p) = trust_policy_out {
        std::fs::write(
            &p,
            policy
                .to_ron()
                .map_err(|e| anyhow::anyhow!("the trust policy: {e}"))?,
        )
        .with_context(|| format!("write {}", p.display()))?;
        println!(
            "TRUST POLICY {} (the pinned signer of these time tokens)",
            p.display()
        );
    }
    Ok(())
}

#[cfg(not(feature = "export-push"))]
fn make(
    _: &std::path::Path,
    _: &std::path::Path,
    _: chrono::DateTime<chrono::Utc>,
    _: i64,
    _: &str,
    _: Option<PathBuf>,
) -> Result<()> {
    anyhow::bail!(
        "this build makes no time tokens (the online side's build, with `export-push`, does)"
    )
}