Add locals add and remove subcommands
- `locals add` adds a local (unintroduced) certificate for encryption. - `locals remove` removes/deacivates a local certificate. - Overall cleanup
This commit is contained in:
+7
-6
@@ -94,8 +94,8 @@ pub struct IntroducerAddCommand {
|
||||
|
||||
#[clap(
|
||||
long = "cert-file",
|
||||
help = "Import certificate from CERT_FILE and declare it as introducer.",
|
||||
value_name = "CERT_FILE",
|
||||
help = "Import certificate from <FILE> and declare it as introducer.",
|
||||
value_name = "FILE",
|
||||
conflicts_with = "cert"
|
||||
)]
|
||||
pub cert_file: Option<String>,
|
||||
@@ -159,8 +159,9 @@ pub enum LocalsSubcommand {
|
||||
)]
|
||||
pub struct LocalsAddCommand {
|
||||
#[clap(
|
||||
long = "cert",
|
||||
help = "Certificate to add."
|
||||
long,
|
||||
help = "Import a certificate from <FILE>.",
|
||||
value_name = "FILE",
|
||||
)]
|
||||
pub cert_file: String,
|
||||
}
|
||||
@@ -168,12 +169,12 @@ pub struct LocalsAddCommand {
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
name = "locals_remove",
|
||||
about = "Certificate to remove.",
|
||||
about = "Disable a Certificate.",
|
||||
)]
|
||||
pub struct LocalsRemoveCommand {
|
||||
#[clap(
|
||||
long = "cert",
|
||||
help = "Certificate to removed."
|
||||
help = "Certificate to be removed."
|
||||
)]
|
||||
pub cert: String,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//! introducer subcommand
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::cli::cli_args::{IntroducerCommand, IntroducerSubcommand};
|
||||
|
||||
@@ -31,14 +31,14 @@ pub async fn dispatch(cmd: IntroducerAddCommand, config: HuskConfigContainer) ->
|
||||
if matches!(vc.revocation_status(), RevocationStatus::Revoked(_)) {
|
||||
return Err(CommandError::CertRevoked(vc.fingerprint()).into());
|
||||
}
|
||||
if !crypto::has_certification_capability(&vc) {
|
||||
if !crypto::can_certify(&vc) {
|
||||
return Err(CommandError::CertNotUsable(vc.fingerprint()).into());
|
||||
}
|
||||
|
||||
Introducer::create(&context, &vc, cmd.domains)?;
|
||||
} else if let Some(cert) = cmd.cert {
|
||||
let fpr = Fingerprint::from_hex(cert.as_str())?;
|
||||
let certs = cert_store.lookup_by_cert(&KeyHandle::try_from(&fpr)?)
|
||||
let certs = cert_store.lookup_by_cert(&KeyHandle::from(&fpr))
|
||||
.map_err(|_| CommandError::CertNotFound(fpr.clone()))?;
|
||||
|
||||
let certs: Vec<ValidCert> = certs.iter()
|
||||
@@ -48,7 +48,7 @@ pub async fn dispatch(cmd: IntroducerAddCommand, config: HuskConfigContainer) ->
|
||||
.collect();
|
||||
|
||||
if let Some(cert) = certs.first() {
|
||||
Introducer::create(&context, &cert, cmd.domains)?;
|
||||
Introducer::create(&context, cert, cmd.domains)?;
|
||||
} else {
|
||||
return Err(CommandError::CertNotUsable(fpr).into());
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub async fn dispatch(cmd: IntroducerRemoveCommand, config: HuskConfigContainer)
|
||||
|
||||
// Get the certificate
|
||||
let fpr = Fingerprint::from_hex(&cmd.cert)?;
|
||||
let certs = cert_store.lookup_by_cert(&KeyHandle::try_from(&fpr)?)
|
||||
let certs = cert_store.lookup_by_cert(&KeyHandle::from(&fpr))
|
||||
.map_err(|_| CommandError::CertNotFound(fpr.clone()))?;
|
||||
|
||||
// Filter out valid certificates. If the certificate is not
|
||||
@@ -35,8 +35,6 @@ pub async fn dispatch(cmd: IntroducerRemoveCommand, config: HuskConfigContainer)
|
||||
} else {
|
||||
return Err(CommandError::CertNotUsable(fpr).into());
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//! locals subcommand
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::cli::cli_args::{LocalsCommand, LocalsSubcommand};
|
||||
@@ -11,10 +12,10 @@ pub mod remove;
|
||||
pub async fn dispatch(cmd: LocalsCommand, config: HuskConfigContainer) -> Result<()> {
|
||||
match cmd.subcommand {
|
||||
LocalsSubcommand::Add(add_cmd) => {
|
||||
add::dispatch(add_cmd, &config).await?;
|
||||
add::dispatch(add_cmd, config).await?;
|
||||
},
|
||||
LocalsSubcommand::Remove(rm_cmd) => {
|
||||
remove::dispatch(rm_cmd, &config).await?;
|
||||
remove::dispatch(rm_cmd, config).await?;
|
||||
},
|
||||
LocalsSubcommand::List => {
|
||||
let context = HuskContext::new(&config.into())?;
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use sequoia_openpgp::cert;
|
||||
use sequoia_openpgp::parse::Parse;
|
||||
use sequoia_openpgp::types::RevocationStatus;
|
||||
|
||||
use crate::cli::cli_args::LocalsAddCommand;
|
||||
use crate::config::HuskConfigContainer;
|
||||
use crate::types::husk_context::HuskContext;
|
||||
use crate::commands::CommandError;
|
||||
use crate::common::crypto;
|
||||
|
||||
pub async fn dispatch(cmd: LocalsAddCommand, _config: &HuskConfigContainer) -> Result<()> {
|
||||
pub async fn dispatch(cmd: LocalsAddCommand, config: HuskConfigContainer) -> Result<()> {
|
||||
println!("add");
|
||||
println!(" {:?}", cmd.cert_file);
|
||||
Ok(())
|
||||
|
||||
let context = HuskContext::new(&config.into())?;
|
||||
let policy = &context.policy;
|
||||
|
||||
let c = cert::Cert::from_file(cmd.cert_file)?;
|
||||
let vc = c.with_policy(policy, None)?;
|
||||
|
||||
// guards
|
||||
if vc.alive().is_err() {
|
||||
return Err(CommandError::CertNotAlive(vc.fingerprint()).into());
|
||||
}
|
||||
if matches!(vc.revocation_status(), RevocationStatus::Revoked(_)) {
|
||||
return Err(CommandError::CertRevoked(vc.fingerprint()).into());
|
||||
}
|
||||
if !crypto::can_encrypt(&vc) {
|
||||
return Err(CommandError::CertNotUsable(vc.fingerprint()).into());
|
||||
}
|
||||
if vc.fingerprint() == context.local_trust_root.fingerprint() {
|
||||
// XXX: find a better error
|
||||
return Err(CommandError::CertNotUsable(vc.fingerprint()).into());
|
||||
}
|
||||
|
||||
crypto::authenticate(
|
||||
&context,
|
||||
&vc,
|
||||
crypto::Role::Local,
|
||||
true,
|
||||
None
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,52 @@
|
||||
//! locals remove subcommand
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::cli::cli_args::LocalsRemoveCommand;
|
||||
use crate::config::HuskConfigContainer;
|
||||
use sequoia_openpgp::Fingerprint;
|
||||
use sequoia_openpgp::KeyHandle;
|
||||
use sequoia_openpgp::cert::ValidCert;
|
||||
use sequoia_cert_store::Store;
|
||||
|
||||
pub async fn dispatch(cmd: LocalsRemoveCommand, _config: &HuskConfigContainer) -> Result<()> {
|
||||
use crate::cli::cli_args::LocalsRemoveCommand;
|
||||
use crate::common::crypto;
|
||||
use crate::config::HuskConfigContainer;
|
||||
use crate::commands::CommandError;
|
||||
use crate::types::husk_context::HuskContext;
|
||||
|
||||
pub async fn dispatch(cmd: LocalsRemoveCommand, config: HuskConfigContainer) -> Result<()> {
|
||||
println!("remove");
|
||||
println!(" {:?}", cmd.cert);
|
||||
Ok(())
|
||||
let context = HuskContext::new(&config.into())?;
|
||||
let policy = &context.policy;
|
||||
let cert_store = &context.cert_store;
|
||||
|
||||
// Get the certificate
|
||||
let fpr = Fingerprint::from_hex(&cmd.cert)?;
|
||||
let certs = cert_store.lookup_by_cert(&KeyHandle::from(&fpr))
|
||||
.map_err(|_| CommandError::CertNotFound(fpr.clone()))?;
|
||||
|
||||
// Filter out valid certificates. If the certificate is not
|
||||
// valid according to the policy, it is already no introducer.
|
||||
let certs: Vec<ValidCert> = certs.iter()
|
||||
.filter_map(|c|
|
||||
c.with_policy(policy, None).ok()
|
||||
)
|
||||
.collect();
|
||||
|
||||
if let Some(cert) = certs.first() {
|
||||
// filter out the trust root
|
||||
if cert.fingerprint() == context.local_trust_root.fingerprint() {
|
||||
return Err(CommandError::CertNotUsable(fpr).into());
|
||||
}
|
||||
crypto::authenticate(
|
||||
&context,
|
||||
cert,
|
||||
crypto::Role::Local,
|
||||
false,
|
||||
None)
|
||||
} else {
|
||||
Err(CommandError::CertNotUsable(fpr).into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+51
-26
@@ -346,17 +346,13 @@ pub fn get_locals<'c>(cert_store: &CertStore<'c>, policy: &StandardPolicy, local
|
||||
for p in targets {
|
||||
let cert_synopsis = p.target().clone();
|
||||
|
||||
match set.entry(cert_synopsis.fingerprint()) {
|
||||
Vacant(_) => {
|
||||
// Only consider non introduced certificates.
|
||||
if !p.certifications().any(|c|
|
||||
introducers.contains(&c.issuer().fingerprint())) {
|
||||
if let Vacant(_) = set.entry(cert_synopsis.fingerprint()) {
|
||||
// Only consider non introduced certificates.
|
||||
if !p.certifications().any(|c|
|
||||
introducers.contains(&c.issuer().fingerprint())) {
|
||||
|
||||
set.insert(cert_synopsis.fingerprint(), cert_synopsis);
|
||||
}
|
||||
},
|
||||
// The target is already known, do nothing.
|
||||
_ => {}
|
||||
set.insert(cert_synopsis.fingerprint(), cert_synopsis);
|
||||
}
|
||||
}
|
||||
}
|
||||
set.into_values().collect()
|
||||
@@ -428,13 +424,20 @@ pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec<Recipie
|
||||
}
|
||||
}
|
||||
|
||||
/// Roles for authenticate()
|
||||
#[derive(PartialEq)]
|
||||
pub enum Role {
|
||||
Local,
|
||||
Introducer,
|
||||
}
|
||||
|
||||
/// Authenticate a certificate.
|
||||
///
|
||||
/// If `activate` is true, the certificate becomes an introducer for
|
||||
/// `domains`. Passing `None` or an empty list in this case is an error.
|
||||
/// If `activate` is false, create a certifiaction with minimal trust_roots
|
||||
/// amount and no depth. In this case `domains` is ignored.
|
||||
pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, domains: Option<Vec<String>>)
|
||||
pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option<Vec<String>>)
|
||||
-> anyhow::Result<()> {
|
||||
|
||||
let policy = &context.policy;
|
||||
@@ -465,27 +468,37 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, dom
|
||||
.set_revocable(true)?;
|
||||
// set no expiration date
|
||||
|
||||
// Add domains
|
||||
if activate {
|
||||
// set the trust level and depth for an active introducer.
|
||||
builder = builder.set_trust_signature(1, 120)?;
|
||||
// set the trust level and depth for an active local/introducer.
|
||||
if role == Role::Local {
|
||||
builder = builder.set_trust_signature(0, 120)?;
|
||||
} else {
|
||||
// Introducer
|
||||
builder = builder.set_trust_signature(1, 120)?;
|
||||
|
||||
if let Some(domains) = domains {
|
||||
for domain in domains {
|
||||
if !mail::is_domain(domain.as_str()) {
|
||||
return Err(HuskError::InvalidDomainName(domain).into());
|
||||
// Add domains
|
||||
if let Some(domains) = domains {
|
||||
if domains.is_empty() {
|
||||
// An introducer without domains would be unlimited.
|
||||
return Err(HuskError::UnlimitedIntroducer.into());
|
||||
}
|
||||
|
||||
builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$",
|
||||
escape::regex_ctrl_escape(domain.as_str())?
|
||||
))?;
|
||||
for domain in domains {
|
||||
if !mail::is_domain(domain.as_str()) {
|
||||
return Err(HuskError::InvalidDomainName(domain).into());
|
||||
}
|
||||
|
||||
builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$",
|
||||
escape::regex_ctrl_escape(domain.as_str())?
|
||||
))?;
|
||||
}
|
||||
} else {
|
||||
// An introducer without domains would be unlimited.
|
||||
return Err(HuskError::UnlimitedIntroducer.into());
|
||||
}
|
||||
} else {
|
||||
// An introducer without domains would be unlimited.
|
||||
return Err(HuskError::UnlimitedIntroducer.into());
|
||||
}
|
||||
} else {
|
||||
// deactive the introducer
|
||||
// deactive the certificate
|
||||
builder = builder.set_trust_signature(0, 1)?;
|
||||
}
|
||||
|
||||
@@ -526,7 +539,8 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, dom
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_certification_capability(cert: &ValidCert) -> bool {
|
||||
/// Returns true if `cert` can be used for certifications.
|
||||
pub fn can_certify(cert: &ValidCert) -> bool {
|
||||
cert.keys()
|
||||
.for_certification()
|
||||
.alive()
|
||||
@@ -535,6 +549,17 @@ pub fn has_certification_capability(cert: &ValidCert) -> bool {
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
/// Returns true if `cert` can be used for encryption.
|
||||
pub fn can_encrypt(cert: &ValidCert) -> bool {
|
||||
cert.keys()
|
||||
.for_transport_encryption()
|
||||
.alive()
|
||||
.supported()
|
||||
.revoked(false)
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Husk milter
|
||||
|
||||
use anyhow;
|
||||
use std::process;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
|
||||
+15
-2
@@ -42,6 +42,7 @@ impl Introducer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Has this introducer certified cert?
|
||||
pub fn is_introducing(&self, cert: &Cert) -> bool {
|
||||
|
||||
let policy = StandardPolicy::new();
|
||||
@@ -66,6 +67,7 @@ impl Introducer {
|
||||
result
|
||||
}
|
||||
|
||||
/// Create an introducer.
|
||||
pub fn create(context: &HuskContext, cert: &ValidCert, domains: Vec<String>)
|
||||
-> Result<()> {
|
||||
|
||||
@@ -75,9 +77,15 @@ impl Introducer {
|
||||
return Err(IntroducerError::UnsuitableCertificate.into());
|
||||
}
|
||||
|
||||
crypto::authenticate(context, cert, true, Some(domains))
|
||||
crypto::authenticate(
|
||||
context,
|
||||
cert,
|
||||
crypto::Role::Introducer,
|
||||
true,
|
||||
Some(domains))
|
||||
}
|
||||
|
||||
/// Remove an introducer.
|
||||
pub fn remove(context: &HuskContext, cert: &ValidCert, _demote: bool)
|
||||
-> Result<()> {
|
||||
|
||||
@@ -87,7 +95,12 @@ impl Introducer {
|
||||
return Err(IntroducerError::UnsuitableCertificate.into());
|
||||
}
|
||||
|
||||
crypto::authenticate(context, cert, false, None)
|
||||
crypto::authenticate(
|
||||
context,
|
||||
cert,
|
||||
crypto::Role::Introducer,
|
||||
false,
|
||||
None)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user