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:
Malte Meiboom
2026-06-13 14:37:37 +02:00
parent 7547776efe
commit 06c0583f5e
8 changed files with 207 additions and 25 deletions
+63 -3
View File
@@ -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>,
}
+4
View File
@@ -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(())
-1
View File
@@ -37,7 +37,6 @@ pub async fn dispatch(cmd: LocalsAddCommand, config: HuskConfigContainer) -> Res
&vc,
crypto::Role::Local,
true,
None,
None
)
}
-1
View File
@@ -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())
+37
View File
@@ -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(())
}
+67
View File
@@ -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
View File
@@ -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,
+1 -3
View File
@@ -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)
}
}