Change sq key userid add to support the cert store and key store.

- Change `sq key userid add` to support the cert store and key
    store.

  - Add `--cert` to specify a certificate by key ID or fingerprint.

  - Change the positional file argument to `--cert-file`.

  - Change the positional user ID to `--userid`.

  - If `--output` is not specified and `--cert` is, import the
    modified certificate into the cert store.  If `--output` is not
    specified and `--cert-file` is, write the modified certificate to
    stdout
This commit is contained in:
Neal H. Walfield 2024-05-01 11:57:07 +02:00
parent 42126b5534
commit bbe350118a
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
4 changed files with 177 additions and 76 deletions

5
NEWS
View File

@ -5,6 +5,11 @@
* Changes in 0.37.0
** Notable changes
- Remove PKS support.
- `sq key userid add` can now use the certificate store and the
keystore.
- `sq key userid add` no longer accepts positional arguments. The
user ID is provided by the `--userid` argument, and the
certificate by `--cert` or `--cert-file`.
* Changes in 0.36.0
- Missing
* Changes in 0.35.0

View File

@ -15,6 +15,7 @@ use crate::cli::KEY_VALIDITY_IN_YEARS;
use crate::cli::types::ClapData;
use crate::cli::types::EncryptPurpose;
use crate::cli::types::Expiry;
use crate::cli::types::FileOrCertStore;
use crate::cli::types::FileOrStdin;
use crate::cli::types::FileOrStdout;
use crate::cli::types::Time;
@ -639,21 +640,23 @@ $ sq key userid add --userid Juliet --creation-time 20210628 \\
juliet.key.pgp --output juliet-new.key.pgp
",
)]
#[clap(group(ArgGroup::new("cert_input").args(&["cert_file", "cert"]).required(true)))]
pub struct UseridAddCommand {
#[clap(
help = FileOrStdin::HELP_REQUIRED,
value_name = FileOrStdin::VALUE_NAME,
)]
pub input: FileOrStdin,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP_OPTIONAL,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
value_name = "FINGERPRINT|KEYID",
help = "Add the user ID to the specified certificate",
)]
pub output: FileOrStdout,
pub cert: Option<KeyHandle>,
#[clap(
long,
help = FileOrStdin::HELP_OPTIONAL,
value_name = FileOrStdin::VALUE_NAME,
conflicts_with = "cert",
)]
pub cert_file: Option<FileOrStdin>,
#[clap(
long,
value_name = "USERID",
required = true,
help = "User ID to add",
@ -667,6 +670,17 @@ pub struct UseridAddCommand {
`Name (Comment) <localpart@example.org>`.",
)]
pub allow_non_canonical_userids: bool,
#[clap(
help = "Write to the specified FILE. If not specified, and the \
certificate was read from the certificate store, imports the \
modified certificate into the cert store. If not specified, \
and the certificate was read from a file, writes the modified \
certificate to stdout.",
long,
short,
value_name = FileOrCertStore::VALUE_NAME,
)]
pub output: Option<FileOrStdout>,
#[clap(
short = 'B',
long,

View File

@ -1,4 +1,5 @@
use std::str::from_utf8;
use std::sync::Arc;
use std::time::SystemTime;
use anyhow::Context;
@ -6,6 +7,7 @@ use anyhow::Context;
use anyhow::anyhow;
use itertools::Itertools;
use sequoia_openpgp as openpgp;
use openpgp::armor::Kind;
use openpgp::armor::Writer;
use openpgp::cert::amalgamation::ValidAmalgamation;
@ -23,7 +25,9 @@ use openpgp::types::SignatureType;
use openpgp::Cert;
use openpgp::Packet;
use openpgp::Result;
use sequoia_openpgp as openpgp;
use sequoia_cert_store as cert_store;
use cert_store::StoreUpdate;
use crate::Sq;
use crate::cli::key::UseridRevokeCommand;
@ -214,10 +218,24 @@ pub fn dispatch(
fn userid_add(
sq: Sq,
command: cli::key::UseridAddCommand,
mut command: cli::key::UseridAddCommand,
) -> Result<()> {
let input = command.input.open()?;
let key = Cert::from_buffered_reader(input)?;
let cert = if let Some(file) = command.cert_file {
// If `--output` is not specified, default to writing to
// stdout, not to the certificate store.
if command.output.is_none() {
command.output = Some(FileOrStdout::new(None));
}
let input = file.open()?;
Cert::from_buffered_reader(input)?
} else if let Some(kh) = command.cert {
sq.lookup_one(&kh, None, false)?
} else {
panic!("--cert or --cert-file is required");
};
let mut signer = sq.get_primary_key(&cert, None)?.0;
// Make sure the user IDs are in canonical form. If not, and
// `--allow-non-canonical-userids` is not set, error out.
@ -226,12 +244,12 @@ fn userid_add(
}
// Fail if any of the User IDs to add already exist in the ValidCert
let key_userids: Vec<_> =
key.userids().map(|u| u.userid().value()).collect();
let cert_userids: Vec<_> =
cert.userids().map(|u| u.userid().value()).collect();
let exists: Vec<_> = command
.userid
.iter()
.filter(|s| key_userids.contains(&s.value()))
.filter(|s| cert_userids.contains(&s.value()))
.collect();
if !exists.is_empty() {
return Err(anyhow::anyhow!(
@ -240,19 +258,10 @@ fn userid_add(
));
}
let mut pk = match sq.get_primary_key(&key, None) {
Ok((key, _password)) => {
key
}
Err(error) => {
return Err(error)
}
};
let vcert = key
let vcert = cert
.with_policy(sq.policy, sq.time)
.with_context(|| {
format!("Certificate {} is not valid", key.fingerprint())
format!("Certificate {} is not valid", cert.fingerprint())
})?;
// Use the primary User ID or direct key signature as template for the
@ -347,19 +356,25 @@ fn userid_add(
// Creation time.
sb = sb.set_signature_creation_time(sq.time)?;
let binding = uid.bind(&mut pk, &key, sb.clone())?;
let binding = uid.bind(&mut signer, &cert, sb.clone())?;
add.push(binding.into());
}
// Merge additional User IDs into key
let cert = key.insert_packets(add)?;
// Merge the new User IDs into cert.
let cert = cert.insert_packets(add)?;
let mut sink = command.output.for_secrets().create_safe(sq.force)?;
if command.binary {
cert.as_tsk().serialize(&mut sink)?;
if let Some(output) = command.output {
let mut sink = output.for_secrets().create_safe(sq.force)?;
if command.binary {
cert.as_tsk().serialize(&mut sink)?;
} else {
cert.as_tsk().armored().serialize(&mut sink)?;
}
} else {
cert.as_tsk().armored().serialize(&mut sink)?;
let cert_store = sq.cert_store_or_else()?;
cert_store.update(Arc::new(cert.into()))?;
}
Ok(())
}

149
src/sq.rs
View File

@ -36,6 +36,7 @@ use sequoia_wot as wot;
use wot::store::Store as _;
use sequoia_keystore as keystore;
use keystore::Protection;
use crate::common::password;
use crate::ImportStatus;
@ -1024,13 +1025,104 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
let mut keys: Vec<(Box<dyn crypto::Signer + Send + Sync>,
Option<Password>)>
= vec![];
'next_cert: for tsk in certs {
let tsk = tsk.borrow();
let vc = match tsk.with_policy(self.policy, self.time) {
let try_tsk = |ka: &ValidKeyAmalgamation<_, _, _>,
keys: &Vec<(_, Option<Password>)>|
-> Result<(_, _)>
{
if let Some(secret) = ka.key().optional_secret() {
let (unencrypted, password) = match secret {
SecretKeyMaterial::Encrypted(ref e) => {
// try passwords from already existing keys
match keys.iter().find_map(|(_, password)| {
password.as_ref().and_then(
|p| e.decrypt(ka.pk_algo(), p).ok()
.map(|key| (key, p.clone())))
}) {
Some((unencrypted, password)) =>
(unencrypted, Some(password)),
None => {
let password = password::prompt_to_unlock(
&format!("key {}/{}",
ka.cert().keyid(),
ka.keyid()))?;
(
e.decrypt(ka.pk_algo(), &password)
.map_err(|_| anyhow!("Incorrect password."))?,
Some(password),
)
}
}
}
SecretKeyMaterial::Unencrypted(ref u) => (u.clone(), None),
};
Ok((
Box::new(
crypto::KeyPair::new(ka.key().clone(), unencrypted).unwrap()
),
password,
))
} else {
Err(anyhow!("No secret key material."))
}
};
let try_keystore = |ka: &ValidKeyAmalgamation<_, _, _>|
-> Result<(_, _)>
{
let ks = self.key_store_or_else()?;
let mut ks = ks.lock().unwrap();
let remote_keys = ks.find_key(ka.key_handle())?;
let uid = self.best_userid(ka.cert(), true);
// XXX: Be a bit smarter. If there are multiple
// keys, sort them so that we try the easiest one
// first (available, no password).
'key: for mut key in remote_keys.into_iter() {
let password = if let Protection::Password(hint) = key.locked()? {
if let Some(hint) = hint {
eprintln!("{}", hint);
}
loop {
let p = password::prompt_to_unlock(&format!(
"Please enter the password to decrypt \
the key {}/{}, {}",
ka.cert().keyid(), ka.keyid(), uid))?;
if p == "".into() {
eprintln!("Giving up.");
continue 'key;
}
match key.unlock(p.clone()) {
Ok(()) => break Some(p),
Err(err) => {
eprintln!("Failed to unlock key: {}", err);
}
}
}
} else {
None
};
return Ok((Box::new(key), password));
}
Err(anyhow!("Key not managed by keystore."))
};
'next_cert: for cert in certs {
let cert = cert.borrow();
let vc = match cert.with_policy(self.policy, self.time) {
Ok(vc) => vc,
Err(err) => {
return Err(
err.context(format!("Found no suitable key on {}", tsk)));
err.context(format!("Found no suitable key on {}", cert)));
}
};
@ -1060,51 +1152,26 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
continue;
}
let key = ka.key();
if let Some(secret) = key.optional_secret() {
let (unencrypted, password) = match secret {
SecretKeyMaterial::Encrypted(ref e) => {
// try passwords from already existing keys
match keys.iter().find_map(|(_, password)| {
password.as_ref().and_then(
|p| e.decrypt(key.pk_algo(), p).ok()
.map(|key| (key, p.clone())))
}) {
Some((unencrypted, password)) =>
(unencrypted, Some(password)),
None => {
let password = password::prompt_to_unlock(
&format!("key {}/{}", tsk, key))?;
(
e.decrypt(key.pk_algo(), &password)
.map_err(|_| anyhow!("Incorrect password."))?,
Some(password),
)
}
}
}
SecretKeyMaterial::Unencrypted(ref u) => (u.clone(), None),
};
keys.push((
Box::new(
crypto::KeyPair::new(key.clone(), unencrypted).unwrap()
),
password,
));
if let Ok((key, password)) = try_tsk(&ka, &keys) {
keys.push((key, password));
continue 'next_cert;
}
if let Ok((key, password)) = try_keystore(&ka) {
keys.push((key, password));
continue 'next_cert;
}
}
// We didn't get a key. Lint the cert.
let time = chrono::DateTime::<chrono::offset::Utc>::from(self.time);
let mut context = Vec::new();
for (fpr, [not_alive, revoked, not_supported]) in bad {
let id: String = if fpr == tsk.fingerprint() {
let id: String = if fpr == cert.fingerprint() {
fpr.to_string()
} else {
format!("{}/{}", tsk.fingerprint(), fpr)
format!("{}/{}", cert.fingerprint(), fpr)
};
let preface = if ! self.time_is_now {
@ -1132,12 +1199,12 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
if context.is_empty() {
return Err(anyhow::anyhow!(
format!("Found no suitable key on {}", tsk)));
format!("Found no suitable key on {}", cert)));
} else {
let context = context.join("\n");
return Err(
anyhow::anyhow!(
format!("Found no suitable key on {}", tsk))
format!("Found no suitable key on {}", cert))
.context(context));
}
}