Implement sq key export.
This commit is contained in:
parent
e75ad72c65
commit
006482b352
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -3210,9 +3210,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sequoia-keystore"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76eaefe78ca373001382f2434ca021145d9b702ba52c04cebc398b8e5956d28a"
|
||||
checksum = "aa77ac702f6be1489580eb092aa5acae36050db04fa5ae445238a84591e1ad7a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"capnp",
|
||||
@ -3272,9 +3272,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sequoia-keystore-softkeys"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1affc41cb24e491cd38a0a47928bc221b7db4318f0e4b17c49131c9a4a13eb1"
|
||||
checksum = "6f9707371cae085b6e1cac9e17bf94a19efcdc04da4dba5cbda1cb8f8c0a655a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
@ -89,6 +89,7 @@ pub enum Subcommands {
|
||||
List(ListCommand),
|
||||
Generate(GenerateCommand),
|
||||
Import(ImportCommand),
|
||||
Export(ExportCommand),
|
||||
Password(PasswordCommand),
|
||||
Expire(expire::Command),
|
||||
Revoke(RevokeCommand),
|
||||
@ -335,6 +336,69 @@ pub struct ImportCommand {
|
||||
pub file: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
const EXPORT_EXAMPLES: Actions = Actions {
|
||||
actions: &[
|
||||
Action::Example(Example {
|
||||
comment: "\
|
||||
Import a certificate.",
|
||||
command: &[
|
||||
"sq", "key", "import", "alice-secret.pgp",
|
||||
],
|
||||
}),
|
||||
Action::Example(Example {
|
||||
comment: "\
|
||||
Export Alice's certificate with all available secret key material.",
|
||||
command: &[
|
||||
"sq", "key", "export",
|
||||
"--cert", "EB28F26E2739A4870ECC47726F0073F60FD0CBF0",
|
||||
],
|
||||
}),
|
||||
Action::Example(Example {
|
||||
comment: "\
|
||||
Export Alice's signing-capable and encryption-capable subkeys, but not \
|
||||
her primary key or her authentication-capable subkey.",
|
||||
command: &[
|
||||
"sq", "key", "export",
|
||||
"--key", "42020B87D51877E5AF8D272124F3955B0B8DECC8",
|
||||
"--key", "74DCDEAF17D9B995679EB52BA6E65EA2C8497728",
|
||||
],
|
||||
}),
|
||||
]
|
||||
};
|
||||
test_examples!(sq_key_export, EXPORT_EXAMPLES);
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[clap(
|
||||
about = "Export keys from the key store",
|
||||
after_help = EXPORT_EXAMPLES,
|
||||
)]
|
||||
#[clap(group(ArgGroup::new("export").args(&["cert", "key"])))]
|
||||
pub struct ExportCommand {
|
||||
#[clap(
|
||||
long,
|
||||
value_name = "FINGERPRINT|KEYID",
|
||||
help = "Export the specified certificate",
|
||||
long_help = "Export the specified certificate by iterating over the \
|
||||
specified certificate's primary key and subkeys and \
|
||||
exporting any keys with secret key material. An \
|
||||
error is returned if the certificate does not contain \
|
||||
any secret key material.",
|
||||
)]
|
||||
pub cert: Vec<KeyHandle>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
value_name = "FINGERPRINT|KEYID",
|
||||
help = "Export the specified key",
|
||||
long_help = "Export the specified key. 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>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[clap(
|
||||
name = "password",
|
||||
|
@ -9,6 +9,8 @@ use adopt::adopt;
|
||||
mod attest_certifications;
|
||||
use attest_certifications::attest_certifications;
|
||||
mod expire;
|
||||
mod export;
|
||||
use export::export;
|
||||
mod import;
|
||||
use import::import;
|
||||
mod list;
|
||||
@ -29,6 +31,7 @@ pub fn dispatch(config: Config, command: cli::key::Command) -> Result<()>
|
||||
List(c) => list(config, c)?,
|
||||
Generate(c) => generate(config, c)?,
|
||||
Import(c) => import(config, c)?,
|
||||
Export(c) => export(config, c)?,
|
||||
Password(c) => password(config, c)?,
|
||||
Expire(c) => expire::dispatch(config, c)?,
|
||||
Userid(c) => userid::dispatch(config, c)?,
|
||||
|
117
src/commands/key/export.rs
Normal file
117
src/commands/key/export.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use sequoia_openpgp as openpgp;
|
||||
use openpgp::cert::amalgamation::key::PrimaryKey;
|
||||
use openpgp::cert::Cert;
|
||||
use openpgp::Fingerprint;
|
||||
use openpgp::packet::Packet;
|
||||
use openpgp::serialize::Serialize;
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::cli;
|
||||
use crate::Config;
|
||||
use crate::Result;
|
||||
|
||||
const NULL: openpgp::policy::NullPolicy =
|
||||
openpgp::policy::NullPolicy::new();
|
||||
|
||||
pub fn export(config: Config, command: cli::key::ExportCommand)
|
||||
-> Result<()>
|
||||
{
|
||||
let ks = config.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 certs: BTreeMap<Fingerprint, Cert> = BTreeMap::new();
|
||||
|
||||
for (export_cert, export) in command.cert.into_iter().map(|kh| (true, kh))
|
||||
.chain(command.key.into_iter().map(|kh| (false, kh)))
|
||||
{
|
||||
let mut cert = config.lookup_one(&export, None, true)?;
|
||||
if let Some(c) = certs.remove(&cert.fingerprint()) {
|
||||
cert = c;
|
||||
}
|
||||
|
||||
let vc = Cert::with_policy(&cert, config.policy, config.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, config.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 {
|
||||
eprintln!("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)?;
|
||||
certs.insert(cert.fingerprint(), cert);
|
||||
}
|
||||
|
||||
let mut output = openpgp::armor::Writer::new(
|
||||
std::io::stdout(),
|
||||
openpgp::armor::Kind::SecretKey)?;
|
||||
|
||||
for (_fpr, cert) in certs.into_iter() {
|
||||
cert.as_tsk().serialize(&mut output)
|
||||
.with_context(|| {
|
||||
format!("Serializing {}", cert.fingerprint())
|
||||
})?;
|
||||
}
|
||||
|
||||
output.finalize()?;
|
||||
|
||||
Ok(())
|
||||
}
|
172
tests/sq-key-import-export.rs
Normal file
172
tests/sq-key-import-export.rs
Normal file
@ -0,0 +1,172 @@
|
||||
use assert_cmd::Command;
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use sequoia_openpgp as openpgp;
|
||||
use openpgp::KeyID;
|
||||
use openpgp::Result;
|
||||
use openpgp::cert::prelude::*;
|
||||
use openpgp::packet::Key;
|
||||
use openpgp::parse::Parse;
|
||||
|
||||
mod integration {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sq_key_import_export() -> Result<()>
|
||||
{
|
||||
let dir = TempDir::new()?;
|
||||
|
||||
let rev_pgp = dir.path().join("rev.pgp");
|
||||
let rev_pgp_str = &*rev_pgp.to_string_lossy();
|
||||
|
||||
let key_pgp = dir.path().join("key.pgp");
|
||||
let key_pgp_str = &*key_pgp.to_string_lossy();
|
||||
|
||||
// Generate a few keys as red herrings.
|
||||
for _ in 0..10 {
|
||||
let mut cmd = Command::cargo_bin("sq")?;
|
||||
cmd.env("SEQUOIA_HOME", dir.path());
|
||||
cmd.args(["--force", "key", "generate",
|
||||
"--no-userids",
|
||||
"--rev-cert", &rev_pgp_str]);
|
||||
cmd.assert().success();
|
||||
}
|
||||
|
||||
// Generate a key in a file.
|
||||
let mut cmd = Command::cargo_bin("sq")?;
|
||||
cmd.env("SEQUOIA_HOME", dir.path());
|
||||
cmd.args(["key", "generate",
|
||||
"--no-userids",
|
||||
"--output", &key_pgp_str]);
|
||||
cmd.assert().success();
|
||||
|
||||
let cert = Cert::from_file(&key_pgp)?;
|
||||
assert!(cert.is_tsk());
|
||||
|
||||
// Import it into the key store.
|
||||
let mut cmd = Command::cargo_bin("sq")?;
|
||||
cmd.env("SEQUOIA_HOME", dir.path());
|
||||
cmd.args(["key", "import",
|
||||
&*key_pgp.to_string_lossy()]);
|
||||
cmd.assert().success();
|
||||
|
||||
// Export the whole certificate.
|
||||
for by_fpr in [true, false] {
|
||||
let mut cmd = Command::cargo_bin("sq")?;
|
||||
cmd.env("SEQUOIA_HOME", dir.path());
|
||||
cmd.args(["key", "export", "--cert",
|
||||
&if by_fpr {
|
||||
cert.fingerprint().to_string()
|
||||
} else {
|
||||
cert.keyid().to_string()
|
||||
}]);
|
||||
let result = cmd.assert().success();
|
||||
let stdout = &result.get_output().stdout;
|
||||
|
||||
let got = Cert::from_bytes(stdout).expect("cert");
|
||||
assert_eq!(cert, got);
|
||||
}
|
||||
|
||||
// Export each non-empty subset of keys.
|
||||
|
||||
eprintln!("Certificate:");
|
||||
for k in cert.keys() {
|
||||
eprintln!(" {}", k.fingerprint());
|
||||
}
|
||||
|
||||
// Returns the power set excluding the empty set.
|
||||
fn power_set<T: Clone>(set: &[T]) -> Vec<Vec<T>> {
|
||||
let mut power_set: Vec<Vec<T>> = Vec::new();
|
||||
for element in set.iter() {
|
||||
power_set.extend(
|
||||
power_set.clone().into_iter().map(|mut v: Vec<T>| {
|
||||
v.push(element.clone());
|
||||
v
|
||||
}));
|
||||
power_set.push(vec![ element.clone() ]);
|
||||
}
|
||||
power_set
|
||||
}
|
||||
|
||||
let keys: Vec<Key<_, _>> = cert.keys()
|
||||
.map(|k| {
|
||||
k.key().clone()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let key_ids = keys.iter().map(|k| k.fingerprint()).collect::<Vec<_>>();
|
||||
|
||||
for (i, selection) in power_set(&key_ids).into_iter().enumerate() {
|
||||
for by_fpr in [true, false] {
|
||||
eprintln!("Test #{}, by {}:",
|
||||
i + 1,
|
||||
if by_fpr { "fingerprint" } else { "key ID" });
|
||||
eprintln!(" Exporting:");
|
||||
for k in selection.iter() {
|
||||
eprintln!(" {}", k);
|
||||
}
|
||||
|
||||
// Export the selection.
|
||||
let mut cmd = Command::cargo_bin("sq")?;
|
||||
cmd.env("SEQUOIA_HOME", dir.path());
|
||||
cmd.args(["key", "export"]);
|
||||
for id in selection.iter() {
|
||||
if by_fpr {
|
||||
cmd.args(["--key", &id.to_string()]);
|
||||
} else {
|
||||
cmd.args(["--key", &KeyID::from(id).to_string()]);
|
||||
}
|
||||
}
|
||||
eprintln!(" Running: {:?}", cmd);
|
||||
let result = cmd.assert().success();
|
||||
let stdout = &result.get_output().stdout;
|
||||
|
||||
let got = Cert::from_bytes(stdout).expect("cert");
|
||||
|
||||
// Make sure we got exactly what we asked for; no
|
||||
// more, no less.
|
||||
eprintln!(" Got:");
|
||||
|
||||
let mut secrets = 0;
|
||||
for got in got.keys() {
|
||||
let expected = keys.iter()
|
||||
.find(|k| k.fingerprint() == got.fingerprint())
|
||||
.expect("have key");
|
||||
|
||||
eprintln!(" {} {} secret key material",
|
||||
got.fingerprint(),
|
||||
if got.has_secret() {
|
||||
"has"
|
||||
} else {
|
||||
"doesn't have"
|
||||
});
|
||||
|
||||
if let Ok(got) = got.parts_as_secret() {
|
||||
assert!(
|
||||
selection.contains(&got.fingerprint()),
|
||||
"got secret key material \
|
||||
for a key we didn't ask for ({})",
|
||||
got.fingerprint());
|
||||
|
||||
assert_eq!(expected.parts_as_secret().expect("have secrets"),
|
||||
got.key());
|
||||
|
||||
secrets += 1;
|
||||
} else {
|
||||
assert!(
|
||||
! selection.contains(&got.fingerprint()),
|
||||
"didn't get secret key material \
|
||||
for a key we asked for ({})",
|
||||
got.fingerprint());
|
||||
|
||||
assert_eq!(expected, got.key());
|
||||
}
|
||||
}
|
||||
assert_eq!(secrets, selection.len());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user