Implement 'introducer remove'
- Implement the subcommand `remove` for `introducer`. - Remove the introducer by adding a certification with trust level 1 and trust depth 0. - Move `crypt.rs` into `common/` and add a generic authenticaton function to be used by `introducer add` and `... remove`.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use sequoia_openpgp::{Fingerprint, KeyHandle};
|
use sequoia_openpgp::{Fingerprint, KeyHandle};
|
||||||
|
use sequoia_openpgp::cert::ValidCert;
|
||||||
use sequoia_cert_store::Store;
|
use sequoia_cert_store::Store;
|
||||||
|
|
||||||
use crate::cli::cli_args::IntroducerRemoveCommand;
|
use crate::cli::cli_args::IntroducerRemoveCommand;
|
||||||
@@ -23,6 +24,19 @@ pub async fn dispatch(cmd: IntroducerRemoveCommand, config: HuskConfigContainer)
|
|||||||
let certs = cert_store.lookup_by_cert(&KeyHandle::try_from(&fpr)?)
|
let certs = cert_store.lookup_by_cert(&KeyHandle::try_from(&fpr)?)
|
||||||
.map_err(|_| CommandError::CertNotFound(fpr.clone()))?;
|
.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() {
|
||||||
|
Introducer::remove(&context, cert, cmd.demote)?;
|
||||||
|
} else {
|
||||||
|
return Err(CommandError::CertNotFound(fpr).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -7,15 +7,19 @@ use std::collections::HashMap;
|
|||||||
use std::collections::hash_map::Entry::{Occupied, Vacant};
|
use std::collections::hash_map::Entry::{Occupied, Vacant};
|
||||||
|
|
||||||
use anyhow;
|
use anyhow;
|
||||||
|
use sequoia_openpgp::cert::ValidCert;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use sequoia_net::{KeyServer, wkd, dane};
|
use sequoia_net::{KeyServer, wkd, dane};
|
||||||
|
use sequoia_openpgp::packet::prelude::SignatureBuilder;
|
||||||
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
|
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
|
||||||
use sequoia_openpgp::policy::StandardPolicy;
|
use sequoia_openpgp::policy::StandardPolicy;
|
||||||
use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, LiteralWriter};
|
use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, LiteralWriter};
|
||||||
use sequoia_openpgp::{Fingerprint, Cert, Packet};
|
use sequoia_openpgp::{Fingerprint, Cert, Packet};
|
||||||
use sequoia_openpgp::cert::raw::RawCertParser;
|
use sequoia_openpgp::cert::raw::RawCertParser;
|
||||||
|
use sequoia_openpgp::types::SignatureType;
|
||||||
use sequoia_cert_store::{Store, CertStore, LazyCert, StoreUpdate};
|
use sequoia_cert_store::{Store, CertStore, LazyCert, StoreUpdate};
|
||||||
|
use sequoia_cert_store::store::MergePublicCollectStats;
|
||||||
use sequoia_wot::{self as wot};
|
use sequoia_wot::{self as wot};
|
||||||
use wot::store::Store as _;
|
use wot::store::Store as _;
|
||||||
use wot::{Depth, Path};
|
use wot::{Depth, Path};
|
||||||
@@ -24,6 +28,8 @@ use crate::types::errors::HuskError;
|
|||||||
use crate::types::husk_context::HuskContext;
|
use crate::types::husk_context::HuskContext;
|
||||||
use crate::types::introducer::Introducer;
|
use crate::types::introducer::Introducer;
|
||||||
use crate::types::recipient::Recipient;
|
use crate::types::recipient::Recipient;
|
||||||
|
use crate::mail;
|
||||||
|
use crate::common::escape;
|
||||||
|
|
||||||
/// Returns the local trust root from the cert_store. If the trust root
|
/// Returns the local trust root from the cert_store. If the trust root
|
||||||
/// cannot be found, it returns an error.
|
/// cannot be found, it returns an error.
|
||||||
@@ -368,6 +374,104 @@ pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec<Recipie
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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>>)
|
||||||
|
-> anyhow::Result<()> {
|
||||||
|
|
||||||
|
let policy = &context.policy;
|
||||||
|
let root_signing_key = match context.local_trust_root.with_policy(policy, None)?
|
||||||
|
.keys()
|
||||||
|
.supported()
|
||||||
|
.alive()
|
||||||
|
.revoked(false)
|
||||||
|
.for_certification()
|
||||||
|
.next() {
|
||||||
|
Some(k) => k.key(),
|
||||||
|
None => {
|
||||||
|
return Err(HuskError::DamangedLocalTrustRoot(
|
||||||
|
"missing certification capability".to_string())
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut root_signer = root_signing_key
|
||||||
|
.clone()
|
||||||
|
.parts_into_secret()?
|
||||||
|
.into_keypair()?;
|
||||||
|
|
||||||
|
// Create the SignatureBuilder with a default preconfiguration.
|
||||||
|
let mut builder =
|
||||||
|
SignatureBuilder::new(SignatureType::GenericCertification)
|
||||||
|
.set_signature_creation_time(std::time::SystemTime::now())?
|
||||||
|
.set_exportable_certification(false)?
|
||||||
|
.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)?;
|
||||||
|
|
||||||
|
if let Some(domains) = domains {
|
||||||
|
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 {
|
||||||
|
// deactive the introducer
|
||||||
|
builder = builder.set_trust_signature(0, 1)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a certification, user ids are needed. Build a list of
|
||||||
|
// valid (non self revoked) user ids.
|
||||||
|
let user_ids: Vec<_> = cert.userids()
|
||||||
|
.filter(|u| {
|
||||||
|
u.self_revocations().count() == 0
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if user_ids.is_empty() {
|
||||||
|
return Err(HuskError::NoUserIDs(cert.fingerprint().to_hex()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// now certify
|
||||||
|
let signatures: Vec<_> = user_ids.into_iter()
|
||||||
|
.filter_map(|u| {
|
||||||
|
let b = builder.clone();
|
||||||
|
|
||||||
|
b.sign_userid_binding(
|
||||||
|
&mut root_signer,
|
||||||
|
cert.primary_key().key(),
|
||||||
|
u.userid()).ok()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let (new_cert, _) = cert.cert().clone().insert_packets(signatures)?;
|
||||||
|
|
||||||
|
// and (re)import
|
||||||
|
let new_cert = Arc::new(new_cert.into());
|
||||||
|
let stats = MergePublicCollectStats::new();
|
||||||
|
let cert_store = &context.cert_store;
|
||||||
|
cert_store.update_by(new_cert, &stats)?;
|
||||||
|
|
||||||
|
log::debug!("{:?}", stats);
|
||||||
|
|
||||||
|
// XXX: do something with the created stats
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -8,12 +8,20 @@ pub enum HuskError {
|
|||||||
ConfigFileOpenError(String),
|
ConfigFileOpenError(String),
|
||||||
#[error("Error while parsing config file: {0}")]
|
#[error("Error while parsing config file: {0}")]
|
||||||
ConfigFileParseError(String),
|
ConfigFileParseError(String),
|
||||||
#[error("Cannot find local trust root")]
|
#[error("Cannot find the local trust root")]
|
||||||
NoLocalTrustRoot,
|
NoLocalTrustRoot,
|
||||||
|
#[error("The local trust root is damaged - {0}")]
|
||||||
|
DamangedLocalTrustRoot(String),
|
||||||
#[error("Cannot access cert store")]
|
#[error("Cannot access cert store")]
|
||||||
NoCertStore,
|
NoCertStore,
|
||||||
#[error("No encryption keys found")]
|
#[error("No encryption keys found")]
|
||||||
NoEncryptionKeys,
|
NoEncryptionKeys,
|
||||||
#[error("Error while looking up email {0}: {1}")]
|
#[error("Error while looking up email {0}: {1}")]
|
||||||
KeyLookupError(String, String),
|
KeyLookupError(String, String),
|
||||||
|
#[error("Unlimited introducer")]
|
||||||
|
UnlimitedIntroducer,
|
||||||
|
#[error("Invalid domain name '{0}'")]
|
||||||
|
InvalidDomainName(String),
|
||||||
|
#[error("Certificate has no user ids - {0}")]
|
||||||
|
NoUserIDs(String),
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-84
@@ -1,22 +1,15 @@
|
|||||||
//! introducers
|
//! introducers
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use sequoia_cert_store::StoreUpdate;
|
|
||||||
use sequoia_cert_store::store::MergePublicCollectStats;
|
|
||||||
use sequoia_openpgp::packet::prelude::SignatureBuilder;
|
|
||||||
use sequoia_wot as wot;
|
use sequoia_wot as wot;
|
||||||
use wot::{CertSynopsis, Certification};
|
use wot::{CertSynopsis, Certification};
|
||||||
use sequoia_openpgp::{cert::ValidCert, policy::StandardPolicy};
|
use sequoia_openpgp::{cert::ValidCert, policy::StandardPolicy};
|
||||||
use sequoia_openpgp::Cert;
|
use sequoia_openpgp::Cert;
|
||||||
use sequoia_openpgp::Fingerprint;
|
use sequoia_openpgp::Fingerprint;
|
||||||
use sequoia_openpgp::types::SignatureType;
|
|
||||||
|
|
||||||
use crate::mail;
|
|
||||||
use crate::types::husk_context::HuskContext;
|
use crate::types::husk_context::HuskContext;
|
||||||
use crate::common::escape;
|
use crate::common::crypto;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum IntroducerError {
|
pub enum IntroducerError {
|
||||||
@@ -87,86 +80,19 @@ impl Introducer {
|
|||||||
return Err(IntroducerError::UnsuitableCertificate.into());
|
return Err(IntroducerError::UnsuitableCertificate.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let policy = &context.policy;
|
crypto::authenticate(context, cert, true, Some(domains))
|
||||||
let root_signing_key = match context.local_trust_root.with_policy(policy, None)?
|
}
|
||||||
.keys()
|
|
||||||
.supported()
|
|
||||||
.alive()
|
|
||||||
.revoked(false)
|
|
||||||
.for_certification()
|
|
||||||
.next() {
|
|
||||||
Some(k) => k.key(),
|
|
||||||
None => {
|
|
||||||
return Err(IntroducerError::RootDamaged.into());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut root_signer = root_signing_key
|
|
||||||
.clone()
|
|
||||||
.parts_into_secret()?
|
|
||||||
.into_keypair()?;
|
|
||||||
|
|
||||||
|
pub fn remove(context: &HuskContext, cert: &ValidCert, _demote: bool)
|
||||||
|
-> Result<()> {
|
||||||
|
|
||||||
// Check if domain list is not empty.
|
// Check if the local trust root certificate should be made an
|
||||||
// An empty list would create an unlimited introducer.
|
// introducer, which would be an error.
|
||||||
if domains.is_empty() {
|
if cert.fingerprint() == context.local_trust_root.fingerprint() {
|
||||||
return Err(IntroducerError::UnlimitedIntroducer.into());
|
return Err(IntroducerError::UnsuitableCertificate.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the SignatureBuilder with a default preconfiguration.
|
crypto::authenticate(context, cert, false, None)
|
||||||
let mut builder =
|
|
||||||
SignatureBuilder::new(SignatureType::GenericCertification)
|
|
||||||
.set_signature_creation_time(std::time::SystemTime::now())?
|
|
||||||
.set_trust_signature(1, 120)?
|
|
||||||
.set_exportable_certification(false)?
|
|
||||||
.set_revocable(true)?;
|
|
||||||
// set no expiration date
|
|
||||||
|
|
||||||
// Add domains
|
|
||||||
for domain in domains {
|
|
||||||
if !mail::is_domain(domain.as_str()) {
|
|
||||||
return Err(IntroducerError::InvalidDomainName(domain).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$",
|
|
||||||
escape::regex_ctrl_escape(domain.as_str())?
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For a certification, user ids are needed. Build a list of
|
|
||||||
// valid (non self revoked) user ids.
|
|
||||||
let user_ids: Vec<_> = cert.userids()
|
|
||||||
.filter(|u| {
|
|
||||||
u.self_revocations().count() == 0
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if user_ids.is_empty() {
|
|
||||||
return Err(IntroducerError::NoUserIDs(cert.fingerprint()).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// now certify
|
|
||||||
let signatures: Vec<_> = user_ids.into_iter()
|
|
||||||
.filter_map(|u| {
|
|
||||||
let b = builder.clone();
|
|
||||||
|
|
||||||
b.sign_userid_binding(
|
|
||||||
&mut root_signer,
|
|
||||||
cert.primary_key().key(),
|
|
||||||
u.userid()).ok()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let (new_cert, _) = cert.cert().clone().insert_packets(signatures)?;
|
|
||||||
|
|
||||||
// and (re)import
|
|
||||||
let new_cert = Arc::new(new_cert.into());
|
|
||||||
let stats = MergePublicCollectStats::new();
|
|
||||||
let cert_store = &context.cert_store;
|
|
||||||
cert_store.update_by(new_cert, &stats)?;
|
|
||||||
|
|
||||||
log::debug!("{:?}", stats);
|
|
||||||
|
|
||||||
// XXX: do something with the created stats
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user