sequoia-sq/tests/integration/sq_key_delete.rs
Neal H. Walfield 0c5e0c9487
Improve how sq key delete handles ambiguous associations.
- Change `sq key delete` to fail if a key is associated with
    multiple certificates.

  - Fixes #457.
2024-11-22 16:03:20 +01:00

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