sequoia-sq/tests/sq-key-subkey.rs
Neal H. Walfield 27093c1709
Add support for using a key store.
- Support using keys managed by `sequoia-keystore`.

  - When decrypting a message, have `sq` automatically ask the
    key store to decrypt the PKESKs.

  - Extend `sq sign` and `sq encrypt` with the `--signer-key`
    parameter to use a key managed by the keystore.

  - Add two top-level options: `--no-key-store`, which disables the
    use of the key store, and `--key-store`, which uses an alternate
    key store instance.

  - Add `sq key list` to list keys on the key store.
2024-02-18 15:24:02 +01:00

522 lines
16 KiB
Rust

use assert_cmd::Command;
use chrono::Duration;
use openpgp::packet::Key;
use openpgp::parse::Parse;
use openpgp::types::ReasonForRevocation;
use openpgp::types::RevocationStatus;
use openpgp::types::SignatureType;
use openpgp::Cert;
use openpgp::Result;
use sequoia_openpgp as openpgp;
mod common;
use common::compare_notations;
use common::sq_key_generate;
use common::STANDARD_POLICY;
#[test]
fn sq_key_subkey_generate_authentication_subkey() -> Result<()> {
let (tmpdir, path, _) = sq_key_generate(None).unwrap();
let output = path.parent().unwrap().join("new_key.pgp");
let mut cmd = Command::cargo_bin("sq")?;
cmd.args([
"--no-cert-store",
"--no-key-store",
"key",
"subkey",
"add",
"--output",
&output.to_string_lossy(),
"--can-authenticate",
&path.to_string_lossy(),
]);
cmd.assert().success();
let cert = Cert::from_file(&output)?;
let valid_cert = cert.with_policy(STANDARD_POLICY, None)?;
assert_eq!(
valid_cert.keys().filter(|x| x.for_authentication()).count(),
2
);
tmpdir.close()?;
Ok(())
}
#[test]
fn sq_key_subkey_generate_encryption_subkey() -> Result<()> {
let (tmpdir, path, _) = sq_key_generate(None).unwrap();
let output = path.parent().unwrap().join("new_key.pgp");
let mut cmd = Command::cargo_bin("sq")?;
cmd.args([
"--no-cert-store",
"--no-key-store",
"key",
"subkey",
"add",
"--output",
&output.to_string_lossy(),
"--can-encrypt=universal",
&path.to_string_lossy(),
]);
cmd.assert().success();
let cert = Cert::from_file(&output)?;
let valid_cert = cert.with_policy(STANDARD_POLICY, None)?;
assert_eq!(
valid_cert
.keys()
.filter(|x| x.for_storage_encryption())
.count(),
2
);
assert_eq!(
valid_cert
.keys()
.filter(|x| x.for_transport_encryption())
.count(),
2
);
tmpdir.close()?;
Ok(())
}
#[test]
fn sq_key_subkey_generate_signing_subkey() -> Result<()> {
let (tmpdir, path, _) = sq_key_generate(None).unwrap();
let output = path.parent().unwrap().join("new_key.pgp");
let mut cmd = Command::cargo_bin("sq")?;
cmd.args([
"--no-cert-store",
"--no-key-store",
"key",
"subkey",
"add",
"--output",
&output.to_string_lossy(),
"--can-sign",
&path.to_string_lossy(),
]);
cmd.assert().success();
let cert = Cert::from_file(&output)?;
let valid_cert = cert.with_policy(STANDARD_POLICY, None)?;
assert_eq!(valid_cert.keys().filter(|x| x.for_signing()).count(), 2);
tmpdir.close()?;
Ok(())
}
#[test]
fn sq_key_subkey_revoke() -> Result<()> {
let (tmpdir, path, time) = sq_key_generate(None)?;
let cert = Cert::from_file(&path)?;
let valid_cert = cert.with_policy(STANDARD_POLICY, Some(time.into()))?;
let fingerprint = valid_cert.clone().fingerprint();
let subkey: Key<_, _> = valid_cert
.with_policy(STANDARD_POLICY, Some(time.into()))
.unwrap()
.keys()
.subkeys()
.nth(0)
.unwrap()
.key()
.clone();
let subkey_fingerprint = subkey.fingerprint();
let message = "message";
// revoke for various reasons, with or without notations added, or with
// a revocation whose reference time is one hour after the creation of the
// certificate
for (reason, reason_str, notations, revocation_time) in [
(
ReasonForRevocation::KeyCompromised,
"compromised",
None,
None,
),
(
ReasonForRevocation::KeyCompromised,
"compromised",
None,
Some(time + Duration::hours(1)),
),
(
ReasonForRevocation::KeyCompromised,
"compromised",
Some(&[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")]),
None,
),
(ReasonForRevocation::KeyRetired, "retired", None, None),
(
ReasonForRevocation::KeyRetired,
"retired",
None,
Some(time + Duration::hours(1)),
),
(
ReasonForRevocation::KeyRetired,
"retired",
Some(&[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")]),
None,
),
(ReasonForRevocation::KeySuperseded, "superseded", None, None),
(
ReasonForRevocation::KeySuperseded,
"superseded",
None,
Some(time + Duration::hours(1)),
),
(
ReasonForRevocation::KeySuperseded,
"superseded",
Some(&[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")]),
None,
),
(ReasonForRevocation::Unspecified, "unspecified", None, None),
(
ReasonForRevocation::Unspecified,
"unspecified",
None,
Some(time + Duration::hours(1)),
),
(
ReasonForRevocation::Unspecified,
"unspecified",
Some(&[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")]),
None,
),
] {
let revocation = &path.parent().unwrap().join(format!(
"revocation_{}_{}_{}.rev",
reason_str,
if notations.is_some() {
"notations"
} else {
"no_notations"
},
if revocation_time.is_some() {
"time"
} else {
"no_time"
}
));
let mut cmd = Command::cargo_bin("sq")?;
cmd.args([
"--no-cert-store",
"--no-key-store",
"key",
"subkey",
"revoke",
"--output",
&revocation.to_string_lossy(),
"--certificate-file",
&path.to_string_lossy(),
&subkey_fingerprint.to_string(),
reason_str,
message,
]);
if let Some(notations) = notations {
for (k, v) in notations {
cmd.args(["--notation", k, v]);
}
}
if let Some(time) = revocation_time {
cmd.args([
"--time",
&time.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
]);
}
let output = cmd.output()?;
if !output.status.success() {
panic!("sq exited with non-zero status code: {:?}", output.stderr);
}
// whether we found a revocation signature
let mut found_revoked = false;
// read revocation cert
let rev = Cert::from_file(&revocation)?;
assert!(! rev.is_tsk());
// and merge it into the certificate.
let cert = cert.clone().merge_public(rev)?;
let valid_cert =
cert.with_policy(STANDARD_POLICY, revocation_time.map(Into::into))?;
valid_cert
.with_policy(STANDARD_POLICY, revocation_time.map(Into::into))
.unwrap()
.keys()
.subkeys()
.for_each(|x| {
if x.fingerprint() == subkey_fingerprint {
let status = x.revocation_status(
STANDARD_POLICY,
revocation_time.map(Into::into),
);
// the subkey is revoked
assert!(matches!(status, RevocationStatus::Revoked(_)));
if let RevocationStatus::Revoked(sigs) = status {
// there is only one signature packet
assert_eq!(sigs.len(), 1);
let sig = sigs.into_iter().next().unwrap();
// it is a subkey revocation
assert_eq!(sig.typ(), SignatureType::SubkeyRevocation);
// the issuer is the certificate owner
assert_eq!(
sig.get_issuers().into_iter().next().as_ref(),
Some(&fingerprint.clone().into())
);
// our reason for revocation and message matches
assert_eq!(
sig.reason_for_revocation(),
Some((reason, message.as_bytes()))
);
// the notations of the revocation match the ones
// we passed in
assert!(compare_notations(sig, notations).is_ok());
found_revoked = true;
}
}
});
if !found_revoked {
panic!("the revoked subkey is not found in the revocation cert");
}
}
tmpdir.close()?;
Ok(())
}
#[test]
fn sq_key_subkey_revoke_thirdparty() -> Result<()> {
let (tmpdir, path, time) = sq_key_generate(None)?;
let (thirdparty_tmpdir, thirdparty_path, thirdparty_time) =
sq_key_generate(Some(&["bob <bob@example.org"]))?;
let cert = Cert::from_file(&path)?;
let valid_cert = cert.with_policy(STANDARD_POLICY, Some(time.into()))?;
let subkey: Key<_, _> = valid_cert
.with_policy(STANDARD_POLICY, Some(time.into()))
.unwrap()
.keys()
.subkeys()
.nth(0)
.unwrap()
.key()
.clone();
let subkey_fingerprint = subkey.fingerprint();
let thirdparty_cert = Cert::from_file(&thirdparty_path)?;
let thirdparty_valid_cert = thirdparty_cert
.with_policy(STANDARD_POLICY, Some(thirdparty_time.into()))?;
let thirdparty_fingerprint = thirdparty_valid_cert.clone().fingerprint();
let message = "message";
// revoke for various reasons, with or without notations added, or with
// a revocation whose reference time is one hour after the creation of the
// certificate
for (reason, reason_str, notations, revocation_time) in [
(
ReasonForRevocation::KeyCompromised,
"compromised",
None,
None,
),
(
ReasonForRevocation::KeyCompromised,
"compromised",
None,
Some(thirdparty_time + Duration::hours(1)),
),
(
ReasonForRevocation::KeyCompromised,
"compromised",
Some(&[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")]),
None,
),
(ReasonForRevocation::KeyRetired, "retired", None, None),
(
ReasonForRevocation::KeyRetired,
"retired",
None,
Some(thirdparty_time + Duration::hours(1)),
),
(
ReasonForRevocation::KeyRetired,
"retired",
Some(&[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")]),
None,
),
(ReasonForRevocation::KeySuperseded, "superseded", None, None),
(
ReasonForRevocation::KeySuperseded,
"superseded",
None,
Some(thirdparty_time + Duration::hours(1)),
),
(
ReasonForRevocation::KeySuperseded,
"superseded",
Some(&[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")]),
None,
),
(ReasonForRevocation::Unspecified, "unspecified", None, None),
(
ReasonForRevocation::Unspecified,
"unspecified",
None,
Some(thirdparty_time + Duration::hours(1)),
),
(
ReasonForRevocation::Unspecified,
"unspecified",
Some(&[("foo", "bar"), ("hallo@sequoia-pgp.org", "VALUE")]),
None,
),
] {
let revocation = &path.parent().unwrap().join(format!(
"revocation_{}_{}_{}.rev",
reason_str,
if notations.is_some() {
"notations"
} else {
"no_notations"
},
if revocation_time.is_some() {
"time"
} else {
"no_time"
}
));
let mut cmd = Command::cargo_bin("sq")?;
cmd.args([
"--no-cert-store",
"--no-key-store",
"key",
"subkey",
"revoke",
"--output",
&revocation.to_string_lossy(),
"--certificate-file",
&path.to_string_lossy(),
"--revocation-file",
&thirdparty_path.to_string_lossy(),
&subkey_fingerprint.to_string(),
reason_str,
message,
]);
if let Some(notations) = notations {
for (k, v) in notations {
cmd.args(["--notation", k, v]);
}
}
if let Some(time) = revocation_time {
cmd.args([
"--time",
&time.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
]);
}
let output = cmd.output()?;
if !output.status.success() {
panic!("sq exited with non-zero status code: {:?}", output.stderr);
}
// whether we found a revocation signature
let mut found_revoked = false;
// read revocation cert
let rev = Cert::from_file(&revocation)?;
assert!(! rev.is_tsk());
// and merge it into the certificate.
let cert = cert.clone().merge_public(rev)?;
let valid_cert =
cert.with_policy(STANDARD_POLICY, revocation_time.map(Into::into))?;
assert_eq!(valid_cert.userids().count(), 1);
valid_cert
.with_policy(STANDARD_POLICY, revocation_time.map(Into::into))
.unwrap()
.keys()
.subkeys()
.for_each(|x| {
if x.fingerprint() == subkey_fingerprint {
if let RevocationStatus::CouldBe(sigs) = x
.revocation_status(
STANDARD_POLICY,
revocation_time.map(Into::into),
)
{
// there is only one signature packet
assert_eq!(sigs.len(), 1);
let sig = sigs.into_iter().next().unwrap();
// it is a subkey revocation
assert_eq!(sig.typ(), SignatureType::SubkeyRevocation);
// the issuer is a thirdparty revoker
assert_eq!(
sig.get_issuers().into_iter().next().as_ref(),
Some(&thirdparty_fingerprint.clone().into())
);
// the revocation can be verified
if sig
.clone()
.verify_subkey_revocation(
&thirdparty_cert.primary_key(),
&cert.primary_key(),
&subkey,
)
.is_err()
{
panic!("revocation is not valid")
}
// our reason for revocation and message matches
assert_eq!(
sig.reason_for_revocation(),
Some((reason, message.as_bytes()))
);
// the notations of the revocation match the ones
// we passed in
assert!(compare_notations(sig, notations).is_ok());
found_revoked = true;
} else {
panic!("there are no signatures in {:?}", x);
}
}
});
if !found_revoked {
panic!("the revoked subkey is not found in the revocation cert");
}
}
tmpdir.close()?;
thirdparty_tmpdir.close()?;
Ok(())
}