Move keyring-linter into sq keyring as a subcommand

This commit is mostly a copy over from the keyring-linter repository,
with a few changes included to make it work in the sq codebase. These
changes are:
 - replaced calls to atty with calls to is-terminal. This was done due
   to is-terminal already being in the dependency tree of sq, and atty
   being unmaintained.
 - replace ansi_term with termcolor, because ansi_term is unmaintained
 - removed a few things from the keyring linter, that were also present
   in sq itself, to avoid duplication. This included the reference time
   parameter, key decryption and IO handling
 - added output file and binary parameters to the linter, so that I
   could handle output the same as the other commands do
This commit is contained in:
Jan Christian Grünhage 2023-06-02 00:22:52 +02:00 committed by Neal H. Walfield
parent f3cfb1b602
commit 74fd9dd8fe
No known key found for this signature in database
GPG Key ID: 6863C9AD5B4D22D3
55 changed files with 1415 additions and 1 deletions

3
Cargo.lock generated
View File

@ -2923,7 +2923,7 @@ dependencies = [
"generic-array",
"getrandom 0.2.9",
"idea",
"idna 0.2.3",
"idna 0.3.0",
"lalrpop",
"lalrpop-util",
"lazy_static",
@ -3000,6 +3000,7 @@ dependencies = [
"subplot-build",
"subplotlib",
"tempfile",
"termcolor",
"terminal_size",
"tokio",
]

View File

@ -51,6 +51,7 @@ serde = { version = "1.0.137", features = ["derive"] }
roff = "0.2.1"
terminal_size = "0.2.6"
is-terminal = "0.4.7"
termcolor = "1.2.0"
[build-dependencies]
anyhow = "1.0.18"

View File

@ -4,6 +4,7 @@ use std::{
fs::File,
io,
path::PathBuf,
process::exit,
};
use std::ops::Deref;
use anyhow::Context;
@ -31,10 +32,13 @@ use crate::{
Config,
Model,
output::KeyringListItem,
print_error_chain,
};
use crate::sq_cli::keyring;
mod linter;
pub fn dispatch(config: Config, c: keyring::Command) -> Result<()> {
use crate::sq_cli::keyring::Subcommands::*;
match c.subcommand {
@ -190,6 +194,15 @@ pub fn dispatch(config: Config, c: keyring::Command) -> Result<()> {
+ "-");
split(&mut input, &prefix, c.binary)
},
Linter(l) => {
match linter::linter(config, l) {
Ok(()) => Ok(()),
Err(e) => {
print_error_chain(&e);
exit(1);
},
}
}
}
}

View File

@ -0,0 +1,859 @@
use std::cmp::Ordering;
use std::io:: Write;
use std::process::exit;
use std::time::SystemTime;
use is_terminal::IsTerminal;
use termcolor::{WriteColor, StandardStream, ColorChoice, ColorSpec, Color};
use sequoia_openpgp as openpgp;
use openpgp::Result;
use openpgp::armor;
use openpgp::cert::prelude::*;
use openpgp::parse::Parse;
use openpgp::packet::prelude::*;
use openpgp::policy::Policy;
use openpgp::policy::StandardPolicy;
use openpgp::serialize::Serialize;
use openpgp::types::HashAlgorithm;
use openpgp::types::KeyFlags;
use openpgp::types::RevocationStatus;
use openpgp::types::SignatureType;
use openpgp::types::SymmetricAlgorithm;
use crate::{
Config,
decrypt_key,
sq_cli::keyring::LinterCommand,
};
fn update_cert_revocation(cert: &Cert, rev: &Signature,
passwords: &mut Vec<String>,
reference_time: &SystemTime)
-> Result<Signature>
{
assert_eq!(rev.typ(), SignatureType::KeyRevocation);
let pk = cert.primary_key().key();
// Derive a signer.
let mut signer =
decrypt_key(
pk.clone().parts_into_secret()?,
passwords)?
.into_keypair()?;
let sig = SignatureBuilder::from(rev.clone())
.set_signature_creation_time(reference_time.clone())?
.set_hash_algo(HashAlgorithm::SHA256)
.preserve_signature_creation_time()?
.sign_direct_key(&mut signer, pk)?;
Ok(sig)
}
const GOOD_HASHES: &[ HashAlgorithm ] = &[
HashAlgorithm::SHA256,
HashAlgorithm::SHA512,
];
// Update the binding signature for ua.
//
// ua is using a weak policy.
fn update_user_id_binding(ua: &ValidUserIDAmalgamation,
passwords: &mut Vec<String>,
reference_time: &SystemTime)
-> Result<Signature>
{
let pk = ua.cert().primary_key().key();
// Derive a signer.
let mut signer =
decrypt_key(
pk.clone().parts_into_secret()?,
passwords)?
.into_keypair()?;
let sym = &[
SymmetricAlgorithm::AES128,
SymmetricAlgorithm::AES192,
SymmetricAlgorithm::AES256,
SymmetricAlgorithm::Twofish,
SymmetricAlgorithm::Camellia128,
SymmetricAlgorithm::Camellia192,
SymmetricAlgorithm::Camellia256,
];
// Update the signature.
let sig = ua.binding_signature();
let mut sig = SignatureBuilder::from(sig.clone())
.set_signature_creation_time(reference_time.clone())?
.set_hash_algo(GOOD_HASHES[0])
.set_preferred_hash_algorithms(
sig.preferred_hash_algorithms()
.unwrap_or(&[ HashAlgorithm::SHA512, HashAlgorithm::SHA256 ])
.iter()
.map(|h| h.clone())
.filter(|a| GOOD_HASHES.contains(&a))
.collect())?
.set_preferred_symmetric_algorithms(
sig.preferred_symmetric_algorithms()
.unwrap_or(&[
SymmetricAlgorithm::AES128,
SymmetricAlgorithm::AES256,
])
.iter()
.map(|h| h.clone())
.filter(|a| sym.contains(&a))
.collect())?
.sign_userid_binding(&mut signer, pk, ua.userid())?;
// Verify it.
assert!(sig.verify_userid_binding(signer.public(), pk, ua.userid())
.is_ok());
// Make sure the signature is integrated.
assert!(ua.cert().cert().clone()
.insert_packets(Packet::from(sig.clone())).unwrap()
.into_packets()
.any(|p| {
if let Packet::Signature(s) = p {
s == sig
} else {
false
}
}));
Ok(sig)
}
// Update the subkey binding signature for ka.
//
// ka is using a weak policy.
fn update_subkey_binding<P>(ka: &ValidSubordinateKeyAmalgamation<P>,
passwords: &mut Vec<String>,
reference_time: &SystemTime)
-> Result<Signature>
where P: key::KeyParts + Clone
{
let pk = ka.cert().primary_key().key();
// Derive a signer.
let mut signer =
decrypt_key(
pk.clone().parts_into_secret()?,
passwords)?
.into_keypair()?;
// Update the signature.
let sig = ka.binding_signature();
let mut builder = SignatureBuilder::from(sig.clone())
.set_signature_creation_time(reference_time.clone())?
.set_hash_algo(HashAlgorithm::SHA256);
// If there is a valid backsig, recreate it.
if let Some(backsig) = sig.embedded_signatures()
.filter(|backsig| {
(*backsig).clone().verify_primary_key_binding(
&ka.cert().cert().primary_key(),
ka.key()).is_ok()
})
.nth(0)
{
// Derive a signer.
let mut subkey_signer
= decrypt_key(
ka.key().clone().parts_into_secret()?,
passwords)?
.into_keypair()?;
let backsig = SignatureBuilder::from(backsig.clone())
.set_signature_creation_time(reference_time.clone())?
.set_hash_algo(HashAlgorithm::SHA256)
.sign_primary_key_binding(&mut subkey_signer, pk, ka.key())?;
builder = builder.set_embedded_signature(backsig)?;
}
let mut sig = builder.sign_subkey_binding(&mut signer, pk, ka.key())?;
// Verify it.
assert!(sig.verify_subkey_binding(signer.public(), pk, ka.key())
.is_ok());
// Make sure the signature is integrated.
assert!(ka.cert().cert().clone()
.insert_packets(Packet::from(sig.clone())).unwrap()
.into_packets()
.any(|p| {
if let Packet::Signature(s) = p {
s == sig
} else {
false
}
}));
Ok(sig)
}
pub fn linter(config: Config, mut args: LinterCommand) -> Result<()> {
// If there were any errors reading the input.
let mut bad_input = false;
// Number of certs that have issues.
let mut certs_with_issues = 0;
// Whether we were unable to fix at least one issue.
let mut unfixed_issue = 0;
// Standard policy that unconditionally rejects SHA-1: this is
// where we want to be.
let mut sp = StandardPolicy::new();
sp.reject_hash(HashAlgorithm::SHA1);
let sp = &sp;
// A standard policy that also accepts SHA-1.
let mut sp_sha1 = StandardPolicy::new();
sp_sha1.accept_hash(HashAlgorithm::SHA1);
let sp_sha1 = &sp_sha1;
// The number of valid and invalid certificates (according to
// SP+SHA-1).
let mut certs_valid = 0;
let mut certs_invalid = 0;
// Certificates that are revoked.
let mut certs_revoked = 0;
let mut certs_with_inadequota_revocations = 0;
let mut certs_expired = 0;
// Certificates that are valid and have a valid User ID.
let mut certs_sp_sha1_userids = 0;
let mut certs_with_a_sha1_protected_userid = 0;
let mut certs_with_only_sha1_protected_userids = 0;
// Subkeys.
let mut certs_with_subkeys = 0;
let mut certs_with_a_sha1_protected_binding_sig = 0;
let mut certs_with_signing_subkeys = 0;
let mut certs_with_sha1_protected_backsig = 0;
if args.list_keys {
args.quiet = true;
}
let reference_time = config.time;
let mut passwords: Vec<String> = args.password;
let mut out = args.output.create_pgp_safe(
config.force, args.binary,
if args.export_secret_keys {
armor::Kind::SecretKey
} else {
armor::Kind::PublicKey
})?;
'next_input: for input in args.file {
let mut input_reader = input.open()?;
let filename = match input.inner() {
Some(path) => path.display().to_string(),
None => "/dev/stdin".to_string(),
};
let certp = CertParser::from_reader(&mut input_reader)?;
'next_cert: for (certi, certo) in certp.enumerate() {
match certo {
Err(err) => {
if ! args.quiet {
if certi == 0 {
eprintln!("{:?} does not appear to be a keyring: {}",
filename, err);
} else {
eprintln!("Encountered an error parsing {:?}: {}",
filename, err);
}
}
bad_input = true;
continue 'next_input;
}
Ok(cert) => {
// Whether we found at least one issue.
let mut found_issue = false;
macro_rules! diag {
($($arg:tt)*) => {{
// found_issue may appear to be unused if
// diag is immediately followed by a
// continue or break.
#[allow(unused_assignments)]
{
if ! found_issue {
certs_with_issues += 1;
found_issue = true;
if args.list_keys {
println!("{}",
cert.fingerprint().to_hex());
}
}
if ! args.quiet {
eprintln!($($arg)*);
}
}
}};
}
let mut updates: Vec<Signature> = Vec::new();
macro_rules! next_cert {
() => {{
if updates.len() > 0 {
let cert = cert.insert_packets(updates)?;
if args.export_secret_keys {
cert.as_tsk().serialize(&mut out)?;
} else {
cert.serialize(&mut out)?;
}
}
continue 'next_cert;
}}
}
let sp_vc = cert.with_policy(sp, reference_time);
let sp_sha1_vc = cert.with_policy(sp_sha1, reference_time);
if let Err(ref err) = sp_sha1_vc {
diag!("Certificate {} is not valid under \
the standard policy + SHA-1: {}",
cert.keyid().to_hex(), err);
certs_invalid += 1;
unfixed_issue += 1;
continue;
}
let sp_sha1_vc = sp_sha1_vc.unwrap();
certs_valid += 1;
// Check if the certificate is revoked.
//
// There are four cases to consider:
//
// 1. SHA1 certificate, SHA1 revocation certificate
// 2. SHA1 certificate, SHA256 revocation certificate
// 3. SHA256 certificate, SHA1 revocation certificate
// 4. SHA256 certificate, SHA256 revocation certificate
//
// When the revocation certificate uses SHA256,
// there is nothing to do even if something else
// relies on SHA1: the certificate should be
// ignore, because it is revoked!
//
// In the case that we have a SHA1 certificate and
// a SHA1 revocation certificate, we also don't
// have to do anything: either the whole
// certificate will be considered invalid or
// implementation accepts SHA1 and it will be
// considered revoked.
//
// So, the only case that we have to fix is when
// the certificate uses SHA256, but the revocation
// certificate uses SHA1. In this case, we need
// to upgrade the revocation certificate.
if let RevocationStatus::Revoked(mut revs)
= sp_sha1_vc.revocation_status()
{
certs_revoked += 1;
if sp_vc.is_err() {
// Cases #1 and #2. Nothing to do.
next_cert!();
}
// Dedup based on creation time and the reason
// for revocation. Prefer revocations that do
// not use SHA-1.
let cmp = |a: &&Signature, b: &&Signature| -> Ordering
{
a.signature_creation_time()
.cmp(&b.signature_creation_time())
.then(a.reason_for_revocation()
.cmp(&b.reason_for_revocation()))
};
revs.sort_by(cmp);
revs.dedup_by(
|a: &mut &Signature, b: &mut &Signature| -> bool
{
let x = cmp(a, b);
if x != Ordering::Equal {
return false;
}
// Prefer the non-SHA-1 variant.
// Recall: if the elements are
// considered equal, a is removed and
// b is kept.
if GOOD_HASHES.contains(&a.hash_algo())
&& b.hash_algo() == HashAlgorithm::SHA1
{
*b = *a;
}
true
});
// See what revocation certificates need to be
// fixed.
let mut inadequate_revocation = false;
for rev in revs {
if rev.hash_algo() == HashAlgorithm::SHA1 {
if ! inadequate_revocation {
inadequate_revocation = true;
certs_with_inadequota_revocations += 1;
}
diag!("Certificate {}: Revocation certificate \
{:02X}{:02X} uses SHA-1.",
cert.keyid().to_hex(),
rev.digest_prefix()[0],
rev.digest_prefix()[1]);
if args.fix {
match update_cert_revocation(
&cert, rev, &mut passwords,
&reference_time)
{
Ok(sig) => {
updates.push(sig);
}
Err(err) => {
unfixed_issue += 1;
eprintln!("Certificate {}: \
Failed to update \
revocation certificate \
{:02X}{:02X}: {}",
cert.keyid().to_hex(),
rev.digest_prefix()[0],
rev.digest_prefix()[1],
err);
}
}
}
}
continue;
}
next_cert!();
}
// Check if the certificate is alive.
match (sp_sha1_vc.alive(),
sp_vc.as_ref().map(|vc| vc.alive()))
{
(Err(_), Err(_)) => {
// SP+SHA1: Not alive, SP: Invalid
//
// It only uses SHA1, and under SP+SHA1,
// it is expired. Invalid or expired, we
// don't need to fix it.
certs_expired += 1;
next_cert!();
}
(Err(_), Ok(Err(_))) => {
// SP+SHA1: Not alive, SP: Not alive.
//
// However you look at it, it's expired.
certs_expired += 1;
next_cert!();
}
(Err(_), Ok(Ok(_))) => {
// SP+SHA1: Not alive, SP: Alive
//
// Impossible.
panic!();
}
(Ok(_), Err(_)) => {
// SP+SHA1: Alive, SP: Invalid.
//
// The certificate only uses SHA-1. Lint
// it as usual.
()
}
(Ok(_), Ok(Err(_))) => {
// SP+SHA1: Alive, SP: Not alive.
//
// We have a binding signature using SHA1
// that says the certificate does not
// expire, and a newer binding signature
// using SHA2+ that is expired.
//
// Linting should(tm) fix this.
diag!("Certificate {} is live under SP+SHA1, \
but expire under SP.",
cert.keyid().to_hex());
}
(Ok(_), Ok(Ok(_))) => {
// SP+SHA1: Alive, SP: Alive. Lint it as
// usual.
()
}
}
if let Err(ref err) = sp_vc {
diag!("Certificate {} is not valid under \
the standard policy: {}",
cert.keyid().to_hex(), err);
}
// User IDs that are not revoked, and valid under
// the standard policy + SHA-1.
let mut a_userid = false;
let mut sha1_protected_userid = false;
let mut not_sha1_protected_userid = false;
let not_revoked = |ua: &ValidUserIDAmalgamation| -> bool {
if let RevocationStatus::Revoked(_)
= ua.revocation_status()
{
false
} else {
true
}
};
for ua in sp_sha1_vc.userids().filter(not_revoked) {
if ! a_userid {
a_userid = true;
certs_sp_sha1_userids += 1;
}
let sig = ua.binding_signature();
if sig.hash_algo() == HashAlgorithm::SHA1 {
diag!("Certificate {} contains a \
User ID ({:?}) protected by SHA-1",
cert.keyid().to_hex(),
String::from_utf8_lossy(ua.value()));
if !sha1_protected_userid {
sha1_protected_userid = true;
certs_with_a_sha1_protected_userid += 1;
}
if args.fix {
match update_user_id_binding(
&ua, &mut passwords, &reference_time)
{
Ok(sig) => {
updates.push(sig);
}
Err(err) => {
unfixed_issue += 1;
eprintln!("Certificate {}: User ID {}: \
Failed to update \
binding signature: {}",
cert.keyid().to_hex(),
String::from_utf8_lossy(
ua.value()),
err);
}
}
}
} else {
if !not_sha1_protected_userid {
not_sha1_protected_userid = true;
}
}
}
if sha1_protected_userid && ! not_sha1_protected_userid {
certs_with_only_sha1_protected_userids += 1;
}
let sha1_subkeys: Vec<_> = sp_sha1_vc
.keys().subkeys()
.revoked(false).alive()
.collect();
if sha1_subkeys.len() > 0 {
certs_with_subkeys += 1;
// Does this certificate have any subkeys whose
// binding signatures use SHA-1?
let mut uses_sha1_protected_binding_sig = false;
let mut uses_certs_with_signing_subkeys = false;
let mut uses_sha1_protected_backsig = false;
for ka in sha1_subkeys.into_iter() {
let sig = ka.binding_signature();
if sig.hash_algo() == HashAlgorithm::SHA1 {
diag!("Certificate {}, key {} uses a \
SHA-1-protected binding signature.",
cert.keyid().to_hex(),
ka.keyid().to_hex());
if ! uses_sha1_protected_binding_sig {
uses_sha1_protected_binding_sig = true;
certs_with_a_sha1_protected_binding_sig += 1;
}
if args.fix {
match update_subkey_binding(
&ka, &mut passwords,
&reference_time)
{
Ok(sig) => updates.push(sig),
Err(err) => {
unfixed_issue += 1;
eprintln!("Certificate {}, key {}: \
Failed to update \
binding signature: {}",
cert.keyid().to_hex(),
ka.keyid().to_hex(),
err);
}
}
}
continue;
}
// Check if the backsig uses SHA-1.
if ! ka.has_any_key_flag(
KeyFlags::empty()
.set_signing()
.set_certification())
{
// No backsig required.
continue;
}
if ! uses_certs_with_signing_subkeys {
uses_certs_with_signing_subkeys = true;
certs_with_signing_subkeys += 1;
}
// Get the cryptographically valid backsigs.
let mut backsigs: Vec<_> = sig.embedded_signatures()
.filter(|backsig| {
(*backsig).clone().verify_primary_key_binding(
&cert.primary_key(),
ka.key()).is_ok()
})
.collect();
if backsigs.len() == 0 {
// We can't get here. If the key is
// valid under sp+SHA-1, and requires
// a backsig, then it must have a
// valid backsig.
panic!("Valid signing-capable subkey without \
a valid backsig?");
}
backsigs.sort();
backsigs.dedup();
if backsigs.len() > 1 {
eprintln!("Warning: multiple cryptographically \
valid backsigs.");
}
if backsigs
.iter()
.any(|s| {
sp.signature(s, ka.hash_algo_security())
.is_ok()
})
{
// It's valid under the standard
// policy. We're fine.
} else if backsigs
.iter()
.any(|s| {
sp_sha1.signature(s, ka.hash_algo_security())
.is_ok()
})
{
// It's valid under SP+SHA-1 policy.
// Update it.
diag!("Certificate {}, key {} uses a \
{}-protected binding signature, \
but a SHA-1-protected backsig",
cert.keyid().to_hex(),
ka.keyid().to_hex(),
sig.hash_algo());
if ! uses_sha1_protected_backsig {
uses_sha1_protected_backsig = true;
certs_with_sha1_protected_backsig += 1;
}
if args.fix {
match update_subkey_binding(
&ka, &mut passwords, &reference_time)
{
Ok(sig) => updates.push(sig),
Err(err) => {
unfixed_issue += 1;
eprintln!("Certificate {}, key: {}: \
Failed to update \
binding signature: {}",
cert.keyid().to_hex(),
ka.keyid().to_hex(),
err);
}
}
}
} else {
let sig = backsigs[0];
let err = sp_sha1.signature(sig, ka.hash_algo_security()).unwrap_err();
diag!("Cert {}: backsig {:02X}{:02X} for \
{} is not valid under SP+SHA-1: {}. \
Ignoring.",
cert.keyid().to_hex(),
sig.digest_prefix()[0],
sig.digest_prefix()[1],
ka.keyid().to_hex(),
err);
unfixed_issue += 1;
}
}
}
if !found_issue {
if let Err(err) = sp_vc {
diag!("Certificate {} is not valid under \
the standard policy: {}",
cert.keyid().to_hex(), err);
}
}
next_cert!();
}
}
}
}
out.finalize()?;
let pl = |n, singular, plural| { if n == 1 { singular } else { plural } };
macro_rules! err {
($n:expr, $($arg:tt)*) => {{
eprint!($($arg)*);
eprint!(" (");
if $n > 0 {
if std::io::stderr().is_terminal() {
let mut stderr = StandardStream::stderr(ColorChoice::Always);
stderr.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
write!(&mut stderr, "BAD")?;
} else {
eprint!("BAD");
}
} else {
if std::io::stderr().is_terminal() {
let mut stderr = StandardStream::stderr(ColorChoice::Always);
stderr.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
write!(&mut stderr, "GOOD")?;
} else {
eprint!("GOOD");
}
}
eprintln!(")");
}};
}
if certs_with_issues > 0 {
eprintln!("Examined {} {}.",
certs_valid + certs_invalid,
pl(certs_valid + certs_invalid,
"certificate", "certificates"));
if ! args.quiet {
err!(certs_invalid,
" {} {} invalid and {} not linted.",
certs_invalid,
pl(certs_invalid, "certificate is", "certificates are"),
pl(certs_invalid, "was", "were"));
if certs_valid > 0 {
eprintln!(" {} {} linted.",
certs_valid,
pl(certs_valid,
"certificate was", "certificates were"));
err!(certs_with_issues,
" {} of the {} certificates ({}%) \
{} at least one issue.",
certs_with_issues,
certs_valid + certs_invalid,
certs_with_issues * 100 / (certs_valid + certs_invalid),
pl(certs_with_issues, "has", "have"));
eprintln!("{} of the linted certificates {} revoked.",
certs_revoked,
pl(certs_revoked, "was", "were"));
err!(certs_with_inadequota_revocations,
" {} of the {} certificates has revocation certificates \
that are weaker than the certificate and should be \
recreated.",
certs_with_inadequota_revocations,
certs_revoked);
eprintln!("{} of the linted certificates {} expired.",
certs_expired,
pl(certs_expired, "was", "were"));
eprintln!("{} of the non-revoked linted {} at least one non-revoked User ID:",
certs_sp_sha1_userids,
pl(certs_sp_sha1_userids,
"certificate has", "certificates have"));
err!(certs_with_a_sha1_protected_userid,
" {} {} at least one User ID protected by SHA-1.",
certs_with_a_sha1_protected_userid,
pl(certs_with_a_sha1_protected_userid, "has", "have"));
err!(certs_with_only_sha1_protected_userids,
" {} {} all User IDs protected by SHA-1.",
certs_with_only_sha1_protected_userids,
pl(certs_with_only_sha1_protected_userids,
"has", "have"));
eprintln!("{} of the non-revoked linted certificates {} at least one \
non-revoked, live subkey:",
certs_with_subkeys,
pl(certs_with_subkeys,
"has", "have"));
err!(certs_with_a_sha1_protected_binding_sig,
" {} {} at least one non-revoked, live subkey with \
a binding signature that uses SHA-1.",
certs_with_a_sha1_protected_binding_sig,
pl(certs_with_a_sha1_protected_binding_sig,
"has", "have"));
eprintln!("{} of the non-revoked linted certificates {} at least one non-revoked, live, \
signing-capable subkey:",
certs_with_signing_subkeys,
pl(certs_with_signing_subkeys,
"has", "have"));
err!(certs_with_sha1_protected_backsig,
" {} {} at least one non-revoked, live, signing-capable subkey \
with a strong binding signature, but a backsig \
that uses SHA-1.",
certs_with_sha1_protected_backsig,
pl(certs_with_sha1_protected_backsig,
"certificate has", "certificates have"));
}
}
if args.fix {
if unfixed_issue == 0 {
if bad_input {
exit(1);
} else {
exit(0);
}
} else {
if ! args.quiet {
err!(unfixed_issue,
"Failed to fix {} {}.",
unfixed_issue,
pl(unfixed_issue, "issue", "issues"));
}
exit(3);
}
} else {
exit(2);
}
}
if bad_input {
exit(1);
}
Ok(())
}

View File

@ -36,6 +36,7 @@ pub enum Subcommands {
Join(JoinCommand),
Merge(MergeCommand),
Filter(FilterCommand),
Linter(LinterCommand),
}
#[derive(Debug, Args)]
@ -313,3 +314,113 @@ pub struct SplitCommand {
)]
pub binary: bool,
}
/// Checks for and optionally repairs OpenPGP certificates that use
/// SHA-1.
#[derive(Debug, Args)]
#[clap(
about,
long_about = None,
after_help="\
`sq keyring linter` checks the supplied certificates for the following
SHA-1-related issues:
- Whether a certificate revocation uses SHA-1.
- Whether the current self signature for a non-revoked User ID uses
SHA-1.
- Whether the current subkey binding signature for a non-revoked,
live subkey uses SHA-1.
- Whether a primary key binding signature (\"backsig\") for a
non-revoked, live subkey uses SHA-1.
Diagnostics are printed to stderr. At the end, some statistics are
shown. This is useful when examining a keyring. If `--fix` is
specified and at least one issue could be fixed, the fixed
certificates are printed to stdout.
This tool does not currently support smart cards. But, if only the
subkeys are on a smart card, this tool may still be able to partially
repair the certificate. In particular, it will be able to fix any
issues with User ID self signatures and subkey binding signatures for
encryption-capable subkeys, but it will not be able to generate new
primary key binding signatures for any signing-capable subkeys.
EXIT STATUS:
If `--fix` is not specified:
2 if any issues were found,
1 if not issues were found, but there were errors reading the input,
0 if there were no issues.
If `--fix` is specified:
3 if any issues could not be fixed,
1 if not issues were found, but there were errors reading the input,
0 if all issues were fixed or there were no issues.
EXAMPLES:
# To gather statistics, simply run:
$ sq keyring linter keyring.pgp
# To fix a key:
$ gpg --export-secret-keys FPR | sq keyring linter --fix -p passw0rd -p password123 | gpg --import
# To get a list of keys with issues:
$ sq keyring linter --list-keys keyring.pgp | while read FPR; do something; done
"
)]
pub struct LinterCommand {
// (progn (make-local-variable 'fill-column) (setq fill-column 70))
/// Quiet; does not output any diagnostics.
#[arg(short, long)]
pub quiet: bool,
/// Attempts to fix certificates, when possible.
#[arg(short, long)]
pub fix: bool,
/// When fixing a certificate, the fixed certificate is exported
/// without any secret key material. Using this switch causes any
/// secret key material to also be exported.
#[arg(short, long)]
pub export_secret_keys: bool,
/// A key's password. Normally this is not needed: if stdin is
/// connected to a tty, the linter will ask for a password when
/// needed.
#[arg(short, long)]
pub password: Vec<String>,
/// If set, outputs a list of fingerprints, one per line, of
/// certificates that have issues. This output is intended for
/// use by scripts.
///
/// This option implies "--quiet". If you also specify "--fix",
/// errors will still be printed to stderr, and fixed certificates
/// will still be emitted to stdout.
#[arg(short='k', long)]
pub list_keys: bool,
/// A list of OpenPGP keyrings to process. If none are specified,
/// a keyring is read from stdin.
pub file: Vec<FileOrStdin>,
#[clap(
default_value_t = FileOrStdout::default(),
help = FileOrStdout::HELP,
long,
short,
value_name = FileOrStdout::VALUE_NAME,
)]
pub output: FileOrStdout,
#[clap(
short = 'B',
long = "binary",
help = "Emits binary data",
)]
pub binary: bool,
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,12 @@
-----BEGIN PGP MESSAGE-----
kA0DAAoBciO1ZnjgJSgByw1iAGNX9Z5mb29iYXIKiQEzBAABCgAdFiEEwD+mQRsD
rhJXZGEYciO1ZnjgJSgFAmNX9Z4ACgkQciO1ZnjgJSjsNgf+L3BLL62hDRUFJIEb
JsCh0iZCn4LWpY2+pO04GK82rYG1BtziMXNy30k3pRDoXDURVJ0yV5KuPvh7XPPe
6p4ZdIzzyRzFJfJLYY21nnjsTY51U71u7TlVABoLFX1vReSLfzNwxrSfZfKVwaEV
aCICMoEeebfqM+ZSjYjEcfzo8UjC3h2jSTkVKW0GI7HH5xO/8V0KLkmksWjOVZop
eMGhAnwu4sMKWCNyTE31LsVl5whezLzNwAevGMn7rEDtpyeGIf81hPF8GRlrF7BG
DDO+KjHh4tHHOSyPkwAAHc9ZXCW68ho7GZE/xK4Z4/otGCGwrwqAfYYU45oVIxLX
toSijQ==
=/d4H
-----END PGP MESSAGE-----

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

417
tests/sq-keyring-linter.rs Normal file
View File

@ -0,0 +1,417 @@
#[cfg(test)]
mod integration {
use std::path;
use assert_cmd::Command;
use predicates::prelude::*;
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("keyring-linter")
}
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 dir = dir();
let mut suffixes = vec![ "pub" ];
if let Some(prv) = prv {
suffixes.push(prv);
}
for suffix in suffixes.iter() {
// Lint it.
let filename = &format!("{}-{}.pgp", base, suffix);
eprintln!("Linting {}", filename);
Command::cargo_bin("sq").unwrap()
.current_dir(&dir)
.arg("--no-cert-store")
.arg("keyring").arg("linter")
.arg("--time").arg(FROZEN_TIME)
.arg(filename)
.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
};
let mut cmd = Command::cargo_bin("sq").unwrap();
let mut cmd = cmd.current_dir(&dir)
.args(&[
"--no-cert-store",
"keyring", "linter",
"--time", FROZEN_TIME,
"--fix", &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()
.current_dir(&dir)
.arg("--no-cert-store")
.arg("keyring").arg("linter")
.arg("--time").arg(FROZEN_TIME)
.arg("-")
.write_stdin(output)
.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::from_file(dir.clone().join(filename)).unwrap()
.into_packets()
.map(|p| {
if let Packet::Signature(_) = p {
1
} else {
0
}
})
.sum();
let fixed_sigs: isize = Cert::from_bytes(output)
.map(|cert| {
cert.into_packets()
.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);
}
#[test]
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);
}
#[test]
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",
"keyring", "linter",
"--time", FROZEN_TIME,
"--list-keys",
// 94F19D3CB5656E0BC3977C09A8AC5ACC2FB87104
"sha1-userid-pub.pgp",
// 55EF7181C288067AE189FF12F5A5CD01D8070917
"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",
"keyring", "linter",
"--time", FROZEN_TIME,
"msg.sig",
])
.assert()
// If there are issues, the exit code is 1.
.code(1);
}
}