summaryrefslogtreecommitdiffstats
path: root/sqv/src
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2018-11-26 17:24:47 +0100
committerJustus Winter <justus@sequoia-pgp.org>2018-11-26 19:50:41 +0100
commit02e61f0758e93b44a054a01b4137ea25ff7dd5ec (patch)
treeb1ef6b097e5fc93faf20433219565bbabfa04fb5 /sqv/src
parent557aca35bad457622642308c1d780757b174bf50 (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.rs27
-rw-r--r--sqv/src/sqv.rs320
-rw-r--r--sqv/src/sqv_cli.rs47
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"))
+}