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:
parent
f3cfb1b602
commit
74fd9dd8fe
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
859
src/commands/keyring/linter.rs
Normal file
859
src/commands/keyring/linter.rs
Normal 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(())
|
||||
}
|
@ -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,
|
||||
}
|
||||
|
BIN
tests/data/keyring-linter/all-sha1-password-Foobar-priv.pgp
Normal file
BIN
tests/data/keyring-linter/all-sha1-password-Foobar-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/all-sha1-password-Foobar-pub.pgp
Normal file
BIN
tests/data/keyring-linter/all-sha1-password-Foobar-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/authentication-subkey-priv.pgp
Normal file
BIN
tests/data/keyring-linter/authentication-subkey-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/authentication-subkey-pub.pgp
Normal file
BIN
tests/data/keyring-linter/authentication-subkey-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/gnupg-ecc-normal-priv.pgp
Normal file
BIN
tests/data/keyring-linter/gnupg-ecc-normal-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/gnupg-ecc-normal-pub.pgp
Normal file
BIN
tests/data/keyring-linter/gnupg-ecc-normal-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/gnupg-rsa-normal-priv.pgp
Normal file
BIN
tests/data/keyring-linter/gnupg-rsa-normal-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/gnupg-rsa-normal-pub.pgp
Normal file
BIN
tests/data/keyring-linter/gnupg-rsa-normal-pub.pgp
Normal file
Binary file not shown.
12
tests/data/keyring-linter/msg.sig
Normal file
12
tests/data/keyring-linter/msg.sig
Normal 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-----
|
BIN
tests/data/keyring-linter/multiple-passwords-priv.pgp
Normal file
BIN
tests/data/keyring-linter/multiple-passwords-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/multiple-passwords-pub.pgp
Normal file
BIN
tests/data/keyring-linter/multiple-passwords-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/no-backsig-priv.pgp
Normal file
BIN
tests/data/keyring-linter/no-backsig-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/no-backsig-pub.pgp
Normal file
BIN
tests/data/keyring-linter/no-backsig-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/only-md5-priv.pgp
Normal file
BIN
tests/data/keyring-linter/only-md5-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/only-md5-pub.pgp
Normal file
BIN
tests/data/keyring-linter/only-md5-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/only-sha1-priv.pgp
Normal file
BIN
tests/data/keyring-linter/only-sha1-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/only-sha1-pub.pgp
Normal file
BIN
tests/data/keyring-linter/only-sha1-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-authentication-subkey-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-authentication-subkey-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-authentication-subkey-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-authentication-subkey-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-backsig-signing-subkey-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-backsig-signing-subkey-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-backsig-signing-subkey-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-backsig-signing-subkey-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-cert-sha1-revocation-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-cert-sha1-revocation-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-cert-sha1-revocation-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-cert-sha1-revocation-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-cert-sha256-revocation-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-cert-sha256-revocation-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-cert-sha256-revocation-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-cert-sha256-revocation-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-encryption-subkey-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-encryption-subkey-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-encryption-subkey-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-encryption-subkey-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-expired-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-expired-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-expired-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-expired-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-offline-subkeys-offline.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-offline-subkeys-offline.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-offline-subkeys-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-offline-subkeys-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-offline-subkeys-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-offline-subkeys-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-signing-subkey-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-signing-subkey-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-signing-subkey-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-signing-subkey-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-userid-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-userid-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-userid-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-userid-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-userid-revoked-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-userid-revoked-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-userid-revoked-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-userid-revoked-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-userid-sha256-subkeys-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-userid-sha256-subkeys-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha1-userid-sha256-subkeys-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha1-userid-sha256-subkeys-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha256-cert-sha256-revocation-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha256-cert-sha256-revocation-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha256-cert-sha256-revocation-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha256-cert-sha256-revocation-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha256-expired-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha256-expired-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha256-expired-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha256-expired-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha256-expired-sha1-live-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha256-expired-sha1-live-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha256-expired-sha1-live-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha256-expired-sha1-live-pub.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha512-cert-sha1-revocation-priv.pgp
Normal file
BIN
tests/data/keyring-linter/sha512-cert-sha1-revocation-priv.pgp
Normal file
Binary file not shown.
BIN
tests/data/keyring-linter/sha512-cert-sha1-revocation-pub.pgp
Normal file
BIN
tests/data/keyring-linter/sha512-cert-sha1-revocation-pub.pgp
Normal file
Binary file not shown.
Binary file not shown.
417
tests/sq-keyring-linter.rs
Normal file
417
tests/sq-keyring-linter.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user