diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2018-11-26 17:24:47 +0100 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2018-11-26 19:50:41 +0100 |
commit | 02e61f0758e93b44a054a01b4137ea25ff7dd5ec (patch) | |
tree | b1ef6b097e5fc93faf20433219565bbabfa04fb5 /sqv/src | |
parent | 557aca35bad457622642308c1d780757b174bf50 (diff) |
sqv: Move sqv into a new crate.
- This allows us to use sequoia-openpgp without compression support
reducing binary size and trusted computing base.
Diffstat (limited to 'sqv/src')
-rw-r--r-- | sqv/src/sqv-usage.rs | 27 | ||||
-rw-r--r-- | sqv/src/sqv.rs | 320 | ||||
-rw-r--r-- | sqv/src/sqv_cli.rs | 47 |
3 files changed, 394 insertions, 0 deletions
diff --git a/sqv/src/sqv-usage.rs b/sqv/src/sqv-usage.rs new file mode 100644 index 00000000..d7e55716 --- /dev/null +++ b/sqv/src/sqv-usage.rs @@ -0,0 +1,27 @@ +//! A command-line frontend for Sequoia. +//! +//! # Usage +//! +//! ```text +//! sqv is a command-line OpenPGP signature verification tool. +//! +//! USAGE: +//! sqv [FLAGS] [OPTIONS] <SIG-FILE> <FILE> --keyring <FILE>... +//! +//! FLAGS: +//! -h, --help Prints help information +//! --trace Trace execution. +//! -V, --version Prints version information +//! +//! OPTIONS: +//! -r, --keyring <FILE>... A keyring. Can be given multiple times. +//! --not-after <YYYY-MM-DD> Consider signatures created after YYYY-MM-DD as invalid. Default: now +//! --not-before <YYYY-MM-DD> Consider signatures created before YYYY-MM-DD as invalid. Default: no constraint +//! -n, --signatures <N> The number of valid signatures to return success. Default: 1 +//! +//! ARGS: +//! <SIG-FILE> File containing the detached signature. +//! <FILE> File to verify. +//! ``` + +include!("sqv.rs"); diff --git a/sqv/src/sqv.rs b/sqv/src/sqv.rs new file mode 100644 index 00000000..567f6427 --- /dev/null +++ b/sqv/src/sqv.rs @@ -0,0 +1,320 @@ +/// A simple signature verification program. +/// +/// See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=872271 for +/// the motivation. + +extern crate clap; +extern crate failure; +use failure::ResultExt; +extern crate time; + +extern crate sequoia_openpgp as openpgp; + +use std::process::exit; +use std::fs::File; +use std::collections::{HashMap, HashSet}; + +use openpgp::{TPK, Packet, packet::Signature, KeyID}; +use openpgp::constants::HashAlgorithm; +use openpgp::parse::{PacketParserResult, PacketParser}; +use openpgp::tpk::TPKParser; + +mod sqv_cli; + +fn real_main() -> Result<(), failure::Error> { + let matches = sqv_cli::build().get_matches(); + + let trace = matches.is_present("trace"); + + let good_threshold + = if let Some(good_threshold) = matches.value_of("signatures") { + match good_threshold.parse::<usize>() { + Ok(good_threshold) => good_threshold, + Err(err) => { + eprintln!("Value passed to --signatures must be numeric: \ + {} (got: {:?}).", + err, good_threshold); + exit(2); + }, + } + } else { + 1 + }; + if good_threshold < 1 { + eprintln!("Value passed to --signatures must be >= 1 (got: {:?}).", + good_threshold); + exit(2); + } + + let not_before = if let Some(t) = matches.value_of("not-before") { + Some(time::strptime(t, "%Y-%m-%d") + .context(format!("Bad value passed to --not-before: {:?}", t))?) + } else { + None + }; + let not_after = if let Some(t) = matches.value_of("not-after") { + Some(time::strptime(t, "%Y-%m-%d") + .context(format!("Bad value passed to --not-after: {:?}", t))?) + } else { + None + }.unwrap_or_else(|| time::now_utc()); + + // First, we collect the signatures and the alleged issuers. + // Then, we scan the keyrings exactly once to find the associated + // TPKs. + + // .unwrap() is safe, because "sig-file" is required. + let sig_file = matches.value_of_os("sig-file").unwrap(); + + let mut ppr = PacketParser::from_reader( + openpgp::Reader::from_file(sig_file)?)?; + + let mut sigs_seen = HashSet::new(); + let mut sigs : Vec<(Signature, KeyID, Option<TPK>)> = Vec::new(); + + // sig_i is count of all Signature packets that we've seen. This + // may be more than sigs.len() if we can't handle some of the + // sigs. + let mut sig_i = 0; + + while let PacketParserResult::Some(pp) = ppr { + let (packet, ppr_tmp) = pp.recurse().unwrap(); + ppr = ppr_tmp; + + match packet { + Packet::Signature(sig) => { + // To check for duplicates, we normalize the + // signature, and put it into the hashset of seen + // signatures. + let mut sig_normalized = sig.clone(); + sig_normalized.unhashed_area_mut().clear(); + if sigs_seen.replace(sig_normalized).is_some() { + eprintln!("Ignoring duplicate signature."); + continue; + } + + sig_i += 1; + if let Some(fp) = sig.issuer_fingerprint() { + if trace { + eprintln!("Will check signature allegedly issued by {}.", + fp); + } + + // XXX: We use a KeyID even though we have a + // fingerprint! + sigs.push((sig, fp.to_keyid(), None)); + } else if let Some(keyid) = sig.issuer() { + if trace { + eprintln!("Will check signature allegedly issued by {}.", + keyid); + } + + sigs.push((sig, keyid, None)); + } else { + eprintln!("Signature #{} does not contain information \ + about the issuer. Unable to validate.", + sig_i); + } + }, + Packet::CompressedData(_) => { + // Skip it. + }, + packet => { + eprintln!("OpenPGP message is not a detached signature. \ + Encountered unexpected packet: {:?} packet.", + packet.tag()); + exit(2); + } + } + } + + if sigs.len() == 0 { + eprintln!("{:?} does not contain an OpenPGP signature.", sig_file); + exit(2); + } + + + // Hash the content. + + // .unwrap() is safe, because "file" is required. + let file = matches.value_of_os("file").unwrap(); + let hash_algos : Vec<HashAlgorithm> + = sigs.iter().map(|&(ref sig, _, _)| sig.hash_algo()).collect(); + let hashes: HashMap<_, _> = + openpgp::crypto::hash_file(File::open(file)?, &hash_algos[..])? + .into_iter().collect(); + + fn tpk_has_key(tpk: &TPK, keyid: &KeyID) -> bool { + if *keyid == tpk.primary().keyid() { + return true; + } + for binding in tpk.subkeys() { + if *keyid == binding.subkey().keyid() { + return true; + } + } + false + } + + // Find the keys. + for filename in matches.values_of_os("keyring") + .expect("No keyring specified.") + { + // Load the keyring. + let tpks : Vec<TPK> = TPKParser::from_reader( + openpgp::Reader::from_file(filename)?)? + .unvalidated_tpk_filter(|tpk, _| { + for &(_, ref issuer, _) in &sigs { + if tpk_has_key(tpk, issuer) { + return true; + } + } + false + }) + .map(|tpkr| { + match tpkr { + Ok(tpk) => tpk, + Err(err) => { + eprintln!("Error reading keyring {:?}: {}", + filename, err); + exit(2); + } + } + }) + .collect(); + + for tpk in tpks { + for &mut (_, ref issuer, ref mut issuer_tpko) in sigs.iter_mut() { + if tpk_has_key(&tpk, issuer) { + if let Some(issuer_tpk) = issuer_tpko.take() { + if trace { + eprintln!("Found key {} again. Merging.", + issuer); + } + + *issuer_tpko + = issuer_tpk.merge(tpk.clone()).ok(); + } else { + if trace { + eprintln!("Found key {}.", issuer); + } + + *issuer_tpko = Some(tpk.clone()); + } + } + } + } + } + + // Verify the signatures. + let mut sigs_seen_from_tpk = HashSet::new(); + let mut good = 0; + 'sig_loop: for (mut sig, issuer, tpko) in sigs.into_iter() { + if trace { + eprintln!("Checking signature allegedly issued by {}.", issuer); + } + + if let Some(ref tpk) = tpko { + // Find the right key. + for (_, key) in tpk.keys() { + if issuer == key.keyid() { + let mut hash = match hashes.get(&sig.hash_algo()) { + Some(h) => h.clone(), + None => { + eprintln!("Cannot check signature, hash algorithm \ + {} not supported.", sig.hash_algo()); + continue 'sig_loop; + }, + }; + sig.hash(&mut hash); + + let mut digest = vec![0u8; hash.digest_size()]; + hash.digest(&mut digest); + let hash_algo = sig.hash_algo(); + sig.set_computed_hash(Some((hash_algo, digest))); + + match sig.verify(key) { + Ok(true) => { + if let Some(t) = sig.signature_creation_time() { + if let Some(not_before) = not_before { + if t < not_before { + eprintln!( + "Signature by {} was created before \ + the --not-before date.", + issuer); + break; + } + } + + if t > not_after { + eprintln!( + "Signature by {} was created after \ + the --not-after date.", + issuer); + break; + } + } else { + eprintln!( + "Signature by {} does not contain \ + information about the creation time.", + issuer); + break; + } + + if trace { + eprintln!("Signature by {} is good.", issuer); + } + + if sigs_seen_from_tpk.replace(tpk.fingerprint()) + .is_some() + { + eprintln!( + "Ignoring additional good signature by {}.", + issuer); + continue; + } + + println!("{}", tpk.primary().fingerprint()); + good += 1; + }, + Ok(false) => { + if trace { + eprintln!("Signature by {} is bad.", issuer); + } + }, + Err(err) => { + if trace { + eprintln!("Verifying signature: {}.", err); + } + }, + } + + break; + } + } + } else { + eprintln!("Can't verify signature by {}, missing key.", + issuer); + } + } + + if trace { + eprintln!("{} of {} signatures are valid (threshold is: {}).", + good, sig_i, good_threshold); + } + + exit(if good >= good_threshold { 0 } else { 1 }); +} + +fn main() { + if let Err(e) = real_main() { + let mut cause = e.as_fail(); + eprint!("{}", cause); + while let Some(c) = cause.cause() { + eprint!(":\n {}", c); + cause = c; + } + eprintln!(); + exit(2); + } +} diff --git a/sqv/src/sqv_cli.rs b/sqv/src/sqv_cli.rs new file mode 100644 index 00000000..b06382ab --- /dev/null +++ b/sqv/src/sqv_cli.rs @@ -0,0 +1,47 @@ +/// Command-line parser for sqv. +/// +/// If you change this file, please rebuild `sqv`, run `make -C tool +/// update-usage`, and commit the resulting changes to +/// `tool/src/sqv-usage.rs`. + +use clap::{App, Arg, AppSettings}; + +// The argument parser. +pub fn build() -> App<'static, 'static> { + App::new("sqv") + .version("0.1.0") + .about("sqv is a command-line OpenPGP signature verification tool.") + .setting(AppSettings::ArgRequiredElseHelp) + .arg(Arg::with_name("keyring").value_name("FILE") + .help("A keyring. Can be given multiple times.") + .long("keyring") + .short("r") + .required(true) + .takes_value(true) + .number_of_values(1) + .multiple(true)) + .arg(Arg::with_name("signatures").value_name("N") + .help("The number of valid signatures to return success. Default: 1") + .long("signatures") + .short("n") + .takes_value(true)) + .arg(Arg::with_name("not-before").value_name("YYYY-MM-DD") + .help("Consider signatures created before YYYY-MM-DD as invalid. \ + Default: no constraint") + .long("not-before") + .takes_value(true)) + .arg(Arg::with_name("not-after").value_name("YYYY-MM-DD") + .help("Consider signatures created after YYYY-MM-DD as invalid. \ + Default: now") + .long("not-after") + .takes_value(true)) + .arg(Arg::with_name("sig-file").value_name("SIG-FILE") + .help("File containing the detached signature.") + .required(true)) + .arg(Arg::with_name("file").value_name("FILE") + .help("File to verify.") + .required(true)) + .arg(Arg::with_name("trace") + .help("Trace execution.") + .long("trace")) +} |