diff options
author | Nora Widdecke <nora@sequoia-pgp.org> | 2021-07-08 15:31:44 +0200 |
---|---|---|
committer | Nora Widdecke <nora@sequoia-pgp.org> | 2021-07-09 21:54:51 +0200 |
commit | c84b096add1d05afc41cc94b35a9db15bac0afea (patch) | |
tree | 3bcf860de8a5ea56ca55a69ca7dbebbfdd378964 /sqv/src | |
parent | c1a03f229e61402e9418ee31609f1b22cbe13e4a (diff) |
sqv: Move to https://gitlab.com/sequoia-pgp/sequoia-sqv
- From this point on, the crate sequoia-sqv will be maintained in
its own repository.
Diffstat (limited to 'sqv/src')
-rw-r--r-- | sqv/src/sqv-usage.rs | 42 | ||||
-rw-r--r-- | sqv/src/sqv.rs | 367 | ||||
-rw-r--r-- | sqv/src/sqv_cli.rs | 50 |
3 files changed, 0 insertions, 459 deletions
diff --git a/sqv/src/sqv-usage.rs b/sqv/src/sqv-usage.rs deleted file mode 100644 index 1838d6cd..00000000 --- a/sqv/src/sqv-usage.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! 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 -//! -V, --version Prints version information -//! -v, --verbose Be verbose. -//! -//! OPTIONS: -//! --keyring <FILE>... A keyring. Can be given multiple times. -//! --not-after <TIMESTAMP> -//! Consider signatures created after TIMESTAMP as invalid. If a date -//! is given, 23:59:59 is used for the time. -//! [default: now] -//! --not-before <TIMESTAMP> -//! Consider signatures created before TIMESTAMP as invalid. If a date -//! is given, 00:00:00 is used for the time. -//! [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. -//! -//! TIMESTAMPs must be given in ISO 8601 format (e.g. '2017-03-04T13:25:35Z', '2017- -//! 03-04T13:25', '20170304T1325+0830', '2017-03-04', '2017031', ...). If no -//! timezone is specified, UTC is assumed. -//! ``` - -#![doc(html_favicon_url = "https://docs.sequoia-pgp.org/favicon.png")] -#![doc(html_logo_url = "https://docs.sequoia-pgp.org/logo.svg")] - -include!("sqv.rs"); diff --git a/sqv/src/sqv.rs b/sqv/src/sqv.rs deleted file mode 100644 index 08beef3c..00000000 --- a/sqv/src/sqv.rs +++ /dev/null @@ -1,367 +0,0 @@ -/// A simple signature verification program. -/// -/// See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=872271 for -/// the motivation. - -use std::process::exit; - -use chrono::{DateTime, offset::Utc}; -use anyhow::Context; - -use sequoia_openpgp as openpgp; - -use crate::openpgp::{ - Cert, - KeyHandle, - Result, - parse::Parse, -}; -use crate::openpgp::parse::stream::{ - DetachedVerifierBuilder, - MessageLayer, - MessageStructure, - VerificationHelper, - GoodChecksum, - VerificationError, -}; -use crate::openpgp::cert::prelude::*; -use crate::openpgp::policy::StandardPolicy as P; - -mod sqv_cli; - -struct VHelper<'a> { - not_before: Option<std::time::SystemTime>, - not_after: std::time::SystemTime, - - good: usize, - total: usize, - threshold: usize, - - keyrings: clap::OsValues<'a>, -} - -impl<'a> std::fmt::Debug for VHelper<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - f.debug_struct("VHelper") - .field("not_before", &self.not_before) - .field("not_after", &self.not_after) - .field("good", &self.good) - .field("total", &self.total) - .field("threshold", &self.threshold) - .field("keyrings", &self.keyrings) - .finish() - } -} - -impl<'a> VHelper<'a> { - fn new(threshold: usize, - not_before: Option<std::time::SystemTime>, - not_after: std::time::SystemTime, - keyrings: clap::OsValues<'a>) -> Self { - VHelper { - not_before: not_before, - not_after: not_after, - good: 0, - total: 0, - threshold: threshold, - keyrings: keyrings, - } - } -} - -impl<'a> VerificationHelper for VHelper<'a> { - fn get_certs(&mut self, ids: &[crate::KeyHandle]) -> Result<Vec<Cert>> { - let mut certs = Vec::with_capacity(ids.len()); - - // Load relevant keys from the keyring. - for filename in self.keyrings.clone() { - for cert in CertParser::from_file(filename) - .with_context(|| format!("Failed to parse keyring {:?}", - filename))? - .unvalidated_cert_filter(|cert, _| { - // We don't skip keys that are valid (not revoked, - // alive, etc.) so that - cert.keys().key_handles(ids.iter()).next().is_some() - }) - { - certs.push(cert.with_context(|| { - format!("Malformed certificate in keyring {:?}", filename) - })?); - } - } - - // Dedup. To avoid cloning the certificates, we don't use - // Vec::dedup. - certs.sort_by(|a, b| a.fingerprint().cmp(&b.fingerprint())); - let count = certs.len(); - let (certs, errs) = certs.into_iter().fold( - (Vec::with_capacity(count), Vec::new()), - |(mut certs, mut errs), a| { - if certs.is_empty() { - certs.push(a); - } else if certs[certs.len() - 1].fingerprint() == a.fingerprint() { - // Merge `a` into the last element. - match certs.pop().expect("non-empty vec").merge_public(a) { - Ok(cert) => certs.push(cert), - Err(err) => errs.push(err), - } - } else { - certs.push(a); - } - - (certs, errs) - }); - - if !errs.is_empty() { - eprintln!("Error merging duplicate keys:"); - for err in errs.iter() { - eprintln!(" {}", err); - } - Err(errs.into_iter().next().expect("non-empty vec")) - } else { - Ok(certs) - } - } - - fn check(&mut self, structure: MessageStructure) -> Result<()> { - use self::VerificationError::*; - - let mut signers = Vec::with_capacity(2); - let mut verification_err = None; - - for layer in structure.into_iter() { - match layer { - MessageLayer::SignatureGroup { results } => - for result in results { - self.total += 1; - match result { - Ok(GoodChecksum { sig, ka, .. }) => { - match (sig.signature_creation_time(), - self.not_before, - self.not_after) - { - (None, _, _) => { - eprintln!("Malformed signature:"); - print_error_chain(&anyhow::anyhow!( - "no signature creation time")); - }, - (Some(t), Some(not_before), not_after) => { - if t < not_before { - eprintln!( - "Signature by {:X} was created before \ - the --not-before date.", - ka.key().fingerprint()); - } else if t > not_after { - eprintln!( - "Signature by {:X} was created after \ - the --not-after date.", - ka.key().fingerprint()); - } else { - signers.push(ka.cert().fingerprint()); - } - } - (Some(t), None, not_after) => { - if t > not_after { - eprintln!( - "Signature by {:X} was created after \ - the --not-after date.", - ka.key().fingerprint()); - } else { - signers.push(ka.cert().fingerprint()); - } - } - }; - } - Err(MalformedSignature { error, .. }) => { - eprintln!("Signature is malformed:"); - print_error_chain(&error); - } - Err(MissingKey { sig, .. }) => { - let issuers = sig.get_issuers(); - eprintln!("Missing key {:X}, which is needed to \ - verify signature.", - issuers.first().unwrap()); - } - Err(UnboundKey { cert, error, .. }) => { - eprintln!("Signing key on {:X} is not bound:", - cert.fingerprint()); - print_error_chain(&error); - } - Err(BadKey { ka, error, .. }) => { - eprintln!("Signing key on {:X} is bad:", - ka.cert().fingerprint()); - print_error_chain(&error); - } - Err(BadSignature { error, .. }) => { - eprintln!("Verifying signature:"); - print_error_chain(&error); - if verification_err.is_none() { - verification_err = Some(error) - } - } - } - } - MessageLayer::Compression { .. } => (), - _ => unreachable!(), - } - } - - // Dedup the keys so that it is not possible to exceed the - // threshold by duplicating signatures or by using the same - // key. - signers.sort(); - signers.dedup(); - - self.good = signers.len(); - for signer in signers { - println!("{:X}", signer); - } - - Ok(()) - } -} - -fn print_error_chain(err: &anyhow::Error) { - eprintln!(" {}", err); - err.chain().skip(1).for_each(|cause| eprintln!(" because: {}", cause)); -} - - -fn main() -> Result<()> { - let p = &P::new(); - - let matches = sqv_cli::build().get_matches(); - - let verbose = matches.is_present("verbose"); - - 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 file = matches.value_of_os("file").expect("'file' is required"); - let sig_file = matches.value_of_os("sig-file") - .expect("'sig-file' is required"); - - let not_before: Option<std::time::SystemTime> = - if let Some(t) = matches.value_of("not-before") { - Some(parse_iso8601(t, chrono::NaiveTime::from_hms(0, 0, 0)) - .context(format!("Bad value passed to --not-before: {:?}", t))? - .into()) - } else { - None - }; - let not_after: std::time::SystemTime = - if let Some(t) = matches.value_of("not-after") { - Some(parse_iso8601(t, chrono::NaiveTime::from_hms(23, 59, 59)) - .context(format!("Bad value passed to --not-after: {:?}", t))? - .into()) - } else { - None - }.unwrap_or_else(std::time::SystemTime::now); - - let keyrings = matches.values_of_os("keyring") - .expect("No keyring specified."); - - let h = VHelper::new(good_threshold, not_before, not_after, keyrings); - - let mut v = - DetachedVerifierBuilder::from_file(sig_file)?.with_policy(p, None, h)?; - v.verify_file(file)?; - - let h = v.into_helper(); - - if verbose { - eprintln!("{} of {} signatures are valid (threshold is: {}).", - h.good, h.total, good_threshold); - } - - exit(if h.good >= good_threshold { 0 } else { 1 }); -} - -/// Parses the given string depicting a ISO 8601 timestamp. -fn parse_iso8601(s: &str, pad_date_with: chrono::NaiveTime) - -> Result<DateTime<Utc>> -{ - // If you modify this function this function, synchronize the - // changes with the copy in sqv.rs! - for f in &[ - "%Y-%m-%dT%H:%M:%S%#z", - "%Y-%m-%dT%H:%M:%S", - "%Y-%m-%dT%H:%M%#z", - "%Y-%m-%dT%H:%M", - "%Y-%m-%dT%H%#z", - "%Y-%m-%dT%H", - "%Y%m%dT%H%M%S%#z", - "%Y%m%dT%H%M%S", - "%Y%m%dT%H%M%#z", - "%Y%m%dT%H%M", - "%Y%m%dT%H%#z", - "%Y%m%dT%H", - ] { - if f.ends_with("%#z") { - if let Ok(d) = DateTime::parse_from_str(s, *f) { - return Ok(d.into()); - } - } else { - if let Ok(d) = chrono::NaiveDateTime::parse_from_str(s, *f) { - return Ok(DateTime::from_utc(d, Utc)); - } - } - } - for f in &[ - "%Y-%m-%d", - "%Y-%m", - "%Y-%j", - "%Y%m%d", - "%Y%m", - "%Y%j", - "%Y", - ] { - if let Ok(d) = chrono::NaiveDate::parse_from_str(s, *f) { - return Ok(DateTime::from_utc(d.and_time(pad_date_with), Utc)); - } - } - Err(anyhow::anyhow!("Malformed ISO8601 timestamp: {}", s)) -} - -#[test] -fn test_parse_iso8601() { - let z = chrono::NaiveTime::from_hms(0, 0, 0); - parse_iso8601("2017-03-04T13:25:35Z", z).unwrap(); - parse_iso8601("2017-03-04T13:25:35+08:30", z).unwrap(); - parse_iso8601("2017-03-04T13:25:35", z).unwrap(); - parse_iso8601("2017-03-04T13:25Z", z).unwrap(); - parse_iso8601("2017-03-04T13:25", z).unwrap(); - // parse_iso8601("2017-03-04T13Z", z).unwrap(); // XXX: chrono doesn't like - // parse_iso8601("2017-03-04T13", z).unwrap(); // ditto - parse_iso8601("2017-03-04", z).unwrap(); - // parse_iso8601("2017-03", z).unwrap(); // ditto - parse_iso8601("2017-031", z).unwrap(); - parse_iso8601("20170304T132535Z", z).unwrap(); - parse_iso8601("20170304T132535+0830", z).unwrap(); - parse_iso8601("20170304T132535", z).unwrap(); - parse_iso8601("20170304T1325Z", z).unwrap(); - parse_iso8601("20170304T1325", z).unwrap(); - // parse_iso8601("20170304T13Z", z).unwrap(); // ditto - // parse_iso8601("20170304T13", z).unwrap(); // ditto - parse_iso8601("20170304", z).unwrap(); - // parse_iso8601("201703", z).unwrap(); // ditto - parse_iso8601("2017031", z).unwrap(); - // parse_iso8601("2017", z).unwrap(); // ditto -} diff --git a/sqv/src/sqv_cli.rs b/sqv/src/sqv_cli.rs deleted file mode 100644 index f1022319..00000000 --- a/sqv/src/sqv_cli.rs +++ /dev/null @@ -1,50 +0,0 @@ -/// Command-line parser for sqv. - -use clap::{App, Arg, AppSettings}; - -// The argument parser. -pub fn build() -> App<'static, 'static> { - configure(App::new("sqv")) -} - -pub fn configure(app: App<'static, 'static>) -> App<'static, 'static> { - app - .version(env!("CARGO_PKG_VERSION")) - .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") - .required(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")) - .arg(Arg::with_name("not-before").value_name("TIMESTAMP") - .help("Consider signatures created before TIMESTAMP as invalid. \ - If a date is given, 00:00:00 is used for the time. \ - \n[default: no constraint]") - .long("not-before")) - .arg(Arg::with_name("not-after").value_name("TIMESTAMP") - .help("Consider signatures created after TIMESTAMP as invalid. \ - If a date is given, 23:59:59 is used for the time. \ - \n[default: now]") - .long("not-after")) - .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("verbose") - .help("Be verbose.") - .long("verbose") - .short("v")) - .after_help( - "TIMESTAMPs must be given in ISO 8601 format \ - (e.g. '2017-03-04T13:25:35Z', '2017-03-04T13:25', \ - '20170304T1325+0830', '2017-03-04', '2017031', ...). \ - If no timezone is specified, UTC is assumed.") -} |