summaryrefslogtreecommitdiffstats
path: root/sqv
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
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')
-rw-r--r--sqv/Cargo.toml36
-rw-r--r--sqv/Makefile29
-rw-r--r--sqv/README.md8
-rw-r--r--sqv/build.rs20
-rw-r--r--sqv/make-usage.sh53
-rw-r--r--sqv/src/sqv-usage.rs27
-rw-r--r--sqv/src/sqv.rs320
-rw-r--r--sqv/src/sqv_cli.rs47
-rw-r--r--sqv/tests/bad-subkey.rs18
-rw-r--r--sqv/tests/data/bad-subkey-keyring.pgpbin0 -> 18064 bytes
-rw-r--r--sqv/tests/data/bad-subkey-keyring.txt12
-rw-r--r--sqv/tests/data/bad-subkey.txt1
-rw-r--r--sqv/tests/data/bad-subkey.txt.sigbin0 -> 310 bytes
-rw-r--r--sqv/tests/duplicate-signatures.rs31
-rw-r--r--sqv/tests/multiple-signatures.rs24
-rw-r--r--sqv/tests/not-before-after.rs62
16 files changed, 688 insertions, 0 deletions
diff --git a/sqv/Cargo.toml b/sqv/Cargo.toml
new file mode 100644
index 00000000..f9edb942
--- /dev/null
+++ b/sqv/Cargo.toml
@@ -0,0 +1,36 @@
+[package]
+name = "sequoia-sqv"
+description = "A simple signature verification program"
+version = "0.1.0"
+authors = [
+ "Justus Winter <justus@sequoia-pgp.org>",
+ "Kai Michaelis <kai@sequoia-pgp.org>",
+ "Neal H. Walfield <neal@sequoia-pgp.org>",
+]
+documentation = "https://docs.sequoia-pgp.org/sqv"
+homepage = "https://sequoia-pgp.org/"
+repository = "https://gitlab.com/sequoia-pgp/sequoia"
+readme = "README.md"
+keywords = ["cryptography", "openpgp", "pgp", "verification", "detached signatures"]
+categories = ["cryptography", "command-line-utilities"]
+license = "GPL-3.0"
+
+[badges]
+gitlab = { repository = "https://gitlab.com/sequoia-pgp/sequoia" }
+maintenance = { status = "actively-developed" }
+
+[dependencies]
+sequoia-openpgp = { path = "../openpgp", version = "0.1", default-features = false }
+clap = "2.32.0"
+failure = "0.1.2"
+time = "0.1.38"
+
+[build-dependencies]
+clap = "2.27.1"
+
+[dev-dependencies]
+assert_cli = "0.6"
+
+[[bin]]
+name = "sqv"
+path = "src/sqv-usage.rs"
diff --git a/sqv/Makefile b/sqv/Makefile
new file mode 100644
index 00000000..10df31b9
--- /dev/null
+++ b/sqv/Makefile
@@ -0,0 +1,29 @@
+# Configuration.
+CARGO_TARGET_DIR ?= $(shell pwd)/../target
+# We currently only support absolute paths.
+CARGO_TARGET_DIR := $(abspath $(CARGO_TARGET_DIR))
+SQV ?= $(CARGO_TARGET_DIR)/debug/sqv
+
+# Tools.
+CARGO ?= cargo
+INSTALL ?= install
+
+all: src/sqv-usage.rs
+
+# Installation.
+.PHONY: build-release
+build-release:
+ CARGO_TARGET_DIR=$(CARGO_TARGET_DIR) \
+ $(CARGO) build $(CARGO_FLAGS) --release --package sequoia-sqv
+
+.PHONY: install
+install: build-release
+ $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin
+ $(INSTALL) -t $(DESTDIR)$(PREFIX)/bin $(CARGO_TARGET_DIR)/release/sqv
+
+# Maintenance.
+.PHONY: update-usage
+update-usage: src/sqv-usage.rs
+
+src/sqv-usage.rs: make-usage.sh $(SQV)
+ sh make-usage.sh $(SQV) >$@
diff --git a/sqv/README.md b/sqv/README.md
new file mode 100644
index 00000000..7cfb9c04
--- /dev/null
+++ b/sqv/README.md
@@ -0,0 +1,8 @@
+A simple signature verification program.
+
+`sqv` verifies detached OpenPGP signatures. It is a replacement for
+`gpgv`. Unlike `gpgv`, it can take additional constraints on the
+signature into account.
+
+See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=872271 for
+the motivation.
diff --git a/sqv/build.rs b/sqv/build.rs
new file mode 100644
index 00000000..290a1a6b
--- /dev/null
+++ b/sqv/build.rs
@@ -0,0 +1,20 @@
+extern crate clap;
+
+use std::env;
+use clap::Shell;
+
+mod sqv_cli {
+ include!("src/sqv_cli.rs");
+}
+
+fn main() {
+ let outdir = match env::var_os("OUT_DIR") {
+ None => return,
+ Some(outdir) => outdir,
+ };
+ let mut sqv = sqv_cli::build();
+ for shell in &[Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell,
+ Shell::Elvish] {
+ sqv.gen_completions("sqv", *shell, &outdir);
+ }
+}
diff --git a/sqv/make-usage.sh b/sqv/make-usage.sh
new file mode 100644
index 00000000..bec96386
--- /dev/null
+++ b/sqv/make-usage.sh
@@ -0,0 +1,53 @@
+#!/bin/sh
+
+tool=$1
+
+quote() {
+ sed 's@^@//! @' | sed 's/ $//'
+}
+
+begin_code() {
+ printf '```text\n'
+}
+
+end_code() {
+ printf '```\n'
+}
+
+dump_help() { # subcommand, indention
+ if [ -z "$1" ]
+ then
+ printf "\n# Usage\n\n"
+ set "" "#"
+ else
+ printf "\n$2 Subcommand$1\n\n"
+ fi
+
+ help="`$tool $1 --help`"
+
+ begin_code
+ printf "$help\n" | tail -n +2
+ end_code
+
+ if echo $help | fgrep -q SUBCOMMANDS
+ then
+ printf "$help\n" |
+ sed -n '/^SUBCOMMANDS:/,$p' |
+ tail -n+2 |
+ while read subcommand desc
+ do
+ if [ "$subcommand" = help ]; then
+ continue
+ fi
+
+ dump_help "$1 $subcommand" "#$2"
+ done
+ fi
+}
+
+(
+ printf "A command-line frontend for Sequoia.\n"
+ dump_help
+) | quote
+
+printf '\ninclude!("'"$(basename $tool)"'.rs");\n'
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"))
+}
diff --git a/sqv/tests/bad-subkey.rs b/sqv/tests/bad-subkey.rs
new file mode 100644
index 00000000..6a6b7fc7
--- /dev/null
+++ b/sqv/tests/bad-subkey.rs
@@ -0,0 +1,18 @@
+extern crate assert_cli;
+
+#[cfg(test)]
+mod integration {
+ use std::path;
+
+ use assert_cli::Assert;
+
+ #[test]
+ fn bad_subkey() {
+ Assert::cargo_binary("sqv")
+ .current_dir(path::Path::new("tests").join("data"))
+ .with_args(&["-r", "bad-subkey-keyring.pgp",
+ "bad-subkey.txt.sig", "bad-subkey.txt"])
+ .stdout().is("8F17 7771 18A3 3DDA 9BA4 8E62 AACB 3243 6300 52D9")
+ .unwrap();
+ }
+}
diff --git a/sqv/tests/data/bad-subkey-keyring.pgp b/sqv/tests/data/bad-subkey-keyring.pgp
new file mode 100644
index 00000000..72acf4de
--- /dev/null
+++ b/sqv/tests/data/bad-subkey-keyring.pgp
Binary files differ
diff --git a/sqv/tests/data/bad-subkey-keyring.txt b/sqv/tests/data/bad-subkey-keyring.txt
new file mode 100644
index 00000000..d98d157b
--- /dev/null
+++ b/sqv/tests/data/bad-subkey-keyring.txt
@@ -0,0 +1,12 @@
+This key keyring contains two keys in the following order: "Justus",
+"Neal".
+
+Justus's key includes all of Neal's subkeys. When Justus's key is
+canonicalized, Neal's subkeys should be dropped.
+
+If an application looks for Neal's signing subkey and either doesn't
+validate the keys or only filters on the unvalidated keys, then it
+will not find the right key.
+
+This was fixed in sqv in commit
+1d63e71a839bf68f50cb7f4c1942f0d0b1eccfca.
diff --git a/sqv/tests/data/bad-subkey.txt b/sqv/tests/data/bad-subkey.txt
new file mode 100644
index 00000000..257cc564
--- /dev/null
+++ b/sqv/tests/data/bad-subkey.txt
@@ -0,0 +1 @@
+foo
diff --git a/sqv/tests/data/bad-subkey.txt.sig b/sqv/tests/data/bad-subkey.txt.sig
new file mode 100644
index 00000000..65fc578c
--- /dev/null
+++ b/sqv/tests/data/bad-subkey.txt.sig
Binary files differ
diff --git a/sqv/tests/duplicate-signatures.rs b/sqv/tests/duplicate-signatures.rs
new file mode 100644
index 00000000..691afb50
--- /dev/null
+++ b/sqv/tests/duplicate-signatures.rs
@@ -0,0 +1,31 @@
+extern crate assert_cli;
+
+use assert_cli::Assert;
+
+fn p(filename: &str) -> String {
+ format!("../openpgp/tests/data/{}", filename)
+}
+
+/// Asserts that duplicate signatures are properly ignored.
+#[test]
+fn ignore_duplicates() {
+ // Duplicate is ignored, but remaining one is ok.
+ Assert::cargo_binary("sqv")
+ .with_args(
+ &["-r",
+ &p("keys/emmelie-dorothea-dina-samantha-awina-ed25519.pgp"),
+ &p("messages/a-cypherpunks-manifesto.txt.ed25519.sig.duplicated"),
+ &p("messages/a-cypherpunks-manifesto.txt")])
+ .unwrap();
+
+ // Duplicate is ignored, and fails to meet the threshold.
+ Assert::cargo_binary("sqv")
+ .with_args(
+ &["-r",
+ &p("keys/emmelie-dorothea-dina-samantha-awina-ed25519.pgp"),
+ "--signatures=2",
+ &p("messages/a-cypherpunks-manifesto.txt.ed25519.sig.duplicated"),
+ &p("messages/a-cypherpunks-manifesto.txt")])
+ .fails()
+ .unwrap();
+}
diff --git a/sqv/tests/multiple-signatures.rs b/sqv/tests/multiple-signatures.rs
new file mode 100644
index 00000000..a2bff844
--- /dev/null
+++ b/sqv/tests/multiple-signatures.rs
@@ -0,0 +1,24 @@
+extern crate assert_cli;
+
+use assert_cli::Assert;
+
+fn p(filename: &str) -> String {
+ format!("../openpgp/tests/data/{}", filename)
+}
+
+/// Asserts that multiple signatures from the same TPK are properly
+/// ignored.
+#[test]
+fn ignore_multiple_signatures() {
+ // Multiple signatures from the same TPK are ignored, and fails to
+ // meet the threshold.
+ Assert::cargo_binary("sqv")
+ .with_args(
+ &["-r",
+ &p("keys/emmelie-dorothea-dina-samantha-awina-ed25519.pgp"),
+ "--signatures=2",
+ &p("messages/a-cypherpunks-manifesto.txt.ed25519.sig.duplicated"),
+ &p("messages/a-cypherpunks-manifesto.txt")])
+ .fails()
+ .unwrap();
+}
diff --git a/sqv/tests/not-before-after.rs b/sqv/tests/not-before-after.rs
new file mode 100644
index 00000000..5be41bf0
--- /dev/null
+++ b/sqv/tests/not-before-after.rs
@@ -0,0 +1,62 @@
+extern crate assert_cli;
+
+#[cfg(test)]
+mod integration {
+ use assert_cli::Assert;
+
+ fn p(filename: &str) -> String {
+ format!("../openpgp/tests/data/{}", filename)
+ }
+
+ #[test]
+ fn unconstrained() {
+ Assert::cargo_binary("sqv")
+ .with_args(
+ &["-r",
+ &p("keys/emmelie-dorothea-dina-samantha-awina-ed25519.pgp"),
+ &p("messages/a-cypherpunks-manifesto.txt.ed25519.sig"),
+ &