summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2022-11-09 10:30:49 +0000
committerLars Wirzenius <liw@liw.fi>2022-11-09 10:30:49 +0000
commit99010c5eeb45e3626656b22f3c7a1bca663cf260 (patch)
treee5c4135106c429bb84e05fa592440def81cc9616
parent119a4a0dc5f73962dd82e2e4aef03ca24f191e45 (diff)
parent0b6d0e20865be81520c8db88d29a177dc47506e4 (diff)
downloadsshca-99010c5eeb45e3626656b22f3c7a1bca663cf260.tar.gz
Merge branch 'chore' into 'main'
generate temporary host keys See merge request larswirzenius/sshca!60
-rw-r--r--Cargo.lock31
-rw-r--r--Cargo.toml1
-rw-r--r--src/cert.rs6
-rw-r--r--src/cmd/host.rs53
-rw-r--r--src/error.rs9
-rw-r--r--src/info.rs24
-rw-r--r--src/store.rs16
-rw-r--r--sshca.md22
8 files changed, 120 insertions, 42 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a1e9c96..fe43418 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index b5879b0..f0f199b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 {
diff --git a/sshca.md b/sshca.md
index dd34a37..c5c05fd 100644
--- a/sshca.md
+++ b/sshca.md
@@ -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