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:
parent
42126b5534
commit
bbe350118a
5
NEWS
5
NEWS
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
149
src/sq.rs
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user