Add CLI interface for the signing key
- 'signkey set' sets a new signing key (disables an old one if existing). - 'signkey show' prints the fingerprint. - 'signkey dump' prints the certificate of the signing key.
This commit is contained in:
+63
-3
@@ -33,6 +33,7 @@ pub enum HuskSubcommands {
|
||||
Daemon(DaemonCommand),
|
||||
Introducer(IntroducerCommand),
|
||||
Locals(LocalsCommand),
|
||||
Signkey(SignkeyCommand),
|
||||
}
|
||||
|
||||
// Daemon subcommands
|
||||
@@ -76,6 +77,9 @@ pub struct IntroducerCommand {
|
||||
pub enum IntroducerSubcommand {
|
||||
Add(IntroducerAddCommand),
|
||||
Remove(IntroducerRemoveCommand),
|
||||
#[clap(
|
||||
about = "List introducers."
|
||||
)]
|
||||
List
|
||||
}
|
||||
|
||||
@@ -88,7 +92,8 @@ pub struct IntroducerAddCommand {
|
||||
#[clap(
|
||||
long = "cert",
|
||||
help = "Certificate to declare as introducer.",
|
||||
conflicts_with = "cert_file"
|
||||
conflicts_with = "cert_file",
|
||||
value_name = "FINGERPRINT",
|
||||
)]
|
||||
pub cert: Option<String>,
|
||||
|
||||
@@ -118,7 +123,8 @@ pub struct IntroducerAddCommand {
|
||||
pub struct IntroducerRemoveCommand {
|
||||
#[clap(
|
||||
long = "cert",
|
||||
help = "Certificate to be removed as introducer."
|
||||
help = "Certificate to be removed as introducer.",
|
||||
value_name = "FINGERPRINT",
|
||||
)]
|
||||
pub cert: String,
|
||||
|
||||
@@ -149,6 +155,9 @@ pub struct LocalsCommand {
|
||||
pub enum LocalsSubcommand {
|
||||
Add(LocalsAddCommand),
|
||||
Remove(LocalsRemoveCommand),
|
||||
#[clap(
|
||||
about = "List locals."
|
||||
)]
|
||||
List
|
||||
}
|
||||
|
||||
@@ -174,8 +183,59 @@ pub struct LocalsAddCommand {
|
||||
pub struct LocalsRemoveCommand {
|
||||
#[clap(
|
||||
long = "cert",
|
||||
help = "Certificate to be removed."
|
||||
help = "Certificate to be removed.",
|
||||
value_name = "FINGERPRINT",
|
||||
)]
|
||||
pub cert: String,
|
||||
}
|
||||
|
||||
// Signkey subcommands
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
name = "signkey",
|
||||
about = "Manage the signing key.",
|
||||
subcommand_required = true,
|
||||
arg_required_else_help = true,
|
||||
disable_colored_help = true,
|
||||
disable_version_flag = true,
|
||||
)]
|
||||
pub struct SignkeyCommand {
|
||||
#[clap(subcommand)]
|
||||
pub subcommand: SignkeySubcommand,
|
||||
}
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum SignkeySubcommand {
|
||||
Set(SignkeySetCommand),
|
||||
#[clap(
|
||||
about = "Show the fingerprint of the signing key."
|
||||
)]
|
||||
Show,
|
||||
#[clap(
|
||||
about = "Print the certificate of the signing key."
|
||||
)]
|
||||
Dump,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(
|
||||
about = "Set a new signing key.",
|
||||
)]
|
||||
pub struct SignkeySetCommand {
|
||||
#[clap(
|
||||
long,
|
||||
help = "Load a new signing key from <FILE>.",
|
||||
value_name = "FILE",
|
||||
conflicts_with = "cert"
|
||||
)]
|
||||
pub cert_file: Option<String>,
|
||||
|
||||
#[clap(
|
||||
long = "cert",
|
||||
help = "Certificate to declare as signing key.",
|
||||
conflicts_with = "cert_file",
|
||||
value_name = "FINGERPRINT",
|
||||
)]
|
||||
pub cert: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::config::HuskConfigContainer;
|
||||
pub mod daemon;
|
||||
pub mod introducer;
|
||||
pub mod locals;
|
||||
pub mod signkey;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum CommandError {
|
||||
@@ -34,6 +35,9 @@ pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> {
|
||||
HuskSubcommands::Locals(subcmd) => {
|
||||
commands::locals::dispatch(subcmd, config).await?;
|
||||
},
|
||||
HuskSubcommands::Signkey(subcmd) => {
|
||||
commands::signkey::dispatch(subcmd, config).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -37,7 +37,6 @@ pub async fn dispatch(cmd: LocalsAddCommand, config: HuskConfigContainer) -> Res
|
||||
&vc,
|
||||
crypto::Role::Local,
|
||||
true,
|
||||
None,
|
||||
None
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ pub async fn dispatch(cmd: LocalsRemoveCommand, config: HuskConfigContainer) ->
|
||||
cert,
|
||||
crypto::Role::Local,
|
||||
false,
|
||||
None,
|
||||
None)
|
||||
} else {
|
||||
Err(CommandError::CertNotUsable(fpr).into())
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//! signkey subcommand
|
||||
|
||||
use anyhow::Result;
|
||||
use sequoia_openpgp::serialize::SerializeInto;
|
||||
|
||||
use crate::cli::cli_args::{SignkeyCommand, SignkeySubcommand};
|
||||
use crate::config::HuskConfigContainer;
|
||||
use crate::types::husk_context::HuskContext;
|
||||
|
||||
pub mod set;
|
||||
|
||||
pub async fn dispatch(cmd: SignkeyCommand, config: HuskConfigContainer) -> Result<()> {
|
||||
match cmd.subcommand {
|
||||
SignkeySubcommand::Set(set_cmd) => {
|
||||
set::dispatch(set_cmd, config).await?;
|
||||
},
|
||||
SignkeySubcommand::Show => {
|
||||
let context = HuskContext::new(&config.into())?;
|
||||
if let Some(key) = context.signing_key {
|
||||
println!("Fingerprint: {}", key.fingerprint());
|
||||
} else {
|
||||
println!("No active signing key available.");
|
||||
}
|
||||
},
|
||||
SignkeySubcommand::Dump => {
|
||||
let context = HuskContext::new(&config.into())?;
|
||||
if let Some(key) = context.signing_key {
|
||||
let armored = key.armored().to_vec()?;
|
||||
println!("{}", String::from_utf8(armored)?);
|
||||
} else {
|
||||
println!("No active signing key available.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//! signkey set command
|
||||
|
||||
use anyhow::Result;
|
||||
use sequoia_cert_store::Store;
|
||||
use sequoia_openpgp::cert::ValidCert;
|
||||
use sequoia_openpgp::cert;
|
||||
use sequoia_openpgp::parse::Parse;
|
||||
use sequoia_openpgp::{Fingerprint, KeyHandle};
|
||||
use sequoia_openpgp::types::RevocationStatus;
|
||||
|
||||
use crate::cli::cli_args::SignkeySetCommand;
|
||||
use crate::commands::CommandError;
|
||||
use crate::common::crypto;
|
||||
use crate::config::HuskConfigContainer;
|
||||
use crate::types::husk_context::HuskContext;
|
||||
|
||||
pub async fn dispatch(cmd: SignkeySetCommand, config: HuskConfigContainer) -> Result<()> {
|
||||
let context = HuskContext::new(&config.into())?;
|
||||
let policy = &context.policy;
|
||||
let cert_store = &context.cert_store;
|
||||
|
||||
if let Some(cert_file) = cmd.cert_file {
|
||||
let c = cert::Cert::from_file(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_sign(&vc) {
|
||||
return Err(CommandError::CertNotUsable(vc.fingerprint()).into());
|
||||
}
|
||||
|
||||
// set the signing key
|
||||
crypto::set_signing_key(
|
||||
&context,
|
||||
&vc
|
||||
)?;
|
||||
|
||||
} else if let Some(cert) = cmd.cert {
|
||||
let fpr = Fingerprint::from_hex(cert.as_str())?;
|
||||
let certs = cert_store.lookup_by_cert(&KeyHandle::from(&fpr))
|
||||
.map_err(|_| CommandError::CertNotFound(fpr.clone()))?;
|
||||
|
||||
let certs: Vec<ValidCert> = certs.iter()
|
||||
.filter_map(|c|
|
||||
c.with_policy(policy, None).ok()
|
||||
)
|
||||
.filter(|vc| crypto::can_sign(vc))
|
||||
.collect();
|
||||
|
||||
if let Some(cert) = certs.first() {
|
||||
crypto::set_signing_key(
|
||||
&context,
|
||||
cert
|
||||
)?;
|
||||
} else {
|
||||
return Err(CommandError::CertNotUsable(fpr).into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+35
-17
@@ -37,6 +37,10 @@ use crate::types::recipient::Recipient;
|
||||
use crate::mail;
|
||||
use crate::common::escape;
|
||||
|
||||
/// Signing keys used by Husk must have a certification with a
|
||||
/// notation of the name `HUSK_SIG_NOTATION`.
|
||||
const HUSK_SIG_NOTATION: &str = "signing_key@husk-project.org";
|
||||
|
||||
/// Returns the local trust root from the cert_store. If the trust root
|
||||
/// cannot be found, it returns an error.
|
||||
pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result<Cert> {
|
||||
@@ -420,7 +424,7 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipien
|
||||
let message = Armorer::new(message).build()?;
|
||||
let message = Encryptor::for_recipients(message, keys).build()?;
|
||||
let mut message = LiteralWriter::new(message).build()?;
|
||||
message.write_all(&body[..])?;
|
||||
message.write_all(body)?;
|
||||
message.finalize()?;
|
||||
|
||||
Ok(Bytes::from(sink))
|
||||
@@ -434,6 +438,7 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipien
|
||||
pub enum Role {
|
||||
Local,
|
||||
Introducer,
|
||||
SigningKey,
|
||||
}
|
||||
|
||||
/// Authenticate a certificate.
|
||||
@@ -442,7 +447,17 @@ pub enum Role {
|
||||
/// `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, role: Role, activate: bool, domains: Option<Vec<String>>, notation: Option<(String, String)>)
|
||||
pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option<Vec<String>>) -> anyhow::Result<()> {
|
||||
internal_authenticate(context, cert, role, activate, domains, None)
|
||||
}
|
||||
|
||||
/// Make `cert` the new signing key.
|
||||
pub fn set_signing_key(context: &HuskContext, cert: &ValidCert) -> anyhow::Result<()> {
|
||||
internal_authenticate(context, cert, Role::SigningKey, true, None, Some((HUSK_SIG_NOTATION.to_string(), "placeholder".to_string())))
|
||||
}
|
||||
|
||||
/// Internal authentication method.
|
||||
fn internal_authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option<Vec<String>>, notation: Option<(String, String)>)
|
||||
-> anyhow::Result<()> {
|
||||
|
||||
let policy = &context.policy;
|
||||
@@ -475,7 +490,7 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activat
|
||||
|
||||
if activate {
|
||||
// set the trust level and depth for an active local/introducer.
|
||||
if role == Role::Local {
|
||||
if role == Role::Local || role == Role::SigningKey {
|
||||
builder = builder.set_trust_signature(0, 120)?;
|
||||
} else {
|
||||
// Introducer
|
||||
@@ -572,9 +587,16 @@ pub fn can_encrypt(cert: &ValidCert) -> bool {
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
/// Signing keys used by Husk must have a certification with a
|
||||
/// notation of the name `HUSK_SIG_NOTATION`.
|
||||
pub const HUSK_SIG_NOTATION: &str = "signing_key@husk-project.org";
|
||||
/// Returns true if `cert` can be used for signing.
|
||||
pub fn can_sign(cert: &ValidCert) -> bool {
|
||||
cert.keys()
|
||||
.for_signing()
|
||||
.alive()
|
||||
.supported()
|
||||
.revoked(false)
|
||||
.count() > 0
|
||||
}
|
||||
|
||||
|
||||
/// Get Husks signing key
|
||||
pub fn get_signing_key(sequoia_home: Home, cert_store: &CertStore<'_>, policy: &StandardPolicy<'_>, local_trust_root: &Cert) -> anyhow::Result<Option<Cert>> {
|
||||
@@ -609,10 +631,10 @@ pub fn get_signing_key(sequoia_home: Home, cert_store: &CertStore<'_>, policy: &
|
||||
}
|
||||
}
|
||||
|
||||
/// If `vc` is a certificate marked as a Husk signing key
|
||||
/// key, return the creation time of the signature - else return `None`.
|
||||
/// If `vc` is a certificate marked as a Husk signing key, return the
|
||||
/// creation time of the signature - else return `None`.
|
||||
pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Option<SystemTime> {
|
||||
for uid in vc.userids().into_iter() {
|
||||
for uid in vc.userids() {
|
||||
for sig in uid.certifications() {
|
||||
if sig.issuer_fingerprints().any(|i| i == local_root_fpr)
|
||||
&& sig.notation(HUSK_SIG_NOTATION).count() > 0 {
|
||||
@@ -628,6 +650,7 @@ pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Opt
|
||||
}
|
||||
|
||||
/// Get all keys from all backends
|
||||
// XXX: Maybe the softkeys backend is sufficient
|
||||
pub fn get_all_keys(sequoia_home: Home) -> Option<Vec<Fingerprint>> {
|
||||
|
||||
let mut result = Vec::new();
|
||||
@@ -669,10 +692,10 @@ pub mod tests {
|
||||
use sequoia_openpgp::{cert::CertBuilder, policy::StandardPolicy};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::crypto::{HUSK_SIG_NOTATION, get_all_keys};
|
||||
use crate::common::crypto::get_all_keys;
|
||||
use crate::types::{husk_context::HuskContext, mail_context::MailContext};
|
||||
|
||||
use super::{authenticate, get_signing_key};
|
||||
use super::get_signing_key;
|
||||
|
||||
#[test]
|
||||
pub fn test_is_encrypted() {
|
||||
@@ -815,12 +838,7 @@ jk6lh01Ej9QTGQ==
|
||||
let keys= get_all_keys(home.clone());
|
||||
eprintln!("{:?}", keys);
|
||||
|
||||
authenticate(&context,
|
||||
&valid_signing_cert,
|
||||
super::Role::Local,
|
||||
true,
|
||||
None,
|
||||
Some((HUSK_SIG_NOTATION.to_string(), "dummy".to_string())))?;
|
||||
super::set_signing_key(&context, &valid_signing_cert)?;
|
||||
|
||||
// get the Husk signing key
|
||||
let key = get_signing_key(home,
|
||||
|
||||
@@ -83,8 +83,7 @@ impl Introducer {
|
||||
cert,
|
||||
crypto::Role::Introducer,
|
||||
true,
|
||||
Some(domains),
|
||||
None)
|
||||
Some(domains))
|
||||
}
|
||||
|
||||
/// Remove an introducer.
|
||||
@@ -102,7 +101,6 @@ impl Introducer {
|
||||
cert,
|
||||
crypto::Role::Introducer,
|
||||
false,
|
||||
None,
|
||||
None)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user