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(
|
#[clap(
|
||||||
long = "cert-file",
|
long = "cert-file",
|
||||||
help = "Import certificate from CERT_FILE and declare it as introducer.",
|
help = "Import certificate from <FILE> and declare it as introducer.",
|
||||||
value_name = "CERT_FILE",
|
value_name = "FILE",
|
||||||
conflicts_with = "cert"
|
conflicts_with = "cert"
|
||||||
)]
|
)]
|
||||||
pub cert_file: Option<String>,
|
pub cert_file: Option<String>,
|
||||||
@@ -159,8 +159,9 @@ pub enum LocalsSubcommand {
|
|||||||
)]
|
)]
|
||||||
pub struct LocalsAddCommand {
|
pub struct LocalsAddCommand {
|
||||||
#[clap(
|
#[clap(
|
||||||
long = "cert",
|
long,
|
||||||
help = "Certificate to add."
|
help = "Import a certificate from <FILE>.",
|
||||||
|
value_name = "FILE",
|
||||||
)]
|
)]
|
||||||
pub cert_file: String,
|
pub cert_file: String,
|
||||||
}
|
}
|
||||||
@@ -168,12 +169,12 @@ pub struct LocalsAddCommand {
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[clap(
|
#[clap(
|
||||||
name = "locals_remove",
|
name = "locals_remove",
|
||||||
about = "Certificate to remove.",
|
about = "Disable a Certificate.",
|
||||||
)]
|
)]
|
||||||
pub struct LocalsRemoveCommand {
|
pub struct LocalsRemoveCommand {
|
||||||
#[clap(
|
#[clap(
|
||||||
long = "cert",
|
long = "cert",
|
||||||
help = "Certificate to removed."
|
help = "Certificate to be removed."
|
||||||
)]
|
)]
|
||||||
pub cert: String,
|
pub cert: String,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//! introducer subcommand
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::cli::cli_args::{IntroducerCommand, IntroducerSubcommand};
|
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(_)) {
|
if matches!(vc.revocation_status(), RevocationStatus::Revoked(_)) {
|
||||||
return Err(CommandError::CertRevoked(vc.fingerprint()).into());
|
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());
|
return Err(CommandError::CertNotUsable(vc.fingerprint()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Introducer::create(&context, &vc, cmd.domains)?;
|
Introducer::create(&context, &vc, cmd.domains)?;
|
||||||
} else if let Some(cert) = cmd.cert {
|
} else if let Some(cert) = cmd.cert {
|
||||||
let fpr = Fingerprint::from_hex(cert.as_str())?;
|
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()))?;
|
.map_err(|_| CommandError::CertNotFound(fpr.clone()))?;
|
||||||
|
|
||||||
let certs: Vec<ValidCert> = certs.iter()
|
let certs: Vec<ValidCert> = certs.iter()
|
||||||
@@ -48,7 +48,7 @@ pub async fn dispatch(cmd: IntroducerAddCommand, config: HuskConfigContainer) ->
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if let Some(cert) = certs.first() {
|
if let Some(cert) = certs.first() {
|
||||||
Introducer::create(&context, &cert, cmd.domains)?;
|
Introducer::create(&context, cert, cmd.domains)?;
|
||||||
} else {
|
} else {
|
||||||
return Err(CommandError::CertNotUsable(fpr).into());
|
return Err(CommandError::CertNotUsable(fpr).into());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub async fn dispatch(cmd: IntroducerRemoveCommand, config: HuskConfigContainer)
|
|||||||
|
|
||||||
// Get the certificate
|
// Get the certificate
|
||||||
let fpr = Fingerprint::from_hex(&cmd.cert)?;
|
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()))?;
|
.map_err(|_| CommandError::CertNotFound(fpr.clone()))?;
|
||||||
|
|
||||||
// Filter out valid certificates. If the certificate is not
|
// Filter out valid certificates. If the certificate is not
|
||||||
@@ -35,8 +35,6 @@ pub async fn dispatch(cmd: IntroducerRemoveCommand, config: HuskConfigContainer)
|
|||||||
} else {
|
} else {
|
||||||
return Err(CommandError::CertNotUsable(fpr).into());
|
return Err(CommandError::CertNotUsable(fpr).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//! locals subcommand
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::cli::cli_args::{LocalsCommand, LocalsSubcommand};
|
use crate::cli::cli_args::{LocalsCommand, LocalsSubcommand};
|
||||||
@@ -11,10 +12,10 @@ pub mod remove;
|
|||||||
pub async fn dispatch(cmd: LocalsCommand, config: HuskConfigContainer) -> Result<()> {
|
pub async fn dispatch(cmd: LocalsCommand, config: HuskConfigContainer) -> Result<()> {
|
||||||
match cmd.subcommand {
|
match cmd.subcommand {
|
||||||
LocalsSubcommand::Add(add_cmd) => {
|
LocalsSubcommand::Add(add_cmd) => {
|
||||||
add::dispatch(add_cmd, &config).await?;
|
add::dispatch(add_cmd, config).await?;
|
||||||
},
|
},
|
||||||
LocalsSubcommand::Remove(rm_cmd) => {
|
LocalsSubcommand::Remove(rm_cmd) => {
|
||||||
remove::dispatch(rm_cmd, &config).await?;
|
remove::dispatch(rm_cmd, config).await?;
|
||||||
},
|
},
|
||||||
LocalsSubcommand::List => {
|
LocalsSubcommand::List => {
|
||||||
let context = HuskContext::new(&config.into())?;
|
let context = HuskContext::new(&config.into())?;
|
||||||
|
|||||||
@@ -1,12 +1,45 @@
|
|||||||
use anyhow::Result;
|
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::cli::cli_args::LocalsAddCommand;
|
||||||
use crate::config::HuskConfigContainer;
|
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!("add");
|
||||||
println!(" {:?}", cmd.cert_file);
|
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 anyhow::Result;
|
||||||
|
|
||||||
use crate::cli::cli_args::LocalsRemoveCommand;
|
use sequoia_openpgp::Fingerprint;
|
||||||
use crate::config::HuskConfigContainer;
|
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!("remove");
|
||||||
println!(" {:?}", cmd.cert);
|
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 {
|
for p in targets {
|
||||||
let cert_synopsis = p.target().clone();
|
let cert_synopsis = p.target().clone();
|
||||||
|
|
||||||
match set.entry(cert_synopsis.fingerprint()) {
|
if let Vacant(_) = set.entry(cert_synopsis.fingerprint()) {
|
||||||
Vacant(_) => {
|
// Only consider non introduced certificates.
|
||||||
// Only consider non introduced certificates.
|
if !p.certifications().any(|c|
|
||||||
if !p.certifications().any(|c|
|
introducers.contains(&c.issuer().fingerprint())) {
|
||||||
introducers.contains(&c.issuer().fingerprint())) {
|
|
||||||
|
|
||||||
set.insert(cert_synopsis.fingerprint(), cert_synopsis);
|
set.insert(cert_synopsis.fingerprint(), cert_synopsis);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
// The target is already known, do nothing.
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set.into_values().collect()
|
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.
|
/// Authenticate a certificate.
|
||||||
///
|
///
|
||||||
/// If `activate` is true, the certificate becomes an introducer for
|
/// If `activate` is true, the certificate becomes an introducer for
|
||||||
/// `domains`. Passing `None` or an empty list in this case is an error.
|
/// `domains`. Passing `None` or an empty list in this case is an error.
|
||||||
/// If `activate` is false, create a certifiaction with minimal trust_roots
|
/// If `activate` is false, create a certifiaction with minimal trust_roots
|
||||||
/// amount and no depth. In this case `domains` is ignored.
|
/// 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<()> {
|
-> anyhow::Result<()> {
|
||||||
|
|
||||||
let policy = &context.policy;
|
let policy = &context.policy;
|
||||||
@@ -465,27 +468,37 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, dom
|
|||||||
.set_revocable(true)?;
|
.set_revocable(true)?;
|
||||||
// set no expiration date
|
// set no expiration date
|
||||||
|
|
||||||
// Add domains
|
|
||||||
if activate {
|
if activate {
|
||||||
// set the trust level and depth for an active introducer.
|
// set the trust level and depth for an active local/introducer.
|
||||||
builder = builder.set_trust_signature(1, 120)?;
|
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 {
|
// Add domains
|
||||||
for domain in domains {
|
if let Some(domains) = domains {
|
||||||
if !mail::is_domain(domain.as_str()) {
|
if domains.is_empty() {
|
||||||
return Err(HuskError::InvalidDomainName(domain).into());
|
// An introducer without domains would be unlimited.
|
||||||
|
return Err(HuskError::UnlimitedIntroducer.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$",
|
for domain in domains {
|
||||||
escape::regex_ctrl_escape(domain.as_str())?
|
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 {
|
} else {
|
||||||
// deactive the introducer
|
// deactive the certificate
|
||||||
builder = builder.set_trust_signature(0, 1)?;
|
builder = builder.set_trust_signature(0, 1)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +539,8 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, dom
|
|||||||
Ok(())
|
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()
|
cert.keys()
|
||||||
.for_certification()
|
.for_certification()
|
||||||
.alive()
|
.alive()
|
||||||
@@ -535,6 +549,17 @@ pub fn has_certification_capability(cert: &ValidCert) -> bool {
|
|||||||
.count() > 0
|
.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)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
//! Husk milter
|
//! Husk milter
|
||||||
|
|
||||||
use anyhow;
|
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::sync::{Arc, Mutex};
|
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 {
|
pub fn is_introducing(&self, cert: &Cert) -> bool {
|
||||||
|
|
||||||
let policy = StandardPolicy::new();
|
let policy = StandardPolicy::new();
|
||||||
@@ -66,6 +67,7 @@ impl Introducer {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create an introducer.
|
||||||
pub fn create(context: &HuskContext, cert: &ValidCert, domains: Vec<String>)
|
pub fn create(context: &HuskContext, cert: &ValidCert, domains: Vec<String>)
|
||||||
-> Result<()> {
|
-> Result<()> {
|
||||||
|
|
||||||
@@ -75,9 +77,15 @@ impl Introducer {
|
|||||||
return Err(IntroducerError::UnsuitableCertificate.into());
|
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)
|
pub fn remove(context: &HuskContext, cert: &ValidCert, _demote: bool)
|
||||||
-> Result<()> {
|
-> Result<()> {
|
||||||
|
|
||||||
@@ -87,7 +95,12 @@ impl Introducer {
|
|||||||
return Err(IntroducerError::UnsuitableCertificate.into());
|
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