diff options
author | Lars Wirzenius <liw@liw.fi> | 2022-11-09 10:30:49 +0000 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2022-11-09 10:30:49 +0000 |
commit | 99010c5eeb45e3626656b22f3c7a1bca663cf260 (patch) | |
tree | e5c4135106c429bb84e05fa592440def81cc9616 | |
parent | 119a4a0dc5f73962dd82e2e4aef03ca24f191e45 (diff) | |
parent | 0b6d0e20865be81520c8db88d29a177dc47506e4 (diff) | |
download | sshca-99010c5eeb45e3626656b22f3c7a1bca663cf260.tar.gz |
Merge branch 'chore' into 'main'
generate temporary host keys
See merge request larswirzenius/sshca!60
-rw-r--r-- | Cargo.lock | 31 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/cert.rs | 6 | ||||
-rw-r--r-- | src/cmd/host.rs | 53 | ||||
-rw-r--r-- | src/error.rs | 9 | ||||
-rw-r--r-- | src/info.rs | 24 | ||||
-rw-r--r-- | src/store.rs | 16 | ||||
-rw-r--r-- | sshca.md | 22 |
8 files changed, 120 insertions, 42 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/cert.rs b/src/cert.rs index de04402..f10516c 100644 --- a/src/cert.rs +++ b/src/cert.rs @@ -117,12 +117,12 @@ pub trait CertificateAuthority { .arg("-s") .arg(&ca_key) .args(Self::OPTS) - .args(&["-n", &principals_list]) - .args(&[ + .args(["-n", &principals_list]) + .args([ "-I", &format!("certificate for {} {}", Self::KIND, principals_list), ]) - .args(&["-V", valid_for]) + .args(["-V", valid_for]) .arg(&sub_key_pub) .output() .map_err(|e| CAError::KeyError(KeyError::Run(e)))?; diff --git a/src/cmd/host.rs b/src/cmd/host.rs index d93bb3d..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)] @@ -130,7 +133,7 @@ impl Runnable for New { } else { self.principals.to_vec() }; - let host = info::Host::new(public, None, self.hostname.clone(), principals); + let host = info::Host::new(public, None, self.hostname.clone()).with_principals(principals); store.insert_host(host).map_err(CAError::KeyStoreError)?; Ok(()) } @@ -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(), - principals, - ); + ) + .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 45fd2f7..cddba6a 100644 --- a/src/info.rs +++ b/src/info.rs @@ -60,30 +60,40 @@ impl Info for SafeCA { pub struct Host { public: PublicKey, private: Option<PrivateKey>, + valid_until: Option<String>, name: String, #[serde(default)] principals: Vec<String>, } impl Host { - pub fn new( - public: PublicKey, - private: Option<PrivateKey>, - name: String, - principals: Vec<String>, - ) -> Self { + pub fn new(public: PublicKey, private: Option<PrivateKey>, name: String) -> Self { Self { public, private, + valid_until: None, name, - principals, + principals: vec![], } } + pub fn with_principals(mut self, principals: Vec<String>) -> Self { + self.principals = principals; + self + } + + pub fn set_valid_until(&mut self, timestamp: String) { + self.valid_until = Some(timestamp); + } + 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(); } diff --git a/src/store.rs b/src/store.rs index e2c5349..17982b0 100644 --- a/src/store.rs +++ b/src/store.rs @@ -134,12 +134,7 @@ impl KeyStore { /// Get host from store by name. pub fn get_host(&self, name: &str) -> Option<&Host> { - for host in self.hosts.iter() { - if host.name() == name { - return Some(host); - } - } - None + self.hosts.iter().find(|&host| host.name() == name) } /// Get a mutable reference to a host in store, by name. @@ -206,12 +201,7 @@ impl KeyStore { /// Find user in store by name. pub fn get_user(&self, name: &str) -> Option<&User> { - for u in self.users.iter() { - if u.name() == name { - return Some(u); - } - } - None + self.users.iter().find(|user| user.name() == name) } /// Get a mutable reference to a user in store, by name. @@ -304,8 +294,8 @@ mod test { PublicKey::new("dummy-public".into()), Some(PrivateKey::new("dummy-private".into())), "dummy-host".into(), - vec!["dummy-host".into()], ) + .with_principals(vec!["dummy-host".into()]) } fn dummy_user() -> User { @@ -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 |