4a5ce6603c
- Currently, it is not possible to delete secret key material that is only associated with a certificate that is not valid under the current policy. The same goes for changing the password protecting the secret key material. - Users shouldn't have to first update a key's binding signature to delete it, or change its password. - Change `sq key subkey delete` and `sq key subkey password` to use the null policy. This is not a security concern, because even if the binding signature is weak, both the certificate and the key are explicitly named. - See #375
254 lines
7.0 KiB
Rust
254 lines
7.0 KiB
Rust
use sequoia_openpgp as openpgp;
|
|
use openpgp::Cert;
|
|
use openpgp::KeyHandle;
|
|
use openpgp::KeyID;
|
|
use openpgp::Result;
|
|
use openpgp::cert::amalgamation::ValidAmalgamation;
|
|
use openpgp::packet::Key;
|
|
use openpgp::parse::Parse;
|
|
use openpgp::types::RevocationStatus;
|
|
|
|
use super::common::FileOrKeyHandle;
|
|
use super::common::power_set;
|
|
use super::common::Sq;
|
|
use super::common::STANDARD_POLICY;
|
|
|
|
fn check<'a, H>(
|
|
sq: &Sq,
|
|
cert_handle: H,
|
|
to_delete: &[KeyHandle],
|
|
success: bool)
|
|
where H: Into<FileOrKeyHandle>,
|
|
{
|
|
let cert_handle = cert_handle.into();
|
|
|
|
eprintln!("Deleting keys from {:?}:", cert_handle);
|
|
for k in to_delete.iter() {
|
|
eprintln!(" - {}", k);
|
|
}
|
|
|
|
// Delete the selection.
|
|
let result = sq.try_key_subkey_delete(&cert_handle, to_delete, None);
|
|
let got = match (success, result) {
|
|
(true, Ok(cert)) => cert,
|
|
(true, Err(err)) => {
|
|
panic!("Failed, but should have succeeded: {}", err)
|
|
}
|
|
(false, Ok(_)) => {
|
|
panic!("Succeded, but should have failed")
|
|
}
|
|
(false, Err(_)) => return,
|
|
};
|
|
|
|
// Make sure we got exactly what we asked for; no
|
|
// more, no less.
|
|
eprintln!("Result:");
|
|
|
|
let mut deletions = 0;
|
|
for got in got.keys() {
|
|
eprintln!(" {} {} secret key material",
|
|
got.fingerprint(),
|
|
if got.has_secret() {
|
|
"has"
|
|
} else {
|
|
"doesn't have"
|
|
});
|
|
|
|
let should_have_deleted
|
|
= to_delete.iter().find(|kh| kh.aliases(got.key_handle())).is_some();
|
|
|
|
if should_have_deleted {
|
|
assert!(
|
|
! got.has_secret(),
|
|
"got secret key material \
|
|
for a key we should have deleted ({})",
|
|
got.fingerprint());
|
|
|
|
deletions += 1;
|
|
} else {
|
|
assert!(
|
|
got.has_secret(),
|
|
"didn't get secret key material \
|
|
for a key we didn't delete ({})",
|
|
got.fingerprint());
|
|
}
|
|
}
|
|
assert_eq!(deletions, to_delete.len());
|
|
}
|
|
|
|
#[test]
|
|
fn sq_key_subkey_delete() -> Result<()>
|
|
{
|
|
let sq = Sq::new();
|
|
|
|
// Generate a key in a file.
|
|
let (cert, cert_file, _rev_file) = sq.key_generate(&[], &["alice"]);
|
|
assert!(cert.is_tsk());
|
|
|
|
// Delete each non-empty subset of keys.
|
|
|
|
eprintln!("Certificate:");
|
|
for k in cert.keys() {
|
|
eprintln!(" {}", k.fingerprint());
|
|
}
|
|
|
|
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, to_delete), keystore), by_fpr) in power_set(&key_ids).into_iter()
|
|
.enumerate()
|
|
.flat_map(|x| [(x.clone(), false), (x.clone(), true)])
|
|
.flat_map(|x| [(x.clone(), false), (x.clone(), true)])
|
|
{
|
|
eprintln!("Test #{}, by {}, from {}:",
|
|
i + 1,
|
|
if by_fpr { "fingerprint" } else { "key ID" },
|
|
if keystore {
|
|
"the key store".to_string()
|
|
} else {
|
|
cert_file.display().to_string()
|
|
});
|
|
|
|
let to_delete: Vec<KeyHandle> = if by_fpr {
|
|
to_delete.iter()
|
|
.map(|fpr| KeyHandle::from(fpr))
|
|
.collect()
|
|
} else {
|
|
to_delete.iter()
|
|
.map(|fpr| KeyHandle::from(KeyID::from(fpr)))
|
|
.collect()
|
|
};
|
|
|
|
if keystore {
|
|
// Import it into the key store.
|
|
sq.key_import(&cert_file);
|
|
check(&sq, cert.key_handle(), &to_delete, true);
|
|
} else {
|
|
check(&sq, &cert_file, &to_delete, true);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn unbound_subkey() {
|
|
// Make sure we can't delete 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.
|
|
assert!(vc.keys().count() < cert.keys().count());
|
|
|
|
let unbound = "E992BF8BA7A27BB4FBB71D973857E47B14874045"
|
|
.parse::<KeyHandle>().expect("valid");
|
|
|
|
check(&sq, &cert_path, &[ unbound ], false);
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
}
|
|
let revoked = if let Some(revoked) = revoked {
|
|
revoked
|
|
} else {
|
|
panic!("Expected a revoked subkey, but didn't fine one");
|
|
};
|
|
|
|
check(&sq, &cert_path, &[ revoked ], true);
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
}
|
|
let revoked = if let Some(revoked) = revoked {
|
|
revoked
|
|
} else {
|
|
panic!("Expected a revoked subkey, but didn't fine one");
|
|
};
|
|
|
|
check(&sq, &cert_path, &[ revoked ], true);
|
|
}
|
|
|
|
#[test]
|
|
fn sha1_subkey() {
|
|
// Make sure we can delete 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);
|
|
|
|
let subkey = all_subkeys[0].clone();
|
|
check(&sq, &cert_path, &[ KeyHandle::from(subkey) ], true);
|
|
}
|