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)"
)
}