sequoia-sq/tests/integration/sq_cert_lint.rs
Justus Winter 91f4400c26
Use --cert- prefix for all cert designators.
- Resolves a conflict with the user ID designators, and makes the
    interface more consistent.

  - Fixes #385.
2024-11-18 14:57:09 +01:00

492 lines
16 KiB
Rust

use std::path;
use predicates::prelude::*;
use sequoia_openpgp as openpgp;
use openpgp::Cert;
use openpgp::Packet;
use openpgp::parse::Parse;
use super::common::Sq;
fn dir() -> path::PathBuf {
path::Path::new("tests").join("data").join("cert-lint")
}
const FROZEN_TIME: &str = "20220101";
// passwords: one '-p' option per element.
// required_fixes: the number of fixes (= new top-level signatures) needed.
// expected_fixes: the number of them that we can create.
fn t(base: &str, prv: Option<&str>, passwords: &[&str],
required_fixes: usize, expected_fixes: usize)
{
assert!(required_fixes >= expected_fixes);
let sq = Sq::new();
let dir = dir();
let mut suffixes = vec![ "pub" ];
if let Some(prv) = prv {
suffixes.push(prv);
}
for suffix in suffixes.iter() {
for keystore in [false, true] {
// Lint it.
let filename = &format!("{}-{}.pgp", base, suffix);
eprintln!("Linting {}", filename);
let cert = Cert::from_file(dir.join(filename))
.expect(&format!("Can parse {}", filename));
if keystore {
// When using the keystore, we need to import the key.
if suffix == &"pub" {
eprintln!("Import certificate from {}", filename);
let mut cmd = sq.command();
cmd
.current_dir(&dir)
.args([
"cert",
"import",
&filename,
]);
let output = cmd.output().expect("can sq cert import");
if !output.status.success() {
panic!(
"sq exited with non-zero status code: {}",
String::from_utf8_lossy(&output.stderr)
);
}
} else {
eprintln!("Import key from {}", filename);
let mut cmd = sq.command();
cmd
.current_dir(&dir)
.args([
"key",
"import",
&filename,
]);
let output = cmd.output().expect("can sq key import");
if !output.status.success() {
panic!(
"sq exited with non-zero status code: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
}
let mut cmd = sq.command();
cmd
.current_dir(&dir)
.arg("cert").arg("lint")
.arg("--time").arg(FROZEN_TIME);
if keystore {
cmd.arg("--cert").arg(&cert.fingerprint().to_string());
} else {
cmd.arg("--cert-file").arg(filename);
}
cmd
.assert()
.code(if required_fixes > 0 { 1 } else { 0 });
// Fix it.
let filename = &format!("{}-{}.pgp", base, suffix);
eprint!("Fixing {}", filename);
if passwords.len() > 0 {
eprint!(" (passwords: ");
for (i, p) in passwords.iter().enumerate() {
if i > 0 {
eprint!(", ");
}
eprint!("{:?}", p)
}
eprint!(")");
}
eprintln!(".");
let expected_fixes = if suffix == &"pub" {
// We only have public key material: we won't be able
// to fix anything.
0
} else {
expected_fixes
};
eprintln!("{} expected fixes, {} required fixes",
expected_fixes, required_fixes);
let mut cmd = sq.command();
let mut cmd = cmd.current_dir(&dir)
.args(&[
"cert", "lint",
"--time", FROZEN_TIME,
"--fix",
]);
if keystore {
cmd.args([
"--cert", &cert.fingerprint().to_string(),
]);
} else {
cmd.args([
"--cert-file", &format!("{}-{}.pgp", base, suffix),
]);
}
for p in passwords.iter() {
cmd = cmd.arg("-p").arg(p)
}
cmd.assert()
// If not everything can be fixed, then --fix's exit code is 1.
.code(if expected_fixes == required_fixes { 0 } else { 1 })
.stdout(predicate::function(|output: &[u8]| -> bool {
if expected_fixes == 0 {
// If there are no fixes, nothing is printed.
output == b""
} else {
// Pass the result through the linter.
let mut cmd = sq.command();
cmd
.current_dir(&dir)
.arg("cert").arg("lint")
.arg("--time").arg(FROZEN_TIME);
if keystore {
cmd.arg("--cert")
.arg(&cert.fingerprint().to_string());
} else {
cmd.arg("--cert-file").arg("-")
.write_stdin(output);
}
cmd.assert()
.code(
if expected_fixes == required_fixes {
// Everything should have been fixed.
0
} else {
// There are still issues.
1
});
// Check that the number of new signatures equals
// the number of expected new signatures.
let orig_sigs: isize = cert
.clone()
.into_packets2()
.map(|p| {
if let Packet::Signature(_) = p {
1
} else {
0
}
})
.sum();
let updated_cert = if keystore {
let mut cmd = sq.command();
let cmd = cmd.current_dir(&dir)
.args(&[
"cert", "export",
"--cert", &cert.fingerprint().to_string(),
]);
let output = cmd.output()
.expect(&format!("Can run sq cert export"));
if !output.status.success() {
panic!(
"sq exited with non-zero status code: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Cert::from_bytes(&output.stdout)
} else {
// When not using the keystore, `sq
// cert lint --fix` emits the fixed
// certificate on stdout.
Cert::from_bytes(output)
};
let fixed_sigs: isize = updated_cert
.map(|cert| {
cert.into_packets2()
.map(|p| {
match p {
Packet::Signature(_) => 1,
Packet::SecretKey(_)
| Packet::SecretSubkey(_) =>
panic!("Secret key material \
should not be exported!"),
_ => 0,
}
})
.sum()
})
.map_err(|err| {
eprintln!("Parsing fixed certificate: {}", err);
0
})
.unwrap();
let fixes = fixed_sigs - orig_sigs;
if expected_fixes as isize != fixes {
eprintln!("Expected {} fixes, \
found {} additional signatures",
expected_fixes, fixes);
false
} else {
true
}
}
}));
}
}
}
#[test]
fn known_good() {
t("gnupg-rsa-normal", Some("priv"), &[], 0, 0);
t("gnupg-ecc-normal", Some("priv"), &[], 0, 0);
}
#[test]
fn userid_certification() {
// User ID: SHA256
// User ID: SHA1
// Enc Subkey: SHA256
t("sha1-userid", Some("priv"), &[], 1, 1);
}
#[test]
fn revoked_userid_certification() {
// A revoked User ID shouldn't be updated.
// User ID: SHA256
// User ID: SHA1 (revoked)
// Enc Subkey: SHA256
t("sha1-userid-revoked", Some("priv"), &[], 0, 0);
}
#[test]
fn signing_subkey_binding_signature() {
// User ID: SHA256
// Enc Subkey: SHA256
// Sig Subkey: SHA1
t("sha1-signing-subkey", Some("priv"), &[], 1, 1);
}
#[test]
fn encryption_subkey_binding_signature() {
// User ID: SHA256
// Enc Subkey: SHA256
// Enc Subkey: SHA1
t("sha1-encryption-subkey", Some("priv"), &[], 1, 1);
}
#[test]
fn subkey_backsig() {
// User ID: SHA256
// Enc Subkey: SHA256
// Sig Subkey: SHA256, backsig: SHA1
t("sha1-backsig-signing-subkey", Some("priv"), &[], 1, 1);
}
#[test]
fn all_bad() {
// User ID: SHA1
// Enc Subkey: SHA1
t("only-sha1", Some("priv"), &[], 2, 2);
// We don't fix MD5 signatures.
//
// User ID: MD5
// Enc Subkey: MD5
t("only-md5", Some("priv"), &[], 2, 0);
}
/// XXX: Disabled because there is no non-interactive way to feed
/// passwords to it.
#[allow(dead_code)]
fn passwords() {
// User ID: SHA1
// Enc Subkey: SHA1
// Wrong password.
t("all-sha1-password-Foobar", Some("priv"), &["foobar"], 2, 0);
// Right password.
t("all-sha1-password-Foobar", Some("priv"), &["Foobar"], 2, 2);
// Try multiple passwords.
t("all-sha1-password-Foobar", Some("priv"), &["Foobar", "bar"], 2, 2);
t("all-sha1-password-Foobar", Some("priv"), &["bar", "Foobar"], 2, 2);
}
/// XXX: Disabled because there is no non-interactive way to feed
/// passwords to it.
#[allow(dead_code)]
fn multiple_passwords() {
// The primary is encrypted with foo and the signing subkey
// with bar. We need to provide both, because the signing
// subkey needs its backsig updated.
// User ID: SHA256
// Enc Subkey: SHA256
// Enc Subkey: SHA1
// Sig Subkey: SHA1
// We only have the password for the signing subkey: we can't
// update anything.
t("multiple-passwords", Some("priv"), &["bar", "Foobar"], 2, 0);
// We only have the password for the primary key: we can't
// update the backsig.
t("multiple-passwords", Some("priv"), &["foo", "Foobar"], 2, 1);
// We have all passwords: we can fix everything.
t("multiple-passwords", Some("priv"), &["bar", "Foobar", "foo"], 2, 2);
}
#[test]
fn offline_subkeys() {
// The User ID, the encryption subkey, and the signing subkey
// all need new signatures. With just the primary key, we are
// able to create two of the three required signatures.
// User ID: SHA1
// Enc Subkey: SHA1
// Sig Subkey: SHA1
// We can't update the backsig.
t("sha1-offline-subkeys", Some("offline"), &[], 3, 2);
// We can fix everything.
t("sha1-offline-subkeys", Some("priv"), &[], 3, 3);
}
#[test]
fn sha1_authentication_subkey() {
// User ID: SHA1
// Enc Subkey: SHA1
// Auth Subkey: SHA1
t("sha1-authentication-subkey", Some("priv"), &[], 3, 3);
}
#[test]
fn authentication_subkey() {
// An authentication subkey doesn't require a backsig. Make
// sure we don't flag a missing backsig as an error.
// User ID: SHA512
// Enc Subkey: SHA512
// Auth Subkey: SHA512
t("authentication-subkey", Some("priv"), &[], 0, 0);
}
#[test]
fn sha1_userid_sha256_subkeys() {
// The User ID is protected with a SHA-1 signature, but two
// subkeys are protected with SHA256. Make sure the subkeys
// don't get new binding signatures.
// User ID: SHA1
// Enc Subkey: SHA1
// Sig Subkey: SHA256
// Enc Subkey: SHA256
t("sha1-userid-sha256-subkeys", Some("priv"), &[], 2, 2);
}
#[test]
fn no_backsig() {
// If a key doesn't have a backsig and needs one, it won't be
// detected as an issue, because it is not valid under
// SHA1+SP. That's okay.
// User ID: SHA512
// Sig Subkey: SHA512, no backsig.
t("no-backsig", Some("priv"), &[], 0, 0);
}
#[test]
fn sha512_self_sig_sha1_revocation() {
// Under the standard policy, SHA1 revocations are considered
// bad. We assume that SP+SHA-1 is strictly more liberal than
// SP (i.e., it accepts at least everything that SP accepts).
// User ID: SHA512, SHA-1 revocation.
t("sha512-self-sig-sha1-revocation", None, &[], 0, 0);
}
#[test]
fn revoked_certificate() {
// The certificate is only valid under SP+SHA1, and the
// revocation certificate uses SHA1. There is no need to
// upgrade the certificate or the revocation certificate.
// User ID: SHA1
// Enc Subkey: SHA1
// Revocation: SHA1
t("sha1-cert-sha1-revocation", Some("priv"), &[], 0, 0);
// The certificate is only valid under SP+SHA1, and the
// revocation certificate uses SHA256. There is no need to
// upgrade the certificate or the revocation certificate.
// User ID: SHA1
// Enc Subkey: SHA1
// Revocation: SHA256
t("sha1-cert-sha256-revocation", Some("priv"), &[], 0, 0);
// The certificate is valid under SP (the signatures use
// SHA512), but there are two revocation certificates that use
// SHA1. Make sure we upgrade them.
// User ID: SHA512
// Enc Subkey: SHA512
// Revocation: SHA1
// Revocation: SHA1
t("sha512-cert-sha1-revocation", Some("priv"), &[], 2, 2);
// The certificate is valid under SP (the signatures use
// SHA256), and it is revoked using a SHA256 revocation
// certificate, which is also valid under SP. It also has a
// SHA-1 protected signing subkey. Because the certificate is
// revoked and the revocation certificate uses SHA256, we
// don't need to fix the SHA-1 signature. Make sure we don't.
// User ID: SHA256
// Enc Subkey: SHA256
// Sig Subkey: SHA1
// Revocation: SHA256
t("sha256-cert-sha256-revocation", Some("priv"), &[], 0, 0);
}
#[test]
fn expired_certificates() {
// User ID: SHA256 (expired)
// Enc Subkey: SHA256
t("sha256-expired", Some("priv"), &[], 0, 0);
// User ID: SHA1 (expired)
// Enc Subkey: SHA1
t("sha1-expired", Some("priv"), &[], 0, 0);
// User ID: SHA256 (old, expired), SHA1 (new, live)
// Enc Subkey: SHA256
t("sha256-expired-sha1-live", Some("priv"), &[], 1, 1);
}
#[test]
fn signature() {
let sq = Sq::new();
sq.command()
.current_dir(&dir())
.args(&[
"cert", "lint",
"--time", FROZEN_TIME,
"--cert-file", "msg.sig",
])
.assert()
// If there are issues, the command fails.
.failure();
}