0c5e0c9487
- Change `sq key delete` to fail if a key is associated with multiple certificates. - Fixes #457.
273 lines
7.8 KiB
Rust
273 lines
7.8 KiB
Rust
use std::collections::HashSet;
|
|
|
|
use sequoia_openpgp as openpgp;
|
|
use openpgp::Cert;
|
|
use openpgp::Fingerprint;
|
|
use openpgp::KeyHandle;
|
|
use openpgp::Result;
|
|
use openpgp::cert::amalgamation::ValidAmalgamation;
|
|
use openpgp::parse::Parse;
|
|
use openpgp::types::RevocationStatus;
|
|
|
|
use super::common::Sq;
|
|
use super::common::STANDARD_POLICY;
|
|
|
|
#[test]
|
|
fn sq_key_delete() -> Result<()> {
|
|
let sq = Sq::new();
|
|
|
|
let (cert, cert_file, _rev_file) = sq.key_generate(&[], &["alice"]);
|
|
assert!(cert.is_tsk());
|
|
|
|
// Delete all the secret key material from a certificate stored in
|
|
// a file. Make sure the result contains no secret key material.
|
|
let updated = sq.key_delete(&cert_file,
|
|
std::path::PathBuf::from("-").as_path());
|
|
assert!(! updated.is_tsk());
|
|
|
|
// Do the same for a certificate whose secret key material is
|
|
// managed by the keystore.
|
|
sq.key_import(cert_file);
|
|
|
|
let cert = sq.key_export(cert.key_handle());
|
|
assert!(cert.is_tsk());
|
|
|
|
let updated = sq.key_delete(cert.key_handle(), None);
|
|
assert!(! updated.is_tsk());
|
|
|
|
// If we really stripped the secret key material, then `sq key
|
|
// export` will fail.
|
|
assert!(sq.key_export_maybe(cert.key_handle()).is_err());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn unbound_subkey() {
|
|
// Make sure we don't delete secret key material if there is an
|
|
// unbound subkey.
|
|
|
|
let sq = Sq::new();
|
|
|
|
let cert_path = sq.test_data()
|
|
.join("keys")
|
|
.join("unbound-subkey.pgp");
|
|
|
|
let cert = Cert::from_file(&cert_path).expect("can read");
|
|
let vc = cert.with_policy(STANDARD_POLICY, sq.now())
|
|
.expect("valid cert");
|
|
|
|
// One subkey should be considered invalid.
|
|
let bound: HashSet<Fingerprint>
|
|
= HashSet::from_iter(vc.keys().map(|ka| ka.fingerprint()));
|
|
let all: HashSet<Fingerprint>
|
|
= HashSet::from_iter(cert.keys().map(|ka| ka.fingerprint()));
|
|
assert!(bound.len() < all.len());
|
|
|
|
let result = sq.key_delete(&cert_path, None);
|
|
for ka in result.keys() {
|
|
if bound.contains(&ka.fingerprint()) {
|
|
assert!(! ka.has_secret());
|
|
} else {
|
|
assert!(ka.has_secret());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn soft_revoked_subkey() {
|
|
// Make sure we can delete a soft revoked subkey.
|
|
|
|
let sq = Sq::new();
|
|
|
|
let cert_path = sq.test_data()
|
|
.join("keys")
|
|
.join("soft-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");
|
|
|
|
// Make sure the revoked key is there and is really revoked.
|
|
let mut revoked = None;
|
|
for k in vc.keys().subkeys() {
|
|
if let RevocationStatus::Revoked(_) = k.revocation_status() {
|
|
assert!(revoked.is_none(),
|
|
"Only expected a single revoked subkey");
|
|
revoked = Some(k.key_handle());
|
|
}
|
|
}
|
|
if revoked.is_none() {
|
|
panic!("Expected a revoked subkey, but didn't fine one");
|
|
}
|
|
|
|
let updated = sq.key_delete(cert_path, None);
|
|
assert!(! updated.is_tsk());
|
|
}
|
|
|
|
#[test]
|
|
fn hard_revoked_subkey() {
|
|
// Make sure we can delete a hard revoked subkey.
|
|
|
|
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");
|
|
|
|
// Make sure the revoked key is there and is really revoked.
|
|
let mut revoked = None;
|
|
for k in vc.keys().subkeys() {
|
|
if let RevocationStatus::Revoked(_) = k.revocation_status() {
|
|
assert!(revoked.is_none(),
|
|
"Only expected a single revoked subkey");
|
|
revoked = Some(k.key_handle());
|
|
}
|
|
}
|
|
if revoked.is_none() {
|
|
panic!("Expected a revoked subkey, but didn't fine one");
|
|
}
|
|
|
|
let updated = sq.key_delete(cert_path, None);
|
|
assert!(! updated.is_tsk());
|
|
}
|
|
|
|
#[test]
|
|
fn sha1_subkey() {
|
|
// Make sure we can't delete secret key material if there is a
|
|
// subkey that is bound using SHA-1.
|
|
|
|
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 cert");
|
|
|
|
// Make sure the subkey key is there and really uses SHA-1.
|
|
let valid_subkeys: Vec<_> = vc.keys().subkeys()
|
|
.map(|ka| ka.fingerprint())
|
|
.collect();
|
|
let all_subkeys: Vec<_> = cert.keys().subkeys()
|
|
.map(|ka| ka.fingerprint())
|
|
.collect();
|
|
|
|
assert_eq!(valid_subkeys.len(), 0);
|
|
assert_eq!(all_subkeys.len(), 1);
|
|
|
|
assert!(sq.try_key_delete(cert_path, None).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn sha1_subkey_without_secret_key_material() {
|
|
// Make sure we can delete secret key material in the presence of
|
|
// a subkey that is bound using SHA-1, but for which there is no
|
|
// secret key material.
|
|
|
|
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 cert");
|
|
|
|
// Make sure the subkey key is there and really uses SHA-1.
|
|
eprintln!("Valid keys:");
|
|
let valid_keys: Vec<_> = vc.keys()
|
|
.map(|ka| {
|
|
let fpr = ka.fingerprint();
|
|
eprintln!(" - {}", fpr);
|
|
fpr
|
|
})
|
|
.collect();
|
|
|
|
eprintln!("All keys:");
|
|
let all_keys: Vec<_> = cert.keys()
|
|
.map(|ka| {
|
|
let fpr = ka.fingerprint();
|
|
eprintln!(" - {}", fpr);
|
|
fpr
|
|
})
|
|
.collect();
|
|
|
|
assert!(valid_keys.len() < all_keys.len());
|
|
|
|
let mut update = cert_path;
|
|
for fpr in all_keys.iter() {
|
|
if ! valid_keys.contains(fpr) {
|
|
let update2 = sq.scratch_file(
|
|
Some(&format!("delete-{}", fpr)[..]));
|
|
sq.key_subkey_delete(
|
|
update, &[KeyHandle::from(fpr)], update2.as_path());
|
|
update = update2;
|
|
}
|
|
}
|
|
|
|
let cert = Cert::from_file(&update).expect("can read");
|
|
for ka in cert.keys() {
|
|
if valid_keys.contains(&ka.fingerprint()) {
|
|
assert!(ka.has_secret());
|
|
} else {
|
|
assert!(! ka.has_secret(),
|
|
"{} still has secret key material", ka.fingerprint());
|
|
}
|
|
}
|
|
|
|
let result = sq.key_delete(update, None);
|
|
assert!(! result.is_tsk());
|
|
}
|
|
|
|
|
|
#[test]
|
|
fn ambiguous() {
|
|
// If a key is associated with multiple certificates, then sq key
|
|
// delete should refuse to delete the secret key material.
|
|
let sq = Sq::new();
|
|
|
|
let (alice1, alice1_path, _alice1_rev)
|
|
= sq.key_generate(&[], &["alice1"]);
|
|
sq.key_import(&alice1_path);
|
|
|
|
let common_subkey = alice1.keys().subkeys().take(1)
|
|
.map(|ka| ka.key_handle())
|
|
.collect::<Vec<_>>();
|
|
|
|
let (alice2, alice2_path, _alice2_rev)
|
|
= sq.key_generate(&[], &["alice2"]);
|
|
sq.key_import(&alice2_path);
|
|
|
|
let alice2_update = sq.scratch_file("alice2-updated");
|
|
sq.key_subkey_bind(&[], vec![ &alice2_path ],
|
|
alice2.fingerprint(),
|
|
common_subkey.clone(),
|
|
&alice2_update);
|
|
|
|
sq.key_import(alice2_update);
|
|
|
|
assert!(sq.try_key_delete(alice2.fingerprint(), None).is_err());
|
|
|
|
// We should be able to delete it using sq key subkey delete.
|
|
sq.key_subkey_delete(alice1.fingerprint(),
|
|
&common_subkey,
|
|
None);
|
|
|
|
sq.key_subkey_delete(alice2.fingerprint(),
|
|
&common_subkey,
|
|
None);
|
|
|
|
// And now we should be able to delete the secret key material
|
|
// associated with the certificate.
|
|
sq.key_delete(alice2.fingerprint(), None);
|
|
}
|
|
|