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),
|
Daemon(DaemonCommand),
|
||||||
Introducer(IntroducerCommand),
|
Introducer(IntroducerCommand),
|
||||||
Locals(LocalsCommand),
|
Locals(LocalsCommand),
|
||||||
|
Signkey(SignkeyCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daemon subcommands
|
// Daemon subcommands
|
||||||
@@ -76,6 +77,9 @@ pub struct IntroducerCommand {
|
|||||||
pub enum IntroducerSubcommand {
|
pub enum IntroducerSubcommand {
|
||||||
Add(IntroducerAddCommand),
|
Add(IntroducerAddCommand),
|
||||||
Remove(IntroducerRemoveCommand),
|
Remove(IntroducerRemoveCommand),
|
||||||
|
#[clap(
|
||||||
|
about = "List introducers."
|
||||||
|
)]
|
||||||
List
|
List
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +92,8 @@ pub struct IntroducerAddCommand {
|
|||||||
#[clap(
|
#[clap(
|
||||||
long = "cert",
|
long = "cert",
|
||||||
help = "Certificate to declare as introducer.",
|
help = "Certificate to declare as introducer.",
|
||||||
conflicts_with = "cert_file"
|
conflicts_with = "cert_file",
|
||||||
|
value_name = "FINGERPRINT",
|
||||||
)]
|
)]
|
||||||
pub cert: Option<String>,
|
pub cert: Option<String>,
|
||||||
|
|
||||||
@@ -118,7 +123,8 @@ pub struct IntroducerAddCommand {
|
|||||||
pub struct IntroducerRemoveCommand {
|
pub struct IntroducerRemoveCommand {
|
||||||
#[clap(
|
#[clap(
|
||||||
long = "cert",
|
long = "cert",
|
||||||
help = "Certificate to be removed as introducer."
|
help = "Certificate to be removed as introducer.",
|
||||||
|
value_name = "FINGERPRINT",
|
||||||
)]
|
)]
|
||||||
pub cert: String,
|
pub cert: String,
|
||||||
|
|
||||||
@@ -149,6 +155,9 @@ pub struct LocalsCommand {
|
|||||||
pub enum LocalsSubcommand {
|
pub enum LocalsSubcommand {
|
||||||
Add(LocalsAddCommand),
|
Add(LocalsAddCommand),
|
||||||
Remove(LocalsRemoveCommand),
|
Remove(LocalsRemoveCommand),
|
||||||
|
#[clap(
|
||||||
|
about = "List locals."
|
||||||
|
)]
|
||||||
List
|
List
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,8 +183,59 @@ pub struct LocalsAddCommand {
|
|||||||
pub struct LocalsRemoveCommand {
|
pub struct LocalsRemoveCommand {
|
||||||
#[clap(
|
#[clap(
|
||||||
long = "cert",
|
long = "cert",
|
||||||
help = "Certificate to be removed."
|
help = "Certificate to be removed.",
|
||||||
|
value_name = "FINGERPRINT",
|
||||||
)]
|
)]
|
||||||
pub cert: String,
|
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 daemon;
|
||||||
pub mod introducer;
|
pub mod introducer;
|
||||||
pub mod locals;
|
pub mod locals;
|
||||||
|
pub mod signkey;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum CommandError {
|
pub enum CommandError {
|
||||||
@@ -34,6 +35,9 @@ pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> {
|
|||||||
HuskSubcommands::Locals(subcmd) => {
|
HuskSubcommands::Locals(subcmd) => {
|
||||||
commands::locals::dispatch(subcmd, config).await?;
|
commands::locals::dispatch(subcmd, config).await?;
|
||||||
},
|
},
|
||||||
|
HuskSubcommands::Signkey(subcmd) => {
|
||||||
|
commands::signkey::dispatch(subcmd, config).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ pub async fn dispatch(cmd: LocalsAddCommand, config: HuskConfigContainer) -> Res
|
|||||||
&vc,
|
&vc,
|
||||||
crypto::Role::Local,
|
crypto::Role::Local,
|
||||||
true,
|
true,
|
||||||
None,
|
|
||||||
None
|
None
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ pub async fn dispatch(cmd: LocalsRemoveCommand, config: HuskConfigContainer) ->
|
|||||||
cert,
|
cert,
|
||||||
crypto::Role::Local,
|
crypto::Role::Local,
|
||||||
false,
|
false,
|
||||||
None,
|
|
||||||
None)
|
None)
|
||||||
} else {
|
} else {
|
||||||
Err(CommandError::CertNotUsable(fpr).into())
|
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::mail;
|
||||||
use crate::common::escape;
|
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
|
/// 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.
|
||||||
pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result<Cert> {
|
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 = Armorer::new(message).build()?;
|
||||||
let message = Encryptor::for_recipients(message, keys).build()?;
|
let message = Encryptor::for_recipients(message, keys).build()?;
|
||||||
let mut message = LiteralWriter::new(message).build()?;
|
let mut message = LiteralWriter::new(message).build()?;
|
||||||
message.write_all(&body[..])?;
|
message.write_all(body)?;
|
||||||
message.finalize()?;
|
message.finalize()?;
|
||||||
|
|
||||||
Ok(Bytes::from(sink))
|
Ok(Bytes::from(sink))
|
||||||
@@ -434,6 +438,7 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipien
|
|||||||
pub enum Role {
|
pub enum Role {
|
||||||
Local,
|
Local,
|
||||||
Introducer,
|
Introducer,
|
||||||
|
SigningKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authenticate a certificate.
|
/// Authenticate a certificate.
|
||||||
@@ -442,7 +447,17 @@ pub enum Role {
|
|||||||
/// `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, 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<()> {
|
-> anyhow::Result<()> {
|
||||||
|
|
||||||
let policy = &context.policy;
|
let policy = &context.policy;
|
||||||
@@ -475,7 +490,7 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activat
|
|||||||
|
|
||||||
if activate {
|
if activate {
|
||||||
// set the trust level and depth for an active local/introducer.
|
// 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)?;
|
builder = builder.set_trust_signature(0, 120)?;
|
||||||
} else {
|
} else {
|
||||||
// Introducer
|
// Introducer
|
||||||
@@ -572,9 +587,16 @@ pub fn can_encrypt(cert: &ValidCert) -> bool {
|
|||||||
.count() > 0
|
.count() > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signing keys used by Husk must have a certification with a
|
/// Returns true if `cert` can be used for signing.
|
||||||
/// notation of the name `HUSK_SIG_NOTATION`.
|
pub fn can_sign(cert: &ValidCert) -> bool {
|
||||||
pub const HUSK_SIG_NOTATION: &str = "signing_key@husk-project.org";
|
cert.keys()
|
||||||
|
.for_signing()
|
||||||
|
.alive()
|
||||||
|
.supported()
|
||||||
|
.revoked(false)
|
||||||
|
.count() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Get Husks signing key
|
/// 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>> {
|
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
|
/// If `vc` is a certificate marked as a Husk signing key, return the
|
||||||
/// key, return the creation time of the signature - else return `None`.
|
/// creation time of the signature - else return `None`.
|
||||||
pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Option<SystemTime> {
|
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() {
|
for sig in uid.certifications() {
|
||||||
if sig.issuer_fingerprints().any(|i| i == local_root_fpr)
|
if sig.issuer_fingerprints().any(|i| i == local_root_fpr)
|
||||||
&& sig.notation(HUSK_SIG_NOTATION).count() > 0 {
|
&& 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
|
/// Get all keys from all backends
|
||||||
|
// XXX: Maybe the softkeys backend is sufficient
|
||||||
pub fn get_all_keys(sequoia_home: Home) -> Option<Vec<Fingerprint>> {
|
pub fn get_all_keys(sequoia_home: Home) -> Option<Vec<Fingerprint>> {
|
||||||
|
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
@@ -669,10 +692,10 @@ pub mod tests {
|
|||||||
use sequoia_openpgp::{cert::CertBuilder, policy::StandardPolicy};
|
use sequoia_openpgp::{cert::CertBuilder, policy::StandardPolicy};
|
||||||
use std::sync::Arc;
|
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 crate::types::{husk_context::HuskContext, mail_context::MailContext};
|
||||||
|
|
||||||
use super::{authenticate, get_signing_key};
|
use super::get_signing_key;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn test_is_encrypted() {
|
pub fn test_is_encrypted() {
|
||||||
@@ -815,12 +838,7 @@ jk6lh01Ej9QTGQ==
|
|||||||
let keys= get_all_keys(home.clone());
|
let keys= get_all_keys(home.clone());
|
||||||
eprintln!("{:?}", keys);
|
eprintln!("{:?}", keys);
|
||||||
|
|
||||||
authenticate(&context,
|
super::set_signing_key(&context, &valid_signing_cert)?;
|
||||||
&valid_signing_cert,
|
|
||||||
super::Role::Local,
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
Some((HUSK_SIG_NOTATION.to_string(), "dummy".to_string())))?;
|
|
||||||
|
|
||||||
// get the Husk signing key
|
// get the Husk signing key
|
||||||
let key = get_signing_key(home,
|
let key = get_signing_key(home,
|
||||||
|
|||||||
@@ -83,8 +83,7 @@ impl Introducer {
|
|||||||
cert,
|
cert,
|
||||||
crypto::Role::Introducer,
|
crypto::Role::Introducer,
|
||||||
true,
|
true,
|
||||||
Some(domains),
|
Some(domains))
|
||||||
None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove an introducer.
|
/// Remove an introducer.
|
||||||
@@ -102,7 +101,6 @@ impl Introducer {
|
|||||||
cert,
|
cert,
|
||||||
crypto::Role::Introducer,
|
crypto::Role::Introducer,
|
||||||
false,
|
false,
|
||||||
None,
|
|
||||||
None)
|
None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user