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:
Neal H. Walfield 2024-11-06 10:57:14 +01:00
parent b5b27aa366
commit f139b50f24
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
13 changed files with 526 additions and 198 deletions

4
NEWS
View File

@ -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

View File

@ -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",
],

View File

@ -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()

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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;

View File

@ -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") ]

View File

@ -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(())
}

View File

@ -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,
};

View File

@ -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;

View File

@ -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.

View File

@ -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.

View 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());
}
}
}
}