Change sq key subkey export to require the certificate to export.
- `sq key subkey export` currently takes a list of keys to export. This is ambiguous if a key is associated with multiple certificates. - Add a new required parameter, `--cert`, which specifies what certificate to export. The specified keys must be attached to that certificate under the NULL policy. - This change means that `sq key subkey export` can only export a single certificate at a time. - As the implementations of `sq key export` and `sq key subkey export` have diverged, don't try to consolidate them any more. - Fixes #386.
This commit is contained in:
parent
b5b27aa366
commit
f139b50f24
4
NEWS
4
NEWS
@ -10,6 +10,10 @@
|
||||
- `sq decrypt` now deletes the output file on failure.
|
||||
- Add a global option, `--policy-as-of`, that selects the
|
||||
cryptographic policy as of the specified time.
|
||||
- `sq key subkey export` takes an additional argument, `--cert`,
|
||||
which is required. The specified keys must be attached to that
|
||||
certificate. This ensures that if a key is attached to multiple
|
||||
certificates, the correct certificate is exported.
|
||||
|
||||
* Changes in 0.39.0
|
||||
** Notable changes
|
||||
|
@ -1,16 +1,26 @@
|
||||
use clap::Args;
|
||||
|
||||
use sequoia_openpgp as openpgp;
|
||||
use openpgp::KeyHandle;
|
||||
|
||||
use crate::cli::examples;
|
||||
use examples::Action;
|
||||
use examples::Actions;
|
||||
use examples::Example;
|
||||
use examples::Setup;
|
||||
|
||||
use crate::cli::types::CertDesignators;
|
||||
use crate::cli::types::ClapData;
|
||||
use crate::cli::types::FileOrStdout;
|
||||
use crate::cli::types::KeyDesignators;
|
||||
use crate::cli::types::cert_designator;
|
||||
use crate::cli::types::key_designator;
|
||||
|
||||
pub struct AdditionalDocs {}
|
||||
|
||||
impl key_designator::AdditionalDocs for AdditionalDocs {
|
||||
fn help(_arg: &'static str, _help: &'static str) -> clap::builder::StyledStr {
|
||||
"Export the secret key material for the specified primary key \
|
||||
or subkey".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[clap(
|
||||
@ -23,26 +33,26 @@ material is available, it may not be exportable. For instance, secret \
|
||||
key material stored on a hardware security module usually cannot be \
|
||||
exported from the device.
|
||||
|
||||
The entire certificate is exported, but only the specified key's \
|
||||
secret key material is exported. An error is returned if the secret \
|
||||
key material for the specified key is not available.
|
||||
|
||||
If you want to export all secret key material associated with a \
|
||||
certificate, use `sq key export`.
|
||||
",
|
||||
after_help = EXAMPLES,
|
||||
)]
|
||||
pub struct Command {
|
||||
#[clap(
|
||||
long,
|
||||
value_name = "FINGERPRINT|KEYID",
|
||||
required = true,
|
||||
help = "\
|
||||
Export the secret key material for the specified key, and its certificate",
|
||||
long_help = "\
|
||||
Export the specified key.
|
||||
#[command(flatten)]
|
||||
pub cert: CertDesignators<
|
||||
cert_designator::CertUserIDEmailFileArgs,
|
||||
cert_designator::NoPrefix,
|
||||
cert_designator::OneValue>,
|
||||
|
||||
The entire certificate is exported, but only the specified key's \
|
||||
secret key material is exported. An error is returned if the secret \
|
||||
key material for the specified key is not available.",
|
||||
)]
|
||||
pub key: Vec<KeyHandle>,
|
||||
#[command(flatten)]
|
||||
pub keys: KeyDesignators<
|
||||
key_designator::DefaultOptions,
|
||||
AdditionalDocs>,
|
||||
|
||||
#[clap(
|
||||
default_value_t = FileOrStdout::default(),
|
||||
@ -72,6 +82,7 @@ Export Alice's signing-capable and encryption-capable subkeys, but not \
|
||||
her primary key or her authentication-capable subkey.",
|
||||
command: &[
|
||||
"sq", "key", "subkey", "export",
|
||||
"--cert=EB28F26E2739A4870ECC47726F0073F60FD0CBF0",
|
||||
"--key=42020B87D51877E5AF8D272124F3955B0B8DECC8",
|
||||
"--key=74DCDEAF17D9B995679EB52BA6E65EA2C8497728",
|
||||
],
|
||||
|
@ -428,6 +428,11 @@ impl<Arguments, Prefix, Options, Doc> CertDesignators<Arguments, Prefix, Options
|
||||
self.designators.is_empty()
|
||||
}
|
||||
|
||||
/// Like `Vec::len`.
|
||||
pub fn len(&self) -> usize {
|
||||
self.designators.len()
|
||||
}
|
||||
|
||||
/// Iterates over the certificate designators.
|
||||
pub fn iter(&self) -> impl Iterator<Item=&CertDesignator> {
|
||||
self.designators.iter()
|
||||
|
@ -1,16 +1,102 @@
|
||||
use anyhow::Context;
|
||||
|
||||
use sequoia_openpgp as openpgp;
|
||||
use openpgp::Cert;
|
||||
use openpgp::Packet;
|
||||
use openpgp::cert::amalgamation::key::PrimaryKey;
|
||||
use openpgp::serialize::Serialize;
|
||||
|
||||
use crate::Result;
|
||||
use crate::Sq;
|
||||
use crate::cli;
|
||||
use crate::common::key::export;
|
||||
use crate::Result;
|
||||
|
||||
pub fn dispatch(sq: Sq, command: cli::key::export::Command)
|
||||
-> Result<()>
|
||||
{
|
||||
let certs =
|
||||
sq.resolve_certs_or_fail(&command.certs, sequoia_wot::FULLY_TRUSTED)?
|
||||
.into_iter()
|
||||
.map(|c| c.key_handle())
|
||||
.collect();
|
||||
let ks = sq.key_store_or_else()?;
|
||||
let mut ks = ks.lock().unwrap();
|
||||
|
||||
export::export(sq, certs, Vec::new(), command.output, command.binary)
|
||||
let certs = sq.resolve_certs_or_fail(
|
||||
&command.certs, sequoia_wot::FULLY_TRUSTED)?;
|
||||
|
||||
// Note: Sq::resolve_certs already deduped the certificates.
|
||||
let mut results = Vec::new();
|
||||
|
||||
for cert in certs.into_iter() {
|
||||
let vc = Cert::with_policy(&cert, sq.policy, sq.time)
|
||||
.with_context(|| {
|
||||
format!("The certificate {} is not valid under the \
|
||||
current policy. Use sq key subkey export --key \
|
||||
to export specific keys.",
|
||||
cert.fingerprint())
|
||||
})?;
|
||||
|
||||
let mut secret_keys: Vec<Packet> = Vec::new();
|
||||
let mut errs = Vec::new();
|
||||
|
||||
for ka in vc.keys().into_iter().collect::<Vec<_>>() {
|
||||
if ka.has_secret() {
|
||||
// We already have the secret key material.
|
||||
continue;
|
||||
}
|
||||
|
||||
let key_handle = ka.key_handle();
|
||||
|
||||
for mut remote in ks.find_key(key_handle)? {
|
||||
match remote.export() {
|
||||
Ok(secret_key) => {
|
||||
if ka.primary() {
|
||||
secret_keys.push(
|
||||
secret_key.role_into_primary().into());
|
||||
} else {
|
||||
secret_keys.push(
|
||||
secret_key.role_into_subordinate().into());
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
errs.push((ka.fingerprint(), err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if secret_keys.is_empty() {
|
||||
for (fpr, err) in errs.into_iter() {
|
||||
wprintln!("Exporting {}: {}", fpr, err);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to export {}: no secret key material is available",
|
||||
cert.fingerprint()));
|
||||
}
|
||||
|
||||
let cert = cert.insert_packets(secret_keys)?;
|
||||
results.push(cert);
|
||||
}
|
||||
|
||||
let mut output = command.output.for_secrets().create_safe(&sq)?;
|
||||
|
||||
if command.binary {
|
||||
for cert in results.into_iter() {
|
||||
cert.as_tsk().serialize(&mut output)
|
||||
.with_context(|| {
|
||||
format!("Serializing {}", cert.fingerprint())
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
let mut output = openpgp::armor::Writer::new(
|
||||
output,
|
||||
openpgp::armor::Kind::SecretKey)?;
|
||||
|
||||
for cert in results.into_iter() {
|
||||
cert.as_tsk().serialize(&mut output)
|
||||
.with_context(|| {
|
||||
format!("Serializing {}", cert.fingerprint())
|
||||
})?;
|
||||
}
|
||||
|
||||
output.finalize()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,12 +1,100 @@
|
||||
use sequoia_openpgp as openpgp;
|
||||
use openpgp::cert::amalgamation::key::PrimaryKey;
|
||||
use openpgp::cert::Cert;
|
||||
use openpgp::packet::Packet;
|
||||
use openpgp::serialize::Serialize;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::Result;
|
||||
use crate::Sq;
|
||||
use crate::common::key::export;
|
||||
use crate::sq::NULL_POLICY;
|
||||
|
||||
pub fn dispatch(sq: Sq, command: crate::cli::key::subkey::export::Command)
|
||||
-> Result<()>
|
||||
{
|
||||
assert!(! command.key.is_empty());
|
||||
let ks = sq.key_store_or_else()?;
|
||||
let mut ks = ks.lock().unwrap();
|
||||
|
||||
export(sq, vec![], command.key, command.output, command.binary)
|
||||
assert_eq!(command.cert.len(), 1);
|
||||
assert!(command.keys.len() > 0);
|
||||
|
||||
let (mut cert, cert_source)
|
||||
= sq.resolve_cert(&command.cert, sequoia_wot::FULLY_TRUSTED)?;
|
||||
|
||||
// Yes, we unconditionally use the NULL policy. This is safe as
|
||||
// the user explicitly named both the certificate, and keys to
|
||||
// export.
|
||||
let vc = Cert::with_policy(&cert, &NULL_POLICY, sq.time)
|
||||
.with_context(|| {
|
||||
format!("The certificate {} is not valid under the \
|
||||
null policy.",
|
||||
cert.fingerprint())
|
||||
})?;
|
||||
|
||||
let kas = sq.resolve_keys(&vc, &cert_source, &command.keys, true)?;
|
||||
|
||||
let mut secret_keys: Vec<Packet> = Vec::new();
|
||||
let mut errs = Vec::new();
|
||||
|
||||
for ka in kas {
|
||||
if ka.has_secret() {
|
||||
// We already have the secret key material.
|
||||
continue;
|
||||
}
|
||||
|
||||
let key_handle = ka.key_handle();
|
||||
|
||||
for mut remote in ks.find_key(key_handle)? {
|
||||
match remote.export() {
|
||||
Ok(secret_key) => {
|
||||
if ka.primary() {
|
||||
secret_keys.push(
|
||||
secret_key.role_into_primary().into());
|
||||
} else {
|
||||
secret_keys.push(
|
||||
secret_key.role_into_subordinate().into());
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
errs.push((ka.fingerprint(), err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if secret_keys.is_empty() {
|
||||
for (fpr, err) in errs.into_iter() {
|
||||
wprintln!("Exporting {}: {}", fpr, err);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to export {}: no secret key material is available",
|
||||
cert.fingerprint()));
|
||||
}
|
||||
|
||||
cert = cert.insert_packets(secret_keys)?;
|
||||
|
||||
let mut output = command.output.for_secrets().create_safe(&sq)?;
|
||||
|
||||
if command.binary {
|
||||
cert.as_tsk().serialize(&mut output)
|
||||
.with_context(|| {
|
||||
format!("Serializing {}", cert.fingerprint())
|
||||
})?;
|
||||
} else {
|
||||
let mut output = openpgp::armor::Writer::new(
|
||||
output,
|
||||
openpgp::armor::Kind::SecretKey)?;
|
||||
|
||||
cert.as_tsk().serialize(&mut output)
|
||||
.with_context(|| {
|
||||
format!("Serializing {}", cert.fingerprint())
|
||||
})?;
|
||||
|
||||
output.finalize()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -19,9 +19,6 @@ pub use expire::expire;
|
||||
pub mod delete;
|
||||
pub use delete::delete;
|
||||
|
||||
pub mod export;
|
||||
pub use export::export;
|
||||
|
||||
pub mod password;
|
||||
pub use password::password;
|
||||
|
||||
|
@ -18,12 +18,12 @@ use sequoia_cert_store::StoreUpdate;
|
||||
|
||||
use sequoia_wot as wot;
|
||||
|
||||
use crate::Sq;
|
||||
use crate::cli::types::CertDesignators;
|
||||
use crate::cli::types::Expiration;
|
||||
use crate::cli::types::FileOrStdout;
|
||||
use crate::cli::types::CertDesignators;
|
||||
use crate::cli::types::cert_designator;
|
||||
use crate::cli::types::KeyDesignators;
|
||||
use crate::Sq;
|
||||
use crate::cli::types::cert_designator;
|
||||
use crate::sq::GetKeysOptions;
|
||||
|
||||
/// cert must resolve to a single certificate.
|
||||
@ -55,7 +55,7 @@ where P: cert_designator::ArgumentPrefix,
|
||||
let vc = cert.with_policy(sq.policy, sq.time)?;
|
||||
|
||||
let keys = if let Some(keys) = keys {
|
||||
sq.resolve_keys(&vc, &cert_handle, &keys)?
|
||||
sq.resolve_keys(&vc, &cert_handle, &keys, false)?
|
||||
} else {
|
||||
// The primary key.
|
||||
vec![ vc.keys().next().expect("have a primary key") ]
|
||||
|
@ -1,133 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use sequoia_openpgp as openpgp;
|
||||
use openpgp::cert::amalgamation::key::PrimaryKey;
|
||||
use openpgp::cert::Cert;
|
||||
use openpgp::Fingerprint;
|
||||
use openpgp::KeyHandle;
|
||||
use openpgp::packet::Packet;
|
||||
use openpgp::serialize::Serialize;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::Sq;
|
||||
use crate::Result;
|
||||
use crate::cli::types::FileOrStdout;
|
||||
|
||||
const NULL: openpgp::policy::NullPolicy =
|
||||
openpgp::policy::NullPolicy::new();
|
||||
|
||||
pub fn export(sq: Sq,
|
||||
certs: Vec<KeyHandle>,
|
||||
keys: Vec<KeyHandle>,
|
||||
output: FileOrStdout,
|
||||
binary: bool)
|
||||
-> Result<()>
|
||||
{
|
||||
let ks = sq.key_store_or_else()?;
|
||||
let mut ks = ks.lock().unwrap();
|
||||
|
||||
// If the user asks for multiple keys from the same certificate,
|
||||
// then we only want to export the certificate once.
|
||||
let mut results: BTreeMap<Fingerprint, Cert> = BTreeMap::new();
|
||||
|
||||
for (export_cert, export) in certs.into_iter().map(|kh| (true, kh))
|
||||
.chain(keys.into_iter().map(|kh| (false, kh)))
|
||||
{
|
||||
let mut cert = sq.lookup_one(&export, None, true)?;
|
||||
if let Some(c) = results.remove(&cert.fingerprint()) {
|
||||
cert = c;
|
||||
}
|
||||
|
||||
let vc = Cert::with_policy(&cert, sq.policy, sq.time)
|
||||
.or_else(|err| {
|
||||
if export_cert {
|
||||
Err(err)
|
||||
} else {
|
||||
// When exporting by --key, fallback to the null
|
||||
// policy. It should be possible to export old
|
||||
// keys, even if the certificate is not considered
|
||||
// safe any more.
|
||||
Cert::with_policy(&cert, &NULL, sq.time)
|
||||
}
|
||||
})
|
||||
.with_context(|| {
|
||||
format!("The certificate {} is not valid under the \
|
||||
current policy. Use --key to export \
|
||||
specific keys.",
|
||||
cert.fingerprint())
|
||||
})?;
|
||||
|
||||
let mut secret_keys: Vec<Packet> = Vec::new();
|
||||
for loud in [false, true] {
|
||||
for key in vc.keys() {
|
||||
if key.has_secret() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key_handle = key.key_handle();
|
||||
|
||||
if ! export_cert && ! key_handle.aliases(&export) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for mut remote in ks.find_key(key_handle)? {
|
||||
match remote.export() {
|
||||
Ok(secret_key) => {
|
||||
if key.primary() {
|
||||
secret_keys.push(
|
||||
secret_key.role_into_primary().into());
|
||||
} else {
|
||||
secret_keys.push(
|
||||
secret_key.role_into_subordinate().into());
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
if loud {
|
||||
wprintln!("Exporting {}: {}",
|
||||
key.fingerprint(), err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if loud {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Failed to export {}: no secret key material is available",
|
||||
cert.fingerprint()));
|
||||
} else if ! secret_keys.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let cert = cert.insert_packets(secret_keys)?;
|
||||
results.insert(cert.fingerprint(), cert);
|
||||
}
|
||||
|
||||
let mut output = output.for_secrets().create_safe(&sq)?;
|
||||
|
||||
if binary {
|
||||
for (_fpr, cert) in results.into_iter() {
|
||||
cert.as_tsk().serialize(&mut output)
|
||||
.with_context(|| {
|
||||
format!("Serializing {}", cert.fingerprint())
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
let mut output = openpgp::armor::Writer::new(
|
||||
output,
|
||||
openpgp::armor::Kind::SecretKey)?;
|
||||
|
||||
for (_fpr, cert) in results.into_iter() {
|
||||
cert.as_tsk().serialize(&mut output)
|
||||
.with_context(|| {
|
||||
format!("Serializing {}", cert.fingerprint())
|
||||
})?;
|
||||
}
|
||||
|
||||
output.finalize()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
33
src/sq.rs
33
src/sq.rs
@ -60,7 +60,7 @@ use crate::print_error_chain;
|
||||
|
||||
const TRACE: bool = false;
|
||||
|
||||
static NULL_POLICY: NullPolicy = NullPolicy::new();
|
||||
pub static NULL_POLICY: NullPolicy = NullPolicy::new();
|
||||
|
||||
/// Flags for Sq::get_keys and related functions.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@ -2199,7 +2199,8 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
|
||||
pub fn resolve_keys<'a, KOptions, KDoc>(
|
||||
&self,
|
||||
vc: &ValidCert<'a>, cert_handle: &FileStdinOrKeyHandle,
|
||||
keys: &KeyDesignators<KOptions, KDoc>)
|
||||
keys: &KeyDesignators<KOptions, KDoc>,
|
||||
return_hard_revoked: bool)
|
||||
-> Result<Vec<ValidErasedKeyAmalgamation<'a, PublicParts>>>
|
||||
{
|
||||
assert!(keys.len() > 0);
|
||||
@ -2222,19 +2223,21 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
|
||||
|
||||
// Make sure it is not hard revoked.
|
||||
let mut hard_revoked = false;
|
||||
if let RevocationStatus::Revoked(sigs)
|
||||
= ka.revocation_status()
|
||||
{
|
||||
for sig in sigs {
|
||||
let reason = sig.reason_for_revocation();
|
||||
hard_revoked = if let Some((reason, _)) = reason {
|
||||
reason.revocation_type() == RevocationType::Hard
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if ! return_hard_revoked {
|
||||
if let RevocationStatus::Revoked(sigs)
|
||||
= ka.revocation_status()
|
||||
{
|
||||
for sig in sigs {
|
||||
let reason = sig.reason_for_revocation();
|
||||
hard_revoked = if let Some((reason, _)) = reason {
|
||||
reason.revocation_type() == RevocationType::Hard
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if hard_revoked {
|
||||
break;
|
||||
if hard_revoked {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2258,7 +2261,7 @@ impl<'store: 'rstore, 'rstore> Sq<'store, 'rstore> {
|
||||
|
||||
let fingerprint = ka.fingerprint();
|
||||
|
||||
let err = match ka.with_policy(self.policy, self.time) {
|
||||
let err = match ka.with_policy(vc.policy(), vc.time()) {
|
||||
Ok(_) => unreachable!("key magically became usable"),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
@ -17,6 +17,7 @@ mod integration {
|
||||
mod sq_key_subkey;
|
||||
mod sq_key_subkey_delete;
|
||||
mod sq_key_subkey_expire;
|
||||
mod sq_key_subkey_export;
|
||||
mod sq_key_subkey_password;
|
||||
mod sq_key_userid;
|
||||
mod sq_pki;
|
||||
|
@ -1039,10 +1039,12 @@ impl Sq {
|
||||
}
|
||||
|
||||
/// Exports the specified keys.
|
||||
pub fn key_subkey_export<H>(&self, khs: Vec<H>) -> Vec<Cert>
|
||||
where H: Into<KeyHandle>
|
||||
pub fn key_subkey_export<H1, H2>(&self, cert: H1, khs: Vec<H2>)
|
||||
-> Cert
|
||||
where H1: Into<KeyHandle>,
|
||||
H2: Into<KeyHandle>
|
||||
{
|
||||
self.key_subkey_export_maybe(khs)
|
||||
self.key_subkey_export_maybe(cert, khs)
|
||||
.expect("can export key")
|
||||
}
|
||||
|
||||
@ -1051,24 +1053,23 @@ impl Sq {
|
||||
/// Returns an error if `sq key subkey export` fails. This
|
||||
/// happens if the key is known, but the key store doesn't manage
|
||||
/// any of its secret key material.
|
||||
pub fn key_subkey_export_maybe<H>(&self, khs: Vec<H>) -> Result<Vec<Cert>>
|
||||
where H: Into<KeyHandle>
|
||||
pub fn key_subkey_export_maybe<H1, H2>(&self, cert: H1, khs: Vec<H2>)
|
||||
-> Result<Cert>
|
||||
where H1: Into<KeyHandle>,
|
||||
H2: Into<KeyHandle>,
|
||||
{
|
||||
let cert = cert.into();
|
||||
|
||||
let mut cmd = self.command();
|
||||
cmd.args([ "key", "subkey", "export" ]);
|
||||
cmd.arg("--cert").arg(cert.to_string());
|
||||
for kh in khs.into_iter() {
|
||||
let kh: KeyHandle = kh.into();
|
||||
cmd.arg("--key").arg(kh.to_string());
|
||||
}
|
||||
let output = self.run(cmd, None);
|
||||
|
||||
if output.status.success() {
|
||||
let parser = CertParser::from_bytes(&output.stdout)
|
||||
.expect("can parse certificate");
|
||||
Ok(parser.collect::<Result<Vec<Cert>>>()?)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("sq key export returned an error"))
|
||||
}
|
||||
self.handle_cert_output(
|
||||
output, cert.into(), Some(PathBuf::from("-").as_path()), true)
|
||||
}
|
||||
|
||||
/// Run `sq key subkey revoke` and return the revocation certificate.
|
||||
|
@ -59,9 +59,8 @@ fn sq_key_import_export() -> Result<()>
|
||||
}
|
||||
|
||||
// Export the selection.
|
||||
let got = sq.key_subkey_export(selection.clone());
|
||||
assert_eq!(got.len(), 1);
|
||||
let got = got.into_iter().next().unwrap();
|
||||
let got = sq.key_subkey_export(
|
||||
cert.key_handle(), selection.clone());
|
||||
|
||||
// Make sure we got exactly what we asked for; no
|
||||
// more, no less.
|
||||
|
266
tests/integration/sq_key_subkey_export.rs
Normal file
266
tests/integration/sq_key_subkey_export.rs
Normal file
@ -0,0 +1,266 @@
|
||||
use sequoia_openpgp as openpgp;
|
||||
use openpgp::Cert;
|
||||
use openpgp::cert::amalgamation::ValidAmalgamation;
|
||||
use openpgp::parse::Parse;
|
||||
use openpgp::types::RevocationStatus;
|
||||
use openpgp::types::RevocationType;
|
||||
|
||||
use super::common::power_set;
|
||||
use super::common::STANDARD_POLICY;
|
||||
use super::common::Sq;
|
||||
|
||||
/// Check that invalid syntax is caught.
|
||||
#[test]
|
||||
fn sq_key_subkey_export_syntax() {
|
||||
let sq = Sq::new();
|
||||
|
||||
let userid = "alice <alice@example.org>";
|
||||
let (cert, cert_path, _rev_path)
|
||||
= sq.key_generate(&[], &[userid]);
|
||||
sq.key_import(&cert_path);
|
||||
|
||||
// A trivial test to make sure it works.
|
||||
sq.key_subkey_export(cert.key_handle(), vec![ cert.key_handle() ]);
|
||||
|
||||
let fpr = cert.fingerprint().to_string();
|
||||
let subkey = cert.keys().subkeys().next().unwrap().fingerprint().to_string();
|
||||
|
||||
// Make sure "--key" is required.
|
||||
let mut cmd = sq.command();
|
||||
cmd.args([
|
||||
"key", "subkey", "export",
|
||||
"--cert", &fpr,
|
||||
]);
|
||||
sq.run(cmd, false);
|
||||
|
||||
// Make sure "--cert" is specified at most once.
|
||||
let mut cmd = sq.command();
|
||||
cmd.args([
|
||||
"key", "subkey", "export",
|
||||
"--cert", &fpr,
|
||||
"--cert", &fpr,
|
||||
"--key", &fpr,
|
||||
]);
|
||||
sq.run(cmd, false);
|
||||
|
||||
// Make sure arguments from the "--cert" family are specified at
|
||||
// most once.
|
||||
let mut cmd = sq.command();
|
||||
cmd.args([
|
||||
"key", "subkey", "export",
|
||||
"--cert", &fpr,
|
||||
"--userid", userid,
|
||||
"--key", &fpr,
|
||||
]);
|
||||
sq.run(cmd, false);
|
||||
|
||||
// Make sure "--cert" is a primary key.
|
||||
let mut cmd = sq.command();
|
||||
cmd.args([
|
||||
"key", "subkey", "export",
|
||||
"--cert", &subkey,
|
||||
"--key", &fpr,
|
||||
]);
|
||||
sq.run(cmd, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn by_email() {
|
||||
let mut sq = Sq::new();
|
||||
|
||||
let userid = "alice <alice@example.org>";
|
||||
let (cert1, cert1_path, _rev_path)
|
||||
= sq.key_generate(&[], &[userid]);
|
||||
sq.key_import(&cert1_path);
|
||||
|
||||
let (cert2, cert2_path, _rev_path)
|
||||
= sq.key_generate(&[], &[userid]);
|
||||
sq.key_import(&cert2_path);
|
||||
|
||||
for i in [0, 1, 2] {
|
||||
let mut cmd = sq.command();
|
||||
cmd.args([
|
||||
"key", "subkey", "export",
|
||||
"--email", "alice@example.org",
|
||||
"--key", &cert1.fingerprint().to_string(),
|
||||
]);
|
||||
let output = sq.run(cmd, i == 1);
|
||||
match i {
|
||||
0 => {
|
||||
// Not linked.
|
||||
assert!(! output.status.success());
|
||||
|
||||
// Link cert1.
|
||||
sq.tick(1);
|
||||
sq.pki_link_add(&[], cert1.key_handle(), &[userid]);
|
||||
}
|
||||
1 => {
|
||||
assert!(output.status.success());
|
||||
let cert = Cert::from_bytes(&output.stdout)
|
||||
.expect("can read cert");
|
||||
assert!(cert.is_tsk());
|
||||
|
||||
// Link cert2.
|
||||
sq.tick(1);
|
||||
sq.pki_link_add(&[], cert2.key_handle(), &[userid]);
|
||||
}
|
||||
2 => {
|
||||
// --email is now ambiguous.
|
||||
assert!(! output.status.success());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoked_userid() {
|
||||
// Make sure we can export keys from a certificate where all user
|
||||
// IDs are revoked.
|
||||
|
||||
let sq = Sq::new();
|
||||
|
||||
let cert_path = sq.test_data()
|
||||
.join("keys")
|
||||
.join("retired-userid.pgp");
|
||||
|
||||
let cert = Cert::from_file(&cert_path).expect("can read");
|
||||
let vc = cert.with_policy(STANDARD_POLICY, sq.now())
|
||||
.expect("valid cert");
|
||||
|
||||
let ua = vc.userids().next().expect("have a user ID");
|
||||
if let RevocationStatus::Revoked(_) = ua.revocation_status() {
|
||||
} else {
|
||||
panic!("User ID should be revoked, but isn't.");
|
||||
};
|
||||
|
||||
sq.key_import(&cert_path);
|
||||
|
||||
let keys = cert.keys().map(|k| k.fingerprint()).collect::<Vec<_>>();
|
||||
for selection in power_set(&keys) {
|
||||
let exported = sq.key_subkey_export(
|
||||
vc.key_handle(), selection.clone());
|
||||
for k in exported.keys() {
|
||||
if selection.contains(&k.fingerprint()) {
|
||||
assert!(k.has_secret());
|
||||
} else {
|
||||
assert!(! k.has_secret());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hard_revoked_subkey() {
|
||||
// Make sure we can export subkeys that are hard revoked.
|
||||
|
||||
let sq = Sq::new();
|
||||
|
||||
let cert_path = sq.test_data()
|
||||
.join("keys")
|
||||
.join("hard-revoked-subkey.pgp");
|
||||
|
||||
let cert = Cert::from_file(&cert_path).expect("can read");
|
||||
let vc = cert.with_policy(STANDARD_POLICY, sq.now())
|
||||
.expect("valid cert");
|
||||
|
||||
let mut hard_revoked_subkeys = 0;
|
||||
for ka in vc.keys() {
|
||||
if let RevocationStatus::Revoked(sigs) = ka.revocation_status() {
|
||||
for sig in sigs {
|
||||
let reason = sig.reason_for_revocation();
|
||||
let bad = if let Some((reason, _)) = reason {
|
||||
reason.revocation_type() == RevocationType::Hard
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if bad {
|
||||
hard_revoked_subkeys += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(hard_revoked_subkeys, 1);
|
||||
|
||||
sq.key_import(&cert_path);
|
||||
|
||||
let keys = cert.keys().map(|k| k.fingerprint()).collect::<Vec<_>>();
|
||||
for selection in power_set(&keys) {
|
||||
let exported = sq.key_subkey_export(
|
||||
vc.key_handle(), selection.clone());
|
||||
for k in exported.keys() {
|
||||
if selection.contains(&k.fingerprint()) {
|
||||
assert!(k.has_secret());
|
||||
} else {
|
||||
assert!(! k.has_secret());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_sha1() {
|
||||
// Make sure we can export subkeys that are not valid under the
|
||||
// standard policy.
|
||||
|
||||
let sq = Sq::new();
|
||||
|
||||
let cert_path = sq.test_data()
|
||||
.join("keys")
|
||||
.join("only-sha1-priv.pgp");
|
||||
|
||||
let cert = Cert::from_file(&cert_path).expect("can read");
|
||||
|
||||
// It shouldn't be valid under the standard policy.
|
||||
assert!(cert.with_policy(STANDARD_POLICY, sq.now()).is_err());
|
||||
|
||||
sq.key_import(&cert_path);
|
||||
|
||||
let keys = cert.keys().map(|k| k.fingerprint()).collect::<Vec<_>>();
|
||||
for selection in power_set(&keys) {
|
||||
let exported = sq.key_subkey_export(
|
||||
cert.key_handle(), selection.clone());
|
||||
for k in exported.keys() {
|
||||
if selection.contains(&k.fingerprint()) {
|
||||
assert!(k.has_secret());
|
||||
} else {
|
||||
assert!(! k.has_secret());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sha1_subkey() {
|
||||
// Make sure we can export subkeys that are not valid under the
|
||||
// standard policy.
|
||||
|
||||
let sq = Sq::new();
|
||||
|
||||
let cert_path = sq.test_data()
|
||||
.join("keys")
|
||||
.join("sha1-subkey-priv.pgp");
|
||||
|
||||
let cert = Cert::from_file(&cert_path).expect("can read");
|
||||
|
||||
let vc = cert.with_policy(STANDARD_POLICY, sq.now())
|
||||
.expect("valid under standard policy");
|
||||
assert!(cert.keys().count() > vc.keys().count());
|
||||
|
||||
sq.key_import(&cert_path);
|
||||
|
||||
let keys = cert.keys().map(|k| k.fingerprint()).collect::<Vec<_>>();
|
||||
for selection in power_set(&keys) {
|
||||
let exported = sq.key_subkey_export(
|
||||
cert.key_handle(), selection.clone());
|
||||
for k in exported.keys() {
|
||||
if selection.contains(&k.fingerprint()) {
|
||||
assert!(k.has_secret());
|
||||
} else {
|
||||
assert!(! k.has_secret());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user