Change sq cert lint to support the cert store and key store.

- See #205.
This commit is contained in:
Neal H. Walfield 2024-05-28 14:33:27 +02:00
parent 5c1cf92f9b
commit ab0e2a446c
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
4 changed files with 764 additions and 605 deletions

2
NEWS
View File

@ -32,6 +32,8 @@
- Change `sq cert lint` to not read from stdin by default.
- In `sq cert lint`, change the certificate file parameter from a
position parameter to a named parameter, `--cert-file`.
- `sq cert lint` can now use the certificate store and the
keystore.
* Changes in 0.36.0
- Missing
* Changes in 0.35.0

View File

@ -1,6 +1,10 @@
//! Command-line parser for `sq cert lint`.
use clap::Args;
use clap::ArgGroup;
use sequoia_openpgp as openpgp;
use openpgp::KeyHandle;
use crate::cli::types::ClapData;
use crate::cli::types::FileOrStdin;
@ -67,6 +71,7 @@ $ sq cert lint --list-keys keyring.pgp \\
| while read FPR; do something; done
"
)]
#[clap(group(ArgGroup::new("cert_input").args(&["cert_file", "cert"]).required(true)))]
pub struct Command {
/// Quiet; does not output any diagnostics.
#[arg(short, long)]
@ -94,20 +99,29 @@ pub struct Command {
#[clap(
long,
help = FileOrStdin::HELP_OPTIONAL,
value_name = FileOrStdin::VALUE_NAME,
required = true,
value_name = "CERT_FILE",
help = "Lint the certificates in the specified file",
)]
pub cert_file: Vec<FileOrStdin>,
#[clap(
long,
value_name = "FINGERPRINT|KEYID",
help = "Lint the specified certificate",
conflicts_with = "cert_file",
)]
pub cert: Vec<KeyHandle>,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP_OPTIONAL,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
help = "Write to the specified FILE. If not specified, and the \
certificate was read from the certificate store, imports the \
modified certificate into the cert store. If not specified, \
and the certificate was read from a file, writes the modified \
certificate to stdout.",
)]
pub output: FileOrStdout,
pub output: Option<FileOrStdout>,
#[clap(
short = 'B',
long = "binary",

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,8 @@ mod integration {
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
use sequoia_openpgp as openpgp;
use openpgp::Cert;
@ -70,130 +72,226 @@ mod integration {
}
for suffix in suffixes.iter() {
// Lint it.
let filename = &format!("{}-{}.pgp", base, suffix);
eprintln!("Linting {}", filename);
sq()
.current_dir(&dir)
.arg("--no-cert-store")
.arg("--no-key-store")
.arg("cert").arg("lint")
.arg("--time").arg(FROZEN_TIME)
.arg("--cert-file").arg(filename)
.assert()
.code(if required_fixes > 0 { 2 } else { 0 });
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);
// 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 cert = Cert::from_file(dir.join(filename))
.expect(&format!("Can parse {}", filename));
let expected_fixes = if suffix == &"pub" {
// We only have public key material: we won't be able
// to fix anything.
0
} else {
expected_fixes
};
if keystore {
// When using the keystore, we need to import the key.
if suffix == &"pub" {
eprintln!("Import certificate from {}", filename);
let mut cmd = sq();
let mut cmd = cmd.current_dir(&dir)
.args(&[
"--no-cert-store",
"--no-key-store",
"cert", "lint",
"--time", FROZEN_TIME,
"--fix",
"--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 {
// We got a certificate on stdout. Pass it
// through the linter.
Command::cargo_bin("sq").unwrap()
let mut cmd = sq();
cmd
.current_dir(&dir)
.arg("--no-cert-store")
.arg("--no-key-store")
.arg("cert").arg("lint")
.arg("--time").arg(FROZEN_TIME)
.arg("--cert-file").arg("-")
.write_stdin(output)
.assert()
.code(
if expected_fixes == required_fixes {
// Everything should have been fixed.
0
} else {
// There are still issues.
2
});
.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);
// Check that the number of new signatures equals
// the number of expected new signatures.
let orig_sigs: isize =
Cert::from_file(dir.clone().join(filename)).unwrap()
.into_packets2()
.map(|p| {
if let Packet::Signature(_) = p {
1
} else {
0
}
})
.sum();
let fixed_sigs: isize = Cert::from_bytes(output)
.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
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
}
}
}));
}
}
}