ab0e2a446c
- See #205.
564 lines
20 KiB
Rust
564 lines
20 KiB
Rust
#[cfg(test)]
|
|
mod integration {
|
|
use std::path;
|
|
|
|
use assert_cmd::Command;
|
|
use predicates::prelude::*;
|
|
|
|
use tempfile::TempDir;
|
|
|
|
use sequoia_openpgp as openpgp;
|
|
|
|
use openpgp::Cert;
|
|
use openpgp::Packet;
|
|
use openpgp::parse::Parse;
|
|
|
|
fn dir() -> path::PathBuf {
|
|
path::Path::new("tests").join("data").join("cert-lint")
|
|
}
|
|
|
|
const FROZEN_TIME: &str = "20220101";
|
|
|
|
/// Returns an assert_cmd::Command for sq with the console detached.
|
|
#[cfg(unix)]
|
|
fn sq() -> Command {
|
|
use std::os::fd::AsRawFd;
|
|
use std::os::unix::process::CommandExt;
|
|
use libc::{TIOCNOTTY, ioctl};
|
|
|
|
let mut c =
|
|
std::process::Command::new(assert_cmd::cargo::cargo_bin("sq"));
|
|
unsafe {
|
|
c.pre_exec(|| {
|
|
// Best-effort, ignores errors.
|
|
if let Ok(h) = std::fs::File::open("/dev/tty") {
|
|
ioctl(h.as_raw_fd(), TIOCNOTTY.into());
|
|
} else {
|
|
ioctl(std::io::stdin().as_raw_fd(), TIOCNOTTY.into());
|
|
}
|
|
Ok(())
|
|
});
|
|
}
|
|
|
|
c.into()
|
|
}
|
|
|
|
/// Returns an assert_cmd::Command for sq with the console detached.
|
|
#[cfg(windows)]
|
|
fn sq() -> Command {
|
|
use std::os::windows::process::CommandExt;
|
|
|
|
let mut c =
|
|
std::process::Command::new(assert_cmd::cargo::cargo_bin("sq"));
|
|
|
|
const DETACHED_PROCESS: u32 = 0x00000008;
|
|
c.creation_flags(DETACHED_PROCESS);
|
|
|
|
c.into()
|
|
}
|
|
|
|
// 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 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] {
|
|
let home = TempDir::new().unwrap();
|
|
let home = home.path().display().to_string();
|
|
|
|
// 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();
|
|
cmd
|
|
.current_dir(&dir)
|
|
.args([
|
|
"--home", &home,
|
|
"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();
|
|
cmd
|
|
.current_dir(&dir)
|
|
.args([
|
|
"--home", &home,
|
|
"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();
|
|
cmd
|
|
.current_dir(&dir)
|
|
.arg("--home").arg(&home)
|
|
.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 { 2 } 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();
|
|
let mut cmd = cmd.current_dir(&dir)
|
|
.args(&[
|
|
"--home", &home,
|
|
"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 3.
|
|
.code(if expected_fixes == required_fixes { 0 } else { 3 })
|
|
.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 = Command::cargo_bin("sq").unwrap();
|
|
cmd
|
|
.current_dir(&dir)
|
|
.arg("--home").arg(&home)
|
|
.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.
|
|
2
|
|
});
|
|
|
|
// 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();
|
|
let cmd = cmd.current_dir(&dir)
|
|
.args(&[
|
|
"--home", &home,
|
|
"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 list_keys() {
|
|
Command::cargo_bin("sq").unwrap()
|
|
.current_dir(&dir())
|
|
.args(&[
|
|
"--no-cert-store",
|
|
"--no-key-store",
|
|
"cert", "lint",
|
|
"--time", FROZEN_TIME,
|
|
"--list-keys",
|
|
// 94F19D3CB5656E0BC3977C09A8AC5ACC2FB87104
|
|
"--cert-file", "sha1-userid-pub.pgp",
|
|
// 55EF7181C288067AE189FF12F5A5CD01D8070917
|
|
"--cert-file", "gnupg-rsa-normal-pub.pgp"
|
|
])
|
|
.assert()
|
|
// If there are issues, the exit code is 2.
|
|
.code(2)
|
|
.stdout(predicate::eq("94F19D3CB5656E0BC3977C09A8AC5ACC2FB87104\n"));
|
|
}
|
|
|
|
#[test]
|
|
fn signature() {
|
|
Command::cargo_bin("sq").unwrap()
|
|
.current_dir(&dir())
|
|
.args(&[
|
|
"--no-cert-store",
|
|
"--no-key-store",
|
|
"cert", "lint",
|
|
"--time", FROZEN_TIME,
|
|
"--cert-file", "msg.sig",
|
|
])
|
|
.assert()
|
|
// If there are issues, the exit code is 1.
|
|
.code(1);
|
|
}
|
|
}
|