diff options
author | Lars Wirzenius <liw@liw.fi> | 2022-11-09 12:27:40 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2022-11-09 12:27:40 +0200 |
commit | 0b6d0e20865be81520c8db88d29a177dc47506e4 (patch) | |
tree | e5c4135106c429bb84e05fa592440def81cc9616 | |
parent | bb230ab8ba940d009995d0a9806b2a3ebf830fe3 (diff) | |
download | sshca-0b6d0e20865be81520c8db88d29a177dc47506e4.tar.gz |
feat: allow marking a generated host key as temporary
Sponsored-by: author
-rw-r--r-- | Cargo.lock | 31 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/cmd/host.rs | 47 | ||||
-rw-r--r-- | src/error.rs | 9 | ||||
-rw-r--r-- | src/info.rs | 7 | ||||
-rw-r--r-- | sshca.md | 22 |
6 files changed, 99 insertions, 18 deletions
@@ -608,15 +608,6 @@ dependencies = [ ] [[package]] -name = "num_threads" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" -dependencies = [ - "libc", -] - -[[package]] name = "once_cell" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1082,6 +1073,7 @@ dependencies = [ "subplotlib", "tempfile", "thiserror", + "time", ] [[package]] @@ -1291,21 +1283,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.9" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa", - "libc", - "num_threads", + "serde", + "time-core", "time-macros", ] [[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] name = "time-macros" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] [[package]] name = "tracing" @@ -18,6 +18,7 @@ serde_yaml = "0.8.17" subplotlib = "0.4.1" tempfile = "3.2.0" thiserror = "1.0.26" +time = { version = "0.3.17", features = ["macros", "formatting"] } [build-dependencies] subplot-build = "0.4.0" diff --git a/src/cmd/host.rs b/src/cmd/host.rs index 2331a6b..c508137 100644 --- a/src/cmd/host.rs +++ b/src/cmd/host.rs @@ -13,8 +13,11 @@ use clap::{ArgAction, Parser}; use std::fs::File; use std::io::Write; use std::path::PathBuf; +use std::time::SystemTime; +use time::{macros::format_description, Duration}; const DEFAULT_VALIDITY: &str = "90d"; +const SHORT_TIME: Duration = Duration::hours(1); /// Manage information about hosts. #[derive(Debug, Parser)] @@ -223,11 +226,39 @@ impl Runnable for Remove { pub struct PublicKeyCmd { /// Name of host. hostname: String, + + /// Pretend it is a different time. This is for testing purposes. + #[clap(long)] + now: Option<String>, +} + +impl PublicKeyCmd { + fn now(&self) -> String { + if let Some(ts) = &self.now { + ts.into() + } else { + let now = SystemTime::now(); + let now = time::OffsetDateTime::from(now); + + let format = format_description!("[year]:[month]:[day]T[hour]:[minute]:[second]"); + + now.format(&format).unwrap() + } + } } impl Runnable for PublicKeyCmd { fn run(&mut self, _config: &Config, store: &mut KeyStore) -> Result<(), CAError> { + eprintln!("now: {:?}", self.now()); if let Some(host) = store.get_host(&self.hostname) { + if let Some(valid_until) = host.valid_until() { + if self.now().as_str() >= valid_until { + return Err(CAError::ExpiredHostKey( + host.name().into(), + valid_until.to_string(), + )); + } + } print!("{}", host.public().as_str()); } else { return Err(CAError::NoSuchHost(self.hostname.clone())); @@ -264,6 +295,10 @@ pub struct Generate { /// Name of host. hostname: String, + /// Make the generated host key short-lived. + #[clap(long)] + temporary: bool, + /// All the principals for this host. Defaults to HOSTNAME if none /// given. #[clap(short, long = "principal", action = ArgAction::Append)] @@ -278,12 +313,22 @@ impl Runnable for Generate { } else { self.principals.to_vec() }; - let host = info::Host::new( + let mut host = info::Host::new( pair.public().clone(), Some(pair.private().clone()), self.hostname.clone(), ) .with_principals(principals); + if self.temporary { + let now = std::time::SystemTime::now(); + if let Some(valid_until) = time::OffsetDateTime::from(now).checked_add(SHORT_TIME) { + let format = format_description!("[year]:[month]:[day]T[hour]:[minute]:[second]"); + let valid_until = valid_until.format(&format).map_err(CAError::TimeFormat)?; + host.set_valid_until(valid_until); + } else { + return Err(CAError::ShortTime); + }; + } store.insert_host(host).map_err(CAError::KeyStoreError)?; Ok(()) } diff --git a/src/error.rs b/src/error.rs index 44fb348..704f4ad 100644 --- a/src/error.rs +++ b/src/error.rs @@ -82,4 +82,13 @@ pub enum CAError { #[error("failed to generate JSON")] Json(#[source] serde_json::Error), + + #[error("failed to compute a time stamps a short time into the future")] + ShortTime, + + #[error("failed to format timestamp")] + TimeFormat(#[source] time::error::Format), + + #[error("host key for {0} has expired, only valid until {1}")] + ExpiredHostKey(String, String), } diff --git a/src/info.rs b/src/info.rs index 16ed18d..cddba6a 100644 --- a/src/info.rs +++ b/src/info.rs @@ -82,15 +82,18 @@ impl Host { self } - pub fn with_valid_until(mut self, timestamp: String) -> Self { + pub fn set_valid_until(&mut self, timestamp: String) { self.valid_until = Some(timestamp); - self } pub fn name(&self) -> &str { &self.name } + pub fn valid_until(&self) -> Option<&str> { + self.valid_until.as_deref() + } + pub fn rename(&mut self, name: &str) { self.name = name.into(); } @@ -549,6 +549,28 @@ when I run sshca host private-key myhost.example.com then stdout contains "-----BEGIN OPENSSH PRIVATE KEY-----" ~~~ +### Generate a short-lived host key + +_Requirement: a host key can be marked as short-lived._ + +Justification: when a host key is generated for installing a new host, +it should be replaced soon. The `sshca` tool should allow marking the +installation host key as short-lived, so that it can't be used for +long. + +~~~scenario +given an installed sshca +given file .config/sshca/config.yaml from config.yaml + +when I run sshca host generate --temporary myhost.example.com +when I run sshca host list +then stdout contains "myhost.example.com" +when I run sshca host public-key myhost.example.com +then stdout contains "ssh-ed25519 " +when I try to run sshca host public-key --now=3000-01-01T00:00:00 myhost.example.com +then command fails +~~~ + ### Re-generate a host key _Requirement: we can generate a new host key for an existing host that |