use std::collections::{HashMap, HashSet};
use std::time::{Duration, SystemTime};
use anyhow::{Context, Result};
use sequoia_openpgp::cert::amalgamation::ValidateAmalgamation;
use sequoia_openpgp::packet::{Signature, UserID};
use sequoia_openpgp::serialize::SerializeInto;
use sequoia_openpgp::Cert;
use crate::db::models;
use crate::pgp::{self, CipherSuite};
use crate::secret::CaSec;
use crate::types::CertificationStatus;
use crate::Oca;
#[allow(clippy::too_many_arguments)]
pub fn user_new(
oca: &Oca,
name: Option<&str>,
emails: &[&str],
duration_days: Option<u64>,
password: bool,
password_file: Option<String>,
output_format_minimal: bool,
cipher_suite: Option<CipherSuite>,
enable_encryption_subkey: bool,
enable_signing_subkey: bool,
enable_authentication_subkey: bool,
) -> Result<()> {
let (user_key, user_revoc, pass) = pgp::make_user_cert(
emails,
name,
password,
password_file,
cipher_suite,
enable_encryption_subkey,
enable_signing_subkey,
enable_authentication_subkey,
)
.context("make_user_cert failed")?;
let user_certified = certify_emails(oca.secret(), &user_key, Some(emails), duration_days)
.context("sign_user_emails failed")?;
let ca_cert = oca.ca_get_cert_pub()?;
let tsigned_ca =
pgp::tsign(ca_cert, &user_key, pass.as_deref()).context("tsign for CA cert failed")?;
let tsigned_ca = pgp::cert_to_armored_private_key(&tsigned_ca)?;
let user_cert = pgp::cert_to_armored(&user_certified)?;
let user_revoc = pgp::revoc_to_armored(&user_revoc, None)?;
oca.storage
.user_add(
name,
(&user_cert, &user_key.fingerprint().to_hex()),
emails,
&[user_revoc],
Some(tsigned_ca.as_bytes()), )
.context("Failed to insert new user into DB")?;
let private = pgp::cert_to_armored_private_key(&user_certified)?;
if output_format_minimal {
if let Some(pass) = pass {
println!("{pass}");
}
println!("{private}");
} else {
if let Some(name) = name {
eprintln!("Created new user key for {name}.\n");
} else {
eprintln!("Created new user key.\n");
}
println!("{private}");
if let Some(pass) = pass {
eprintln!("Password for this key: '{pass}'.\n");
} else {
eprintln!("No password set for this key.\n");
}
}
Ok(())
}
pub fn cert_import_new(
oca: &Oca,
user_cert: &[u8],
revoc_certs: &[&[u8]],
name: Option<&str>,
cert_emails: &[&str],
duration_days: Option<u64>,
) -> Result<()> {
let user_cert =
pgp::to_cert(user_cert).context("cert_import_new: Couldn't process user cert.")?;
let fp = user_cert.fingerprint().to_hex();
if let Some(_exists) = oca
.storage
.cert_by_fp(&fp)
.context("cert_import_new(): get_cert() check by fingerprint failed")?
{
return Err(anyhow::anyhow!(
"A key with this fingerprint already exists in the DB.\nTo update it, use the 'user update' command."
));
}
let certified = certify_emails(oca.secret(), &user_cert, Some(cert_emails), duration_days)
.context("sign_cert_emails() failed")?;
let name = if let Some(name) = name {
Some(name.to_string())
} else {
let names: HashSet<_> = user_cert
.userids()
.filter_map(|u| u.userid().name2().ok().flatten().map(|s| s.to_string()))
.collect();
if names.len() == 1 {
names.into_iter().next()
} else {
None
}
};
let pub_cert =
pgp::cert_to_armored(&certified).context("cert_import_new: Couldn't re-armor key")?;
let rev_sig: Result<Vec<_>> = revoc_certs.iter().map(|r| pgp::to_signature(r)).collect();
let rev_armored: Result<Vec<_>> = rev_sig?
.iter()
.map(|s| pgp::revoc_to_armored(s, None))
.collect();
oca.storage
.user_add(
name.as_deref(),
(&pub_cert, &fp),
cert_emails,
&rev_armored?,
None,
)
.context("Couldn't insert user")?;
Ok(())
}
pub fn cert_import_update(oca: &Oca, cert: &[u8]) -> Result<()> {
oca.storage.cert_update(cert)
}
fn add_certifications(
oca: &Oca,
certify: Vec<&UserID>,
c: &Cert,
validity_days: u64,
) -> Result<()> {
if !certify.is_empty() {
let sigs = oca
.secret()
.sign_user_ids(c, &certify[..], Some(validity_days))?;
let certified = c.clone().insert_packets(sigs)?;
oca.storage.cert_update(&certified.to_vec()?)?;
}
Ok(())
}
pub fn certs_refresh_ca_certifications(
oca: &Oca,
threshold_days: u64,
validity_days: u64,
) -> Result<()> {
let threshold_time =
SystemTime::now() + Duration::from_secs(threshold_days * pgp::SECONDS_IN_DAY);
let ca = oca.ca_get_cert_pub()?;
for db_cert in oca
.storage
.certs()?
.into_iter()
.filter(|c| !c.inactive)
{
let c = pgp::to_cert(db_cert.pub_cert.as_bytes())?;
let mut re_certify = Vec::new();
for uid in c.userids() {
let ca_certifications = pgp::valid_certifications_by(&uid, &c, ca.clone());
let sig_valid_past_threshold = |sig: &Signature| {
if let Some(expiration) = sig.signature_expiration_time() {
expiration > threshold_time
} else {
true }
};
if !ca_certifications.is_empty()
&& !ca_certifications.iter().any(sig_valid_past_threshold)
{
re_certify.push(uid.userid());
}
}
add_certifications(oca, re_certify, &c, validity_days)?;
}
Ok(())
}
pub fn certs_re_certify(oca: &Oca, cert_old: Cert, validity_days: u64) -> Result<()> {
for db_cert in oca
.storage
.certs()?
.into_iter()
.filter(|c| !c.inactive)
{
let ca_new = oca.ca_get_cert_pub()?;
let c = pgp::to_cert(db_cert.pub_cert.as_bytes())?;
let mut re_certify = Vec::new();
for uid in c.userids() {
let ca_certifications = pgp::valid_certifications_by(&uid, &c, cert_old.clone());
if !ca_certifications.is_empty() {
if pgp::valid_certifications_by(&uid, &c, ca_new.clone()).is_empty() {
re_certify.push(uid.userid());
}
}
}
add_certifications(oca, re_certify, &c, validity_days)?;
}
Ok(())
}
pub fn certs_expired(oca: &Oca, days: u64) -> Result<HashMap<models::Cert, Option<SystemTime>>> {
let mut res = HashMap::new();
let days = Duration::new(60 * 60 * 24 * days, 0);
let expiry_test = SystemTime::now().checked_add(days).unwrap();
let certs = oca.user_certs_get_all().context("couldn't load certs")?;
for db_cert in certs {
let c = pgp::to_cert(db_cert.pub_cert.as_bytes())?;
if c.with_policy(pgp::SP, None)?.alive().is_ok()
&& c.with_policy(pgp::SP, expiry_test)?.alive().is_err()
{
res.insert(db_cert, pgp::get_expiry(&c)?);
}
}
Ok(res)
}
pub fn cert_check_ca_sig(oca: &Oca, cert: &models::Cert) -> Result<CertificationStatus> {
let c = pgp::to_cert(cert.pub_cert.as_bytes())?;
let ca = oca.ca_get_cert_pub()?;
let mut certified = vec![];
let mut uncertified = vec![];
for uid in c.userids() {
if pgp::valid_certifications_by(&uid, &c, ca.clone()).is_empty() {
uncertified.push(uid.userid().clone());
} else {
certified.push(uid.userid().clone());
}
}
Ok(CertificationStatus {
certified,
uncertified,
})
}
pub(crate) fn check_tsig_on_cert(signer: &Cert, signee: &Cert) -> Result<bool> {
let tsigs = pgp::get_trust_sigs(signee)?;
Ok(tsigs.iter().any(|t| {
t.issuer_fingerprints()
.any(|fp| fp == &signer.fingerprint())
}))
}
pub fn cert_check_tsig_on_ca(oca: &Oca, cert: &models::Cert) -> Result<bool> {
let ca = oca.ca_get_cert_pub()?;
let user_cert = pgp::to_cert(cert.pub_cert.as_bytes())?;
check_tsig_on_cert(&user_cert, &ca)
}
fn certify_emails(
ca_sec: &dyn CaSec,
cert: &Cert,
emails_filter: Option<&[&str]>,
duration_days: Option<u64>,
) -> Result<Cert> {
let fp_ca = ca_sec.cert()?.fingerprint();
let mut uids = Vec::new();
let mut unused_email: HashSet<&str> = if let Some(emails) = emails_filter {
emails.iter().copied().collect()
} else {
HashSet::new()
};
for uid in cert.userids() {
if !uid
.clone()
.with_policy(pgp::SP, None)?
.certifications()
.any(|s| s.issuer_fingerprints().any(|fp| fp == &fp_ca))
{
let userid = uid.userid();
let uid_email: Option<String> = match userid.email_normalized() {
Ok(email) => email.clone(),
Err(_) => None,
};
if emails_filter.is_none()
|| (uid_email.is_some()
&& emails_filter
.unwrap()
.contains(&uid_email.clone().unwrap().as_str()))
{
if let Some(uid_email) = uid_email {
unused_email.remove(uid_email.as_str());
}
uids.push(userid);
}
}
}
if !unused_email.is_empty() {
let mut unused: Vec<_> = unused_email.into_iter().collect();
unused.sort_unstable();
println!(
"Warning: Couldn't find a User ID to certify for '{}' in {}",
unused.join(", "),
cert.fingerprint()
);
}
let sigs = ca_sec.sign_user_ids(cert, &uids, duration_days)?;
cert.clone().insert_packets(sigs)
}