use std::convert::AsRef;
use std::fs::create_dir_all;
use std::fs::read_dir;
use std::fs::read_to_string;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::string::FromUtf8Error;
use std::time::Duration;
use std::time::SystemTime;
use std::time::SystemTimeError;
use chrono::DateTime;
use chrono::Utc;
use clap::Parser;
use fqdn::FQDN;
use sequoia_net::wkd::cert_contains_domain_userid;
use sequoia_net::wkd::insert;
use sequoia_net::wkd::Variant;
use sequoia_openpgp::cert::amalgamation::key::PrimaryKey;
use sequoia_openpgp::cert::CertBuilder;
use sequoia_openpgp::cert::SubkeyRevocationBuilder;
use sequoia_openpgp::cert::ValidCert;
use sequoia_openpgp::crypto::mpi::PublicKey;
use sequoia_openpgp::crypto::mpi::MPI;
use sequoia_openpgp::packet::key::PublicParts;
use sequoia_openpgp::packet::key::SubordinateRole;
use sequoia_openpgp::packet::prelude::Key4;
use sequoia_openpgp::packet::signature::SignatureBuilder;
use sequoia_openpgp::packet::Key;
use sequoia_openpgp::packet::Signature;
use sequoia_openpgp::packet::UserID;
use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::serialize::SerializeInto;
use sequoia_openpgp::types::Curve;
use sequoia_openpgp::types::Features;
use sequoia_openpgp::types::HashAlgorithm;
use sequoia_openpgp::types::KeyFlags;
use sequoia_openpgp::types::PublicKeyAlgorithm;
use sequoia_openpgp::types::ReasonForRevocation;
use sequoia_openpgp::types::RevocationStatus;
use sequoia_openpgp::types::SignatureType;
use sequoia_openpgp::Cert;
use sequoia_openpgp::Fingerprint;
use sequoia_openpgp::Packet;
use ssh_key::known_hosts::KnownHosts;
use ssh_key::public::EcdsaPublicKey;
use ssh_key::public::KeyData;
use strum::IntoEnumIterator;
const EXPIRY_THRESHOLD_DAYS: u64 = 250;
const EXPIRY_PERIOD_DAYS: u64 = 365;
pub const SECONDS_IN_A_DAY: u64 = 24 * 60 * 60;
pub const EXPIRY_THRESHOLD: Duration = Duration::new(EXPIRY_THRESHOLD_DAYS * SECONDS_IN_A_DAY, 0);
pub const EXPIRY_PERIOD: Duration = Duration::new(EXPIRY_PERIOD_DAYS * SECONDS_IN_A_DAY, 0);
pub const CERT_LOCATION: &str = "/var/lib/sshd-openpgp-auth/";
pub const SSH_HOST_KEY_LOCATION: &str = "/etc/ssh/";
pub const REVOCATION_REASON: KeyRevocationType = KeyRevocationType::Superseded;
pub const WKD_TYPE: WkdType = WkdType::Advanced;
pub const WKD_OUTPUT_DIR: &str = "wkd";
#[derive(Debug, Parser)]
#[command(about, author, version)]
pub enum Commands {
Add(AddCommand),
Export(ExportCommand),
Extend(ExtendCommand),
Init(InitCommand),
List(ListCommand),
Revoke(RevokeCommand),
}
#[derive(Debug, Parser)]
#[command(
about = "Add public SSH host keys as authentication subkeys to an OpenPGP certificate",
long_about = format!(
"Add public SSH host keys as authentication subkeys to an OpenPGP certificate
By default this command adds SSH host keys found in \"{}\" as authentication subkeys to an OpenPGP certificate in \"{}\".
Custom locations for SSH public keys as well as OpenPGP certificates can be provided.
If more than one OpenPGP certificate is found in the target directory, an OpenPGP fingerprint must be specified.
When adding from SSH host key files the subkey creation time is derived from the file creation timestamp of the respective files.
It is possible to add subkeys by piping a known_hosts formatted string to this command when using the \"--known-hosts\" option.
When adding from stdin the current time is used for the subkey creation time.
A custom creation time can be provided.
",
SSH_HOST_KEY_LOCATION,
CERT_LOCATION)
)]
pub struct AddCommand {
#[arg(
env = "SOA_FINGERPRINT",
help = "An OpenPGP fingerprint to identify a specific certificate",
long,
short
)]
pub fingerprint: Option<Fingerprint>,
#[arg(
conflicts_with = "ssh_dir",
help = "Read the SSH public keys in known_hosts format from stdin instead of from a directory",
long,
short
)]
pub known_hosts: bool,
#[arg(
env = "SOA_OPENPGP_DIR",
help = format!("A custom directory in which to look for OpenPGP certificates (defaults to \"{}\")", CERT_LOCATION),
long,
short,
value_name = "DIR",
)]
pub openpgp_dir: Option<PathBuf>,
#[arg(
env = "SOA_SSH_DIR",
help = format!("A custom directory in which to look for SSH public keys (defaults to \"{}\")", SSH_HOST_KEY_LOCATION),
long,
short = 'S',
value_name = "DIR",
)]
pub ssh_dir: Option<PathBuf>,
#[arg(
help = "Output the OpenPGP certificate to stdout instead of a file",
long,
short
)]
pub stdout: bool,
#[arg(
env = "SOA_TIME",
help = "A custom reference time formatted as an RFC3339 string (defaults to now)",
long,
short
)]
pub time: Option<DateTime<Utc>>,
}
#[derive(Debug, Parser)]
#[command(
about = "Export OpenPGP certificates to Web Key Directory (WKD)",
long_about = format!(
"Export OpenPGP certificates to Web Key Directory (WKD)
By default this command exports all valid OpenPGP certificates, that match a hostname, to a Web Key Directory (WKD) structure in \"{}\".
Optionally, a different WKD export type can be selected and a custom reference time be chosen.",
WKD_OUTPUT_DIR)
)]
pub struct ExportCommand {
#[arg(help = "The hostname, as fully qualified domain name (FQDN), for which to export")]
pub hostname: FQDN,
#[arg(
env = "SOA_OPENPGP_DIR",
help = format!("A custom directory in which to look for OpenPGP certificates (defaults to \"{}\")", CERT_LOCATION),
long,
short,
value_name = "DIR",
)]
pub openpgp_dir: Option<PathBuf>,
#[arg(
env = "SOA_WKD_OUTPUT_DIR",
help = format!("A custom output directory (defaults to \"{}\")", WKD_OUTPUT_DIR),
long,
short = 'O',
value_name = "DIR",
)]
pub output_dir: Option<PathBuf>,
#[arg(
env = "SOA_WKD_TYPE",
help = format!("A custom WKD type to export to (defaults to \"{}\")", WKD_TYPE),
long,
long_help = format!(
"A custom WKD type to export for (defaults to \"{}\").\nChoose one of {:?}.",
WKD_TYPE,
WkdType::iter().map(|wkd_type| wkd_type.to_string()).collect::<Vec<String>>()
),
short,
)]
pub wkd_type: Option<WkdType>,
#[arg(
env = "SOA_TIME",
help = "A custom reference time formatted as an RFC3339 string (defaults to now)",
long,
short
)]
pub time: Option<DateTime<Utc>>,
}
#[derive(Debug, Parser)]
#[command(
about = "Extend the expiration period of an OpenPGP certificate",
long_about = format!(
"Extend the expiration period of an OpenPGP certificate
By default this command extends the expiration period of an OpenPGP certificate by {} days from now, if the certificate would expire within the next {} days.
If more than one OpenPGP certificate is found in the target directory, an OpenPGP fingerprint must be specified.
Optionally, the reference time, expiration period and threshold may be provided.
Additionally, the certificate may be written to stdout instead of a file.",
EXPIRY_PERIOD_DAYS,
EXPIRY_THRESHOLD_DAYS)
)]
pub struct ExtendCommand {
#[arg(
env = "SOA_EXPIRY",
help = format!("The expiry period in days from reference time (defaults to {})", EXPIRY_PERIOD_DAYS),
long,
short
)]
pub expiry: Option<u64>,
#[arg(
env = "SOA_FINGERPRINT",
help = "An OpenPGP fingerprint to identify a specific certificate",
long,
short
)]
pub fingerprint: Option<Fingerprint>,
#[arg(
env = "SOA_OPENPGP_DIR",
help = format!("A custom directory in which to look for OpenPGP certificates (defaults to \"{}\")", CERT_LOCATION),
long,
short,
value_name = "DIR",
)]
pub openpgp_dir: Option<PathBuf>,
#[arg(
help = "Output the OpenPGP certificate to stdout instead of a file",
long,
short
)]
pub stdout: bool,
#[arg(
env = "SOA_THRESHOLD",
help = format!("A custom threshold in days from reference time, after which expiry period is extended (defaults to {})", EXPIRY_THRESHOLD_DAYS),
long,
short = 'T',
)]
pub threshold: Option<u64>,
#[arg(
env = "SOA_TIME",
help = "A custom reference time formatted as an RFC3339 string (defaults to now)",
long,
short
)]
pub time: Option<DateTime<Utc>>,
}
#[derive(Debug, Parser)]
#[command(
about = "Initialize a new OpenPGP certificate, that serves as trust anchor for public SSH host keys",
long_about = format!("Initialize a new OpenPGP certificate, that serves as trust anchor for public SSH host keys
By default this function creates an OpenPGP certificate for a hostname, that is valid from now for the next {} days and writes it to a file in \"{}\".
The validity period, as well as the point in time from which the certificate is valid can be adjusted.
Additionally, the certificate may be written to stdout instead of a file.",
EXPIRY_PERIOD_DAYS,
CERT_LOCATION)
)]
pub struct InitCommand {
#[arg(
env = "SOA_EXPIRY",
help = format!("The expiry period in days from reference time (defaults to {})", EXPIRY_PERIOD_DAYS),
long,
short,
)]
pub expiry: Option<u64>,
#[arg(
help = "The hostname, as fully qualified domain name (FQDN), for which a certificate is created"
)]
pub host: FQDN,
#[arg(
env = "SOA_OPENPGP_DIR",
conflicts_with = "stdout",
help = format!("A custom directory into which the OpenPGP certificate is written (defaults to \"{}\")", CERT_LOCATION),
long,
value_name = "DIR",
short,
)]
pub openpgp_dir: Option<PathBuf>,
#[arg(
help = "Output the OpenPGP certificate to stdout instead of a file",
long,
short
)]
pub stdout: bool,
#[arg(
env = "SOA_TIME",
help = format!("A custom reference time formatted as an RFC3339 string (defaults to now)"),
long,
short,
)]
pub time: Option<DateTime<Utc>>,
}
#[derive(Debug, Parser)]
#[command(
about = "List local OpenPGP certificates that serve as trust anchor",
long_about = "List local OpenPGP certificates that serve as trust anchor
By default this command lists all OpenPGP certificates in a directory, that are currently valid.
Optionally, the certificates can be filtered by a hostname.
Additionally, a custom reference time may be provided to show valid certificates at a different point in time."
)]
pub struct ListCommand {
#[arg(
env = "SOA_FILTER",
help = "A hostname, as fully qualified domain name (FQDN), by which to filter",
long,
short,
value_name = "HOSTNAME"
)]
pub filter: Option<FQDN>,
#[arg(
env = "SOA_OPENPGP_DIR",
help = format!("A custom directory in which to look for OpenPGP certificates (defaults to \"{}\")", CERT_LOCATION),
long,
short,
value_name = "DIR",
)]
pub openpgp_dir: Option<PathBuf>,
#[arg(
env = "SOA_TIME",
help = "A custom reference time formatted as an RFC3339 string (defaults to now)",
long,
short
)]
pub time: Option<DateTime<Utc>>,
}
#[derive(Debug, Parser)]
#[command(
about = "Revoke subkeys of an OpenPGP certificate",
long_about = format!("
Revoke subkeys of an OpenPGP certificate
By default this command revokes the subkeys of an OpenPGP certificate in {}.
If more than one OpenPGP certificate is found in the target directory, an OpenPGP fingerprint must be specified.
",
CERT_LOCATION,
),
)]
pub struct RevokeCommand {
#[arg(
help = "Revoke all subkeys of the chosen OpenPGP certificate",
long,
short
)]
pub all: bool,
#[arg(
env = "SOA_FINGERPRINT",
help = "An OpenPGP fingerprint to identify a specific certificate",
long,
short
)]
pub fingerprint: Option<Fingerprint>,
#[arg(
env = "SOA_REVOCATION_MESSAGE",
help = "An optional message for the revocation",
long,
short
)]
pub message: Option<String>,
#[arg(
env = "SOA_OPENPGP_DIR",
help = format!("A custom directory in which to look for OpenPGP certificates (defaults to \"{}\")", CERT_LOCATION),
long,
short,
value_name = "DIR",
)]
pub openpgp_dir: Option<PathBuf>,
#[arg(
env = "SOA_REVOCATION_REASON",
help = format!("A custom revocation reason (defaults to \"{}\")", KeyRevocationType::Superseded),
long,
long_help = format!(
"A custom revocation reason (defaults to \"{}\").\nOne of {}.",
KeyRevocationType::Superseded,
KeyRevocationType::iter()
.map(|rev_type| format!("\"{}\" ({:?})", rev_type, ReasonForRevocation::from(rev_type).revocation_type()))
.collect::<Vec<String>>().join(", ")
),
short,
)]
pub reason: Option<KeyRevocationType>,
#[arg(
help = "Output the OpenPGP certificate to stdout instead of a file",
long,
short
)]
pub stdout: bool,
#[arg(
conflicts_with = "all",
env = "SOA_SUBKEY_FINGERPRINT",
help = "An OpenPGP fingerprint to identify a specific subkey",
long,
long_help = "An OpenPGP fingerprint to identify a specific subkey.\nThis option can be provided more than once",
short = 'S'
)]
pub subkey_fingerprint: Vec<Fingerprint>,
#[arg(
env = "SOA_TIME",
help = "A custom reference time formatted as an RFC3339 string (defaults to now)",
long,
short
)]
pub time: Option<DateTime<Utc>>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("OpenPGP Error: {0}")]
OpenPGP(#[from] anyhow::Error),
#[error("I/O Error: {0}")]
IO(#[from] std::io::Error),
#[error("SSH Key Error: {0}")]
SshKey(#[from] ssh_key::Error),
#[error("Unsupported algorithm: {0}")]
UnsupportedAlgorithm(String),
#[error("No primary key was found")]
PrimaryKeyNotFound,
#[error("No subkey found matching the fingerprint {0}")]
SubkeyNotFound(Fingerprint),
#[error("Multiple subkeys found matching the fingerprint {0}")]
MultipleSubkeysFound(Fingerprint),
#[error("Time Error: {0}")]
TimeError(#[from] SystemTimeError),
#[error("Unable to extend expiry to {0}s from reference time {1:?}s.")]
UnableToExtendExpiry(u64, SystemTime),
#[error("Unable to convert string: {0}")]
StringError(#[from] FromUtf8Error),
#[error("There are no certificates in {0}. Create one first!")]
NoCertificates(PathBuf),
#[error("There are multiple certificates in {0}. Provide a fingerprint!")]
MultipleCertificates(PathBuf),
#[error("The subkey with the fingerprint {0} has already been added!")]
SubkeyAlreadyAdded(Fingerprint),
#[error("Multiple subkeys found for the certificate with the fingerprint {0}")]
MultipleSubkeys(Fingerprint),
#[error("Subkey fingerprints can not be found: {0}")]
SubkeyFingerprintNotFound(String),
}
#[derive(Debug, Clone, Copy, strum::Display, strum::EnumIter, strum::EnumString)]
pub enum KeyRevocationType {
#[strum(ascii_case_insensitive, to_string = "compromised")]
Compromised,
#[strum(ascii_case_insensitive, to_string = "retired")]
Retired,
#[strum(ascii_case_insensitive, to_string = "superseded")]
Superseded,
}
impl From<KeyRevocationType> for ReasonForRevocation {
fn from(from: KeyRevocationType) -> Self {
match from {
KeyRevocationType::Compromised => ReasonForRevocation::KeyCompromised,
KeyRevocationType::Retired => ReasonForRevocation::KeyRetired,
KeyRevocationType::Superseded => ReasonForRevocation::KeySuperseded,
}
}
}
#[derive(Debug, Clone, Copy, strum::Display, strum::EnumIter, strum::EnumString)]
pub enum WkdType {
#[strum(ascii_case_insensitive, to_string = "advanced")]
Advanced,
#[strum(ascii_case_insensitive, to_string = "direct")]
Direct,
}
impl From<WkdType> for Variant {
fn from(from: WkdType) -> Self {
match from {
WkdType::Advanced => Variant::Advanced,
WkdType::Direct => Variant::Direct,
}
}
}
pub fn create_trust_anchor(
host: &FQDN,
creation_time: Option<SystemTime>,
validity_period: Option<Duration>,
) -> Result<Cert, Error> {
let validity_period = Some(validity_period.unwrap_or(EXPIRY_PERIOD));
let cert = CertBuilder::new()
.set_creation_time(creation_time)
.set_validity_period(validity_period)
.add_userid(UserID::from(format!("<ssh-openpgp-auth@{}>", host)))
.generate()?
.0;
Ok(cert)
}
pub fn write_tsk_to_stdout(cert: &Cert) -> Result<(), Error> {
let writer = &mut std::io::stdout().lock();
writer.write_all(&cert.as_tsk().armored().to_vec()?)?;
Ok(())
}
pub fn write_tsk(cert: &Cert, output_dir: Option<&Path>) -> Result<(), Error> {
let output_dir = output_dir.unwrap_or(Path::new(CERT_LOCATION));
if !output_dir.exists() {
create_dir_all(output_dir)?;
}
let mut file = File::create(output_dir.join(format!("{}.asc", cert.fingerprint())))?;
file.write_all(&cert.as_tsk().armored().to_vec()?)?;
Ok(())
}
pub fn get_public_ssh_host_keys(ssh_config_dir: Option<&Path>) -> Result<Vec<PathBuf>, Error> {
Ok(
read_dir(ssh_config_dir.unwrap_or(Path::new(SSH_HOST_KEY_LOCATION).as_ref()))?
.filter_map(|x| {
x.as_ref()
.is_ok_and(|y| {
y.path().is_file() && y.path().extension().is_some_and(|e| e == "pub")
})
.then_some(x.unwrap().path())
})
.collect(),
)
}
pub fn create_openpgp_subkey_from_ssh_public_key_file(
file: &Path,
creation_time: Option<SystemTime>,
) -> Result<Key<PublicParts, SubordinateRole>, Error> {
let file_creation_time = if let Ok(metadata) = file.metadata() {
if let Ok(creation_time) = metadata.created() {
Some(creation_time)
} else {
None
}
} else {
None
};
create_openpgp_subkey_from_ssh_public_key(
ssh_key::PublicKey::from_openssh(&read_to_string(file)?)?,
match (creation_time, file_creation_time) {
(Some(creation_time), None) | (Some(creation_time), Some(_)) => Some(creation_time),
(None, Some(file_creation_time)) => Some(file_creation_time),
(None, None) => None,
},
)
}
pub fn create_openpgp_subkey_from_ssh_public_key(
public_key: ssh_key::PublicKey,
creation_time: Option<SystemTime>,
) -> Result<Key<PublicParts, SubordinateRole>, Error> {
Ok(match public_key.key_data() {
KeyData::Rsa(pubkey) => {
Key4::import_public_rsa(pubkey.e.as_bytes(), pubkey.n.as_bytes(), creation_time)?.into()
}
KeyData::Ed25519(pubkey) => Key4::import_public_ed25519(&pubkey.0, creation_time)?.into(),
KeyData::Ecdsa(EcdsaPublicKey::NistP256(pubkey)) => Key4::new(
creation_time.unwrap_or(SystemTime::now()),
PublicKeyAlgorithm::ECDSA,
PublicKey::ECDSA {
curve: Curve::NistP256,
q: MPI::from(pubkey.as_bytes().to_vec()),
},
)?
.into(),
KeyData::Ecdsa(EcdsaPublicKey::NistP384(pubkey)) => Key4::new(
creation_time.unwrap_or(SystemTime::now()),
PublicKeyAlgorithm::ECDSA,
PublicKey::ECDSA {
curve: Curve::NistP384,
q: MPI::from(pubkey.as_bytes().to_vec()),
},
)?
.into(),
KeyData::Ecdsa(EcdsaPublicKey::NistP521(pubkey)) => Key4::new(
creation_time.unwrap_or(SystemTime::now()),
PublicKeyAlgorithm::ECDSA,
PublicKey::ECDSA {
curve: Curve::NistP521,
q: MPI::from(pubkey.as_bytes().to_vec()),
},
)?
.into(),
_ => {
return Err(Error::UnsupportedAlgorithm(
public_key.algorithm().to_string(),
))
}
})
}
pub fn attach_subkeys_to_cert(
cert: Cert,
subkeys: Vec<Key<PublicParts, SubordinateRole>>,
) -> Result<Cert, Error> {
let new_fingerprints = subkeys
.iter()
.map(|subkey| subkey.fingerprint())
.collect::<Vec<Fingerprint>>();
let current_fingerprints = cert
.keys()
.subkeys()
.map(|subkey| subkey.fingerprint())
.collect::<Vec<Fingerprint>>();
for new_fingerprint in new_fingerprints {
if current_fingerprints.contains(&new_fingerprint) {
return Err(Error::SubkeyAlreadyAdded(new_fingerprint));
}
}
let key = cert
.clone()
.keys()
.unencrypted_secret()
.next()
.unwrap()
.key()
.clone();
let mut signer = key.into_keypair()?;
let signature_builder = SignatureBuilder::new(SignatureType::SubkeyBinding)
.set_signature_creation_time(SystemTime::now())?
.set_preferred_hash_algorithms(vec![HashAlgorithm::SHA512])?
.set_hash_algo(HashAlgorithm::SHA512)
.set_features(Features::sequoia())?
.set_key_flags(KeyFlags::empty().set_authentication())?;
let signatures: Vec<Signature> = subkeys
.iter()
.filter_map(|x| x.bind(&mut signer, &cert, signature_builder.clone()).ok())
.collect();
let packets: Vec<Packet> = signatures.iter().map(|x| x.clone().into()).collect();
Ok(cert.insert_packets(packets)?.insert_packets(subkeys)?)
}
pub fn revoke_subkey_of_cert(
cert: Cert,
fingerprints: Vec<Fingerprint>,
creation_time: Option<SystemTime>,
reason_type: Option<KeyRevocationType>,
reason_msg: Option<&str>,
) -> Result<Cert, Error> {
let creation_time = creation_time.unwrap_or(SystemTime::now());
let reason_type = reason_type.unwrap_or(REVOCATION_REASON);
let reason_msg = reason_msg.unwrap_or_default();
let current_fingerprints = cert
.keys()
.subkeys()
.map(|subkey| subkey.fingerprint())
.collect::<Vec<Fingerprint>>();
let non_matching = fingerprints
.iter()
.filter_map(|fingerprint| {
if !current_fingerprints.contains(fingerprint) {
Some(fingerprint.clone())
} else {
None
}
})
.collect::<Vec<Fingerprint>>();
if !non_matching.is_empty() {
return Err(Error::SubkeyFingerprintNotFound(
non_matching.iter().fold(String::new(), |s, fingerprint| {
s + &format!("{} ", fingerprint)
}),
));
}
let key = cert
.keys()
.unencrypted_secret()
.filter_map(|x| {
if x.primary() {
Some(x.key().clone())
} else {
None
}
})
.last()
.ok_or(Error::PrimaryKeyNotFound)?;
let mut signer = key.into_keypair()?;
let subkeys = cert
.keys()
.subkeys()
.filter_map(|subkey| {
if fingerprints.contains(&subkey.fingerprint()) {
Some(subkey.key().clone())
} else {
None
}
})
.collect::<Vec<_>>();
let mut cert_packets = cert.clone().into_packets().collect::<Vec<Packet>>();
for subkey in subkeys {
cert_packets.push(Packet::Signature(
SubkeyRevocationBuilder::new()
.set_reason_for_revocation(reason_type.into(), reason_msg.as_bytes())?
.set_signature_creation_time(creation_time)?
.build(&mut signer, &cert, &subkey, None)?,
));
}
Ok(Cert::from_packets(cert_packets.into_iter())?)
}
pub fn extend_expiry_of_cert(
cert: Cert,
expiry_threshold: Option<Duration>,
expiry_period: Option<Duration>,
reference_time: Option<SystemTime>,
) -> Result<Cert, Error> {
let reference_time = reference_time.unwrap_or(SystemTime::now());
let expiry_threshold = expiry_threshold.unwrap_or(EXPIRY_THRESHOLD);
let expiry_period = expiry_period.unwrap_or(EXPIRY_PERIOD);
let policy = StandardPolicy::new();
let expiration_time = cert
.with_policy(&policy, reference_time)?
.primary_key()
.key_expiration_time()
.unwrap_or(SystemTime::now());
if expiration_time.duration_since(reference_time)? > expiry_threshold {
return Ok(cert);
}
let mut keypair = cert
.primary_key()
.key()
.clone()
.parts_into_secret()?
.into_keypair()?;
let signature = cert.set_expiration_time(
&policy,
None,
&mut keypair,
Some(
reference_time
.checked_add(expiry_period)
.ok_or(Error::UnableToExtendExpiry(
expiry_period.as_secs(),
reference_time,
))?,
),
)?;
Ok(cert.insert_packets(signature)?)
}
pub fn export_certs_to_wkd(
certs: Vec<Cert>,
fqdn: FQDN,
wkd_type: WkdType,
output_dir: &Path,
reference_time: Option<SystemTime>,
) -> Result<(), Error> {
let reference_time = Some(reference_time.unwrap_or(SystemTime::now()));
let policy = StandardPolicy::new();
let valid_certs: Vec<ValidCert> = certs
.iter()
.filter_map(|cert| {
match cert.with_policy(&policy, reference_time) {
Ok(valid_cert) => {
if cert_contains_domain_userid(format!("{}", fqdn), &valid_cert) {
Some(valid_cert)
} else {
eprintln!("Skipping certificate with fingerprint {}, as it is missing a User ID with FQDN {}.", valid_cert.fingerprint(), fqdn);
None
}
}
Err(_) => {
eprintln!("Skipping certificate with fingerprint {} as it is invalid.", cert.fingerprint());
None
}
}
})
.collect();
for cert in valid_certs {
insert(
output_dir,
format!("{}", fqdn),
Some(wkd_type.into()),
&cert,
)?;
}
Ok(())
}
pub fn parse_known_hosts(input: &str) -> Result<Vec<ssh_key::PublicKey>, Error> {
Ok(KnownHosts::new(input)
.filter_map(|x| x.ok())
.map(|x| x.into())
.collect::<Vec<ssh_key::PublicKey>>())
}
pub fn show_tsks_in_dir(
dir: Option<&Path>,
filter: Option<FQDN>,
reference_time: Option<SystemTime>,
) -> Result<(), Error> {
let dir = dir.unwrap_or(Path::new(CERT_LOCATION));
let filter = filter.map_or_else(String::new, |fqdn| format!("{}>", fqdn));
let reference_time = reference_time.unwrap_or(SystemTime::now());
let asc_files = read_dir(dir)?
.filter_map(|path| {
if let Ok(path) = path.as_ref() {
let path = path.path();
if path.is_file() && path.extension().is_some_and(|e| e == "asc") {
Some(path)
} else {
None
}
} else {
None
}
})
.collect::<Vec<PathBuf>>();
if !asc_files.is_empty() {
let policy = StandardPolicy::new();
for asc_file in asc_files.iter() {
if let Ok(cert) = Cert::from_file(asc_file) {
if cert.is_tsk() {
if let Ok(valid_cert) = cert.with_policy(&policy, reference_time) {
if let Ok(primary_uid) = valid_cert.primary_userid() {
let user_id = primary_uid.userid();
if filter.is_empty() && !format!("{}", user_id).contains(&filter) {
continue;
}
println!("{}:", asc_file.to_str().unwrap_or(""));
println!(
"🔑️ {} ({}): {} - {}",
cert.fingerprint(),
primary_uid.userid(),
DateTime::<Utc>::from(cert.primary_key().creation_time()),
DateTime::<Utc>::from(
valid_cert
.primary_key()
.key_expiration_time()
.unwrap_or(SystemTime::now())
)
);
for subkey in cert.keys().subkeys() {
println!(
"\t{} {}",
match subkey.revocation_status(&policy, SystemTime::now()) {
RevocationStatus::NotAsFarAsWeKnow => "✅️",
_ => "❌️",
},
subkey.fingerprint()
);
}
}
}
}
}
}
}
Ok(())
}
pub fn get_single_cert_from_dir(
dir: Option<&Path>,
fingerprint: Option<Fingerprint>,
) -> Result<Cert, Error> {
let dir = dir.unwrap_or(Path::new(CERT_LOCATION));
if let Some(fingerprint) = fingerprint {
Ok(Cert::from_file(dir.join(format!("{}.asc", fingerprint)))?)
} else {
let asc_files = read_dir(dir)?
.filter_map(|path| {
if let Ok(path) = path.as_ref() {
let path = path.path();
if path.is_file() && path.extension().is_some_and(|e| e == "asc") {
Some(path)
} else {
None
}
} else {
None
}
})
.collect::<Vec<PathBuf>>();
match asc_files.len() {
0 => Err(Error::NoCertificates(dir.into())),
1 => Ok(Cert::from_file(
asc_files
.first()
.expect("We should have exactly one certificate!"),
)?),
_ => Err(Error::MultipleCertificates(dir.into())),
}
}
}
pub fn read_all_certs(dir: Option<&Path>) -> Result<Vec<Cert>, Error> {
let dir = dir.unwrap_or(Path::new(CERT_LOCATION));
Ok(read_dir(dir)?
.filter_map(|path| {
if let Ok(path) = path.as_ref() {
let path = path.path();
if path.is_file() && path.extension().is_some_and(|e| e == "asc") {
Some(path)
} else {
None
}
} else {
None
}
})
.filter_map(|asc_file| Cert::from_file(asc_file).ok())
.collect::<Vec<Cert>>())
}