summaryrefslogtreecommitdiffstats
path: root/sop
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2020-04-15 10:36:40 +0200
committerJustus Winter <justus@sequoia-pgp.org>2020-04-21 17:10:27 +0200
commit8af5e4357c7348fe231b70a0fbe19788137caaa3 (patch)
treee8c28ca95583b4e4272cc95f7b2f4f1997dab6ac /sop
parent50c6526133ae72adc26800f3cc8e9884dab88518 (diff)
sop: An implementation of the Stateless OpenPGP CLI using Sequoia.
- This adds a new frontend to Sequoia that implements the Stateless OpenPGP Command Line Interface. - Compared to sq, sop has a much smaller feature set and hence a smaller set of dependencies. It is less opinionated, and tries to faithfully implement the SOP protocol. We will use it to test Sequoia using the OpenPGP Interoperability Test Suite.
Diffstat (limited to 'sop')
-rw-r--r--sop/Cargo.toml30
-rw-r--r--sop/Makefile24
-rw-r--r--sop/README.md7
-rw-r--r--sop/src/cli.rs315
-rw-r--r--sop/src/dates.rs95
-rw-r--r--sop/src/errors.rs101
-rw-r--r--sop/src/main.rs780
7 files changed, 1352 insertions, 0 deletions
diff --git a/sop/Cargo.toml b/sop/Cargo.toml
new file mode 100644
index 00000000..4e30997f
--- /dev/null
+++ b/sop/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "sequoia-sop"
+description = "An implementation of the Stateless OpenPGP Command Line Interface using Sequoia"
+version = "0.16.0"
+authors = [
+ "Justus Winter <justus@sequoia-pgp.org>",
+]
+documentation = "https://docs.sequoia-pgp.org/0.16.0/sop"
+homepage = "https://sequoia-pgp.org/"
+repository = "https://gitlab.com/sequoia-pgp/sequoia"
+readme = "README.md"
+keywords = ["cryptography", "openpgp", "pgp", "sop", "stateless-openpgp-protocol"]
+categories = ["cryptography", "command-line-utilities"]
+license = "GPL-2.0-or-later"
+edition = "2018"
+
+[badges]
+gitlab = { repository = "sequoia-pgp/sequoia" }
+maintenance = { status = "actively-developed" }
+
+[dependencies]
+sequoia-openpgp = { path = "../openpgp", version = "0.16" }
+anyhow = "1"
+chrono = "0.4"
+structopt = { version = "0.3", default-features = false }
+thiserror = "1"
+
+[[bin]]
+name = "sqop"
+path = "src/main.rs"
diff --git a/sop/Makefile b/sop/Makefile
new file mode 100644
index 00000000..5ffb9b45
--- /dev/null
+++ b/sop/Makefile
@@ -0,0 +1,24 @@
+# Configuration.
+CARGO_TARGET_DIR ?= $(shell pwd)/../target
+# We currently only support absolute paths.
+CARGO_TARGET_DIR := $(abspath $(CARGO_TARGET_DIR))
+SOP ?= $(CARGO_TARGET_DIR)/debug/sqop
+
+# Tools.
+CARGO ?= cargo
+ifeq ($(shell uname -s), Darwin)
+ INSTALL ?= ginstall
+else
+ INSTALL ?= install
+endif
+
+# Installation.
+.PHONY: build-release
+build-release:
+ CARGO_TARGET_DIR=$(CARGO_TARGET_DIR) \
+ $(CARGO) build $(CARGO_FLAGS) --release --package sequoia-sop
+
+.PHONY: install
+install: build-release
+ $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin
+ $(INSTALL) -t $(DESTDIR)$(PREFIX)/bin $(CARGO_TARGET_DIR)/release/sqop
diff --git a/sop/README.md b/sop/README.md
new file mode 100644
index 00000000..0b00731a
--- /dev/null
+++ b/sop/README.md
@@ -0,0 +1,7 @@
+An implementation of the Stateless OpenPGP Command Line Interface
+using Sequoia.
+
+This implements a subset of the [Stateless OpenPGP Command Line
+Interface] using the Sequoia OpenPGP implementation.
+
+ [Stateless OpenPGP Command Line Interface]: https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/
diff --git a/sop/src/cli.rs b/sop/src/cli.rs
new file mode 100644
index 00000000..58bbdd2d
--- /dev/null
+++ b/sop/src/cli.rs
@@ -0,0 +1,315 @@
+use std::fmt;
+use std::path::Path;
+
+use anyhow::Context;
+use chrono::{DateTime, offset::Utc};
+use structopt::StructOpt;
+
+use sequoia_openpgp as openpgp;
+use openpgp::{
+ Cert,
+ crypto::{
+ Password,
+ },
+ types::{
+ SignatureType,
+ DataFormat,
+ },
+ parse::Parse,
+};
+
+use super::{
+ dates,
+ Error,
+ Result,
+};
+
+#[derive(StructOpt)]
+#[structopt(about = "An implementation of the \
+ Stateless OpenPGP Command Line Interface \
+ using Sequoia")]
+pub enum SOP {
+ /// Prints version information.
+ Version {
+ },
+ /// Generates a Secret Key.
+ GenerateKey {
+ /// Don't ASCII-armor output.
+ #[structopt(long)]
+ no_armor: bool,
+ /// UserIDs for the generated key.
+ userids: Vec<String>,
+ },
+ /// Extracts a Certificate from a Secret Key.
+ ExtractCert {
+ /// Don't ASCII-armor output.
+ #[structopt(long)]
+ no_armor: bool,
+ },
+ /// Creates Detached Signatures.
+ Sign {
+ /// Don't ASCII-armor output.
+ #[structopt(long)]
+ no_armor: bool,
+ /// Sign binary data or UTF-8 text.
+ #[structopt(default_value = "binary", long = "as")]
+ as_: SignAs,
+ /// Keys for signing.
+ keys: Vec<String>,
+ },
+ /// Verifies Detached Signatures.
+ Verify {
+ /// Consider signatures before this date invalid.
+ #[structopt(long, parse(try_from_str = dates::parse_bound_round_down))]
+ not_before: Option<DateTime<Utc>>,
+ /// Consider signatures after this date invalid.
+ #[structopt(long, parse(try_from_str = dates::parse_bound_round_up))]
+ not_after: Option<DateTime<Utc>>,
+ /// Signatures to verify.
+ signatures: String,
+ /// Certs for verification.
+ certs: Vec<String>,
+ },
+ /// Encrypts a Message.
+ Encrypt {
+ /// Don't ASCII-armor output.
+ #[structopt(long)]
+ no_armor: bool,
+ /// Encrypt binary data, UTF-8 text, or MIME data.
+ #[structopt(default_value = "binary", long = "as")]
+ as_: EncryptAs,
+ /// Encrypt with passwords.
+ #[structopt(long)]
+ with_password: Vec<String>,
+ /// Keys for signing.
+ #[structopt(long)]
+ sign_with: Vec<String>,
+ /// Encrypt for these certs.
+ certs: Vec<String>,
+ },
+ /// Decrypts a Message.
+ Decrypt {
+ /// Write the session key here.
+ #[structopt(long)]
+ session_key_out: Option<String>,
+ /// Try to decrypt with this session key.
+ #[structopt(long)]
+ with_session_key: Vec<String>,
+ /// Try to decrypt with this password.
+ #[structopt(long)]
+ with_password: Vec<String>,
+ /// Write verification result here.
+ #[structopt(long)]
+ verify_out: Option<String>,
+ /// Certs for verification.
+ #[structopt(long)]
+ verify_with: Vec<String>,
+ /// Consider signatures before this date invalid.
+ #[structopt(long, parse(try_from_str = dates::parse_bound_round_down))]
+ verify_not_before: Option<DateTime<Utc>>,
+ /// Consider signatures after this date invalid.
+ #[structopt(long, parse(try_from_str = dates::parse_bound_round_up))]
+ verify_not_after: Option<DateTime<Utc>>,
+ /// Try to decrypt with this key.
+ key: Vec<String>,
+ },
+ /// Converts binary OpenPGP data to ASCII
+ Armor {
+ /// Indicates the kind of data
+ #[structopt(long, default_value = "auto")]
+ label: ArmorKind,
+ },
+ /// Converts ASCII OpenPGP data to binary
+ Dearmor {
+ },
+ /// Unsupported subcommand.
+ #[structopt(external_subcommand)]
+ Unsupported(Vec<String>),
+}
+
+#[derive(Clone, Copy)]
+pub enum SignAs {
+ Binary,
+ Text,
+}
+
+impl std::str::FromStr for SignAs {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> openpgp::Result<Self> {
+ match s {
+ "binary" => Ok(SignAs::Binary),
+ "text" => Ok(SignAs::Text),
+ _ => Err(anyhow::anyhow!(
+ "{:?}, expected one of {{binary|text}}", s)),
+ }
+ }
+}
+
+impl fmt::Display for SignAs {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ SignAs::Binary => f.write_str("binary"),
+ SignAs::Text => f.write_str("text"),
+ }
+ }
+}
+
+impl From<SignAs> for SignatureType {
+ fn from(a: SignAs) -> Self {
+ match a {
+ SignAs::Binary => SignatureType::Binary,
+ SignAs::Text => SignatureType::Text,
+ }
+ }
+}
+
+#[derive(Clone, Copy)]
+pub enum EncryptAs {
+ Binary,
+ Text,
+ MIME,
+}
+
+impl std::str::FromStr for EncryptAs {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> openpgp::Result<Self> {
+ match s {
+ "binary" => Ok(EncryptAs::Binary),
+ "text" => Ok(EncryptAs::Text),
+ "mime" => Ok(EncryptAs::MIME),
+ _ => Err(anyhow::anyhow!(
+ "{}, expected one of {{binary|text|mime}}", s)),
+ }
+ }
+}
+
+impl fmt::Display for EncryptAs {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ EncryptAs::Binary => f.write_str("binary"),
+ EncryptAs::Text => f.write_str("text"),
+ EncryptAs::MIME => f.write_str("mime"),
+ }
+ }
+}
+
+impl From<EncryptAs> for SignatureType {
+ fn from(a: EncryptAs) -> Self {
+ match a {
+ EncryptAs::Binary => SignatureType::Binary,
+ EncryptAs::Text => SignatureType::Text,
+ // XXX: We should inspect the serialized MIME structure
+ // and use Text if it is UTF-8, Binary otherwise. But, we
+ // cannot be bothered at this point.
+ EncryptAs::MIME => SignatureType::Binary,
+ }
+ }
+}
+
+impl From<EncryptAs> for DataFormat {
+ fn from(a: EncryptAs) -> Self {
+ match a {
+ EncryptAs::Binary => DataFormat::Binary,
+ EncryptAs::Text => DataFormat::Text,
+ EncryptAs::MIME => DataFormat::MIME,
+ }
+ }
+}
+
+#[derive(Clone, Copy)]
+pub enum ArmorKind {
+ Auto,
+ Sig,
+ Key,
+ Cert,
+ Message,
+}
+
+impl std::str::FromStr for ArmorKind {
+ type Err = anyhow::Error;
+ fn from_str(s: &str) -> openpgp::Result<Self> {
+ match s {
+ "auto" => Ok(ArmorKind::Auto),
+ "sig" => Ok(ArmorKind::Sig),
+ "key" => Ok(ArmorKind::Key),
+ "cert" => Ok(ArmorKind::Cert),
+ "message" => Ok(ArmorKind::Message),
+ _ => Err(anyhow::anyhow!(
+ "{:?}, expected one of \
+ {{auto|sig|key|cert|message}}", s)),
+ }
+ }
+}
+
+impl fmt::Display for ArmorKind {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ArmorKind::Auto => f.write_str("auto"),
+ ArmorKind::Sig => f.write_str("sig"),
+ ArmorKind::Key => f.write_str("key"),
+ ArmorKind::Cert => f.write_str("cert"),
+ ArmorKind::Message => f.write_str("message"),
+ }
+ }
+}
+
+
+fn is_special_designator<S: AsRef<str>>(file: S) -> bool {
+ file.as_ref().starts_with("@")
+}
+
+/// Loads the given (special) file.
+pub fn load_file<S: AsRef<str>>(file: S) -> Result<std::fs::File> {
+ let f = file.as_ref();
+
+ if is_special_designator(f) {
+ if Path::new(f).exists() {
+ return Err(anyhow::Error::from(Error::AmbiguousInput))
+ .context(format!("File {:?} exists", f));
+ }
+
+ return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix));
+ }
+
+ std::fs::File::open(f).map_err(|_| Error::MissingInput)
+ .context(format!("Failed to open file {:?}", f))
+}
+
+/// Creates the given (special) file.
+pub fn create_file<S: AsRef<str>>(file: S) -> Result<std::fs::File> {
+ let f = file.as_ref();
+
+ if is_special_designator(f) {
+ if Path::new(f).exists() {
+ return Err(anyhow::Error::from(Error::AmbiguousInput))
+ .context(format!("File {:?} exists", f));
+ }
+
+ return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix));
+ }
+
+ if Path::new(f).exists() {
+ return Err(anyhow::Error::from(Error::OutputExists))
+ .context(format!("File {:?} exists", f));
+ }
+
+ std::fs::File::create(f).map_err(|_| Error::MissingInput) // XXX
+ .context(format!("Failed to create file {:?}", f))
+}
+
+/// Loads the certs given by the (special) files.
+pub fn load_certs(files: Vec<String>) -> Result<Vec<Cert>> {
+ let mut certs = vec![];
+ for f in files {
+ let r = load_file(&f)?;
+ certs.push(Cert::from_reader(r).map_err(|_| Error::BadData)
+ .context(format!("Failed to load key from file {:?}", f))?);
+ }
+ Ok(certs)
+}
+
+/// Frobnicates the strings and converts them to passwords.
+pub fn frob_passwords(p: Vec<String>) -> Result<Vec<Password>> {
+ // XXX: Maybe do additional checks.
+ Ok(p.iter().map(|p| p.trim_end().into()).collect())
+}
diff --git a/sop/src/dates.rs b/sop/src/dates.rs
new file mode 100644
index 00000000..b76fe1fc
--- /dev/null
+++ b/sop/src/dates.rs
@@ -0,0 +1,95 @@
+use chrono::{DateTime, offset::Utc};
+
+use crate::Result;
+
+/// Parses the given string depicting a ISO 8601 timestamp, rounding down.
+pub fn parse_bound_round_down(s: &str) -> Result<DateTime<Utc>> {
+ match s {
+ // XXX: parse "-" to None once we figure out how to do that
+ // with structopt.
+ "now" => Ok(Utc::now()),
+ _ => parse_iso8601(s, chrono::NaiveTime::from_hms(0, 0, 0)),
+ }
+}
+
+/// Parses the given string depicting a ISO 8601 timestamp, rounding up.
+pub fn parse_bound_round_up(s: &str) -> Result<DateTime<Utc>> {
+ match s {
+ // XXX: parse "-" to None once we figure out how to do that
+ // with structopt.
+ "now" => Ok(Utc::now()),
+ _ => parse_iso8601(s, chrono::NaiveTime::from_hms(23, 59, 59)),
+ }
+}
+
+/// 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/sop/src/errors.rs b/sop/src/errors.rs
new file mode 100644
index 00000000..0eae86b7
--- /dev/null
+++ b/sop/src/errors.rs
@@ -0,0 +1,101 @@
+/// Errors defined by the Stateless OpenPGP Protocol.
+#[derive(thiserror::Error, Debug, Clone)]
+pub enum Error {
+ /// No acceptable signatures found ("sop verify").
+ #[error("No acceptable signatures found")]
+ NoSignature,
+
+ /// Asymmetric algorithm unsupported ("sop encrypt").
+ #[error("Asymmetric algorithm unsupported")]
+ UnsupportedAsymmetricAlgo,
+
+ /// Certificate not encryption-capable (e.g., expired, revoked,
+ /// unacceptable usage flags) ("sop encrypt").
+ #[error("Certificate not encryption-capable")]
+ CertCannotEncrypt,
+
+ /// Missing required argument.
+ #[error("Missing required argument")]
+ MissingArg,
+
+ /// Incomplete verification instructions ("sop decrypt").
+ #[error("Incomplete verification instructions")]
+ IncompleteVerification,
+
+ /// Unable to decrypt ("sop decrypt").
+ #[error("Unable to decrypt")]
+ CannotDecrypt,
+
+ /// Non-"UTF-8" or otherwise unreliable password ("sop encrypt").
+ #[error("Non-UTF-8 or otherwise unreliable password")]
+ PasswordNotHumanReadable,
+
+ /// Unsupported option.
+ #[error("Unsupported option")]
+ UnsupportedOption,
+
+ /// Invalid data type (no secret key where "KEY" expected, etc).
+ #[error("Invalid data type")]
+ BadData,
+
+ /// Non-text input where text expected.
+ #[error("Non-text input where text expected")]
+ ExpectedText,
+
+ /// Output file already exists.
+ #[error("Output file already exists")]
+ OutputExists,
+
+ /// Input file does not exist.
+ #[error("Input file does not exist")]
+ MissingInput,
+
+ /// A "KEY" input is protected (locked) with a password, and "sop" cannot
+ /// unlock it.
+ #[error("A KEY input is protected with a password")]
+ KeyIsProtected,
+
+ /// Unsupported subcommand.
+ #[error("Unsupported subcommand")]
+ UnsupportedSubcommand,
+
+ /// An indirect parameter is a special designator (it starts with "@") but
+ /// "sop" does not know how to handle the prefix.
+ #[error("An indirect parameter is a special designator with unknown prefix")]
+ UnsupportedSpecialPrefix,
+
+ /// A indirect input parameter is a special designator (it starts with
+ /// "@"), and a filename matching the designator is actually present.
+ #[error("A indirect input parameter is a special designator matches file")]
+ AmbiguousInput,
+}
+
+impl From<Error> for i32 {
+ fn from(e: Error) -> Self {
+ use Error::*;
+ match e {
+ NoSignature => 3,
+ UnsupportedAsymmetricAlgo => 13,
+ CertCannotEncrypt => 17,
+ MissingArg => 19,
+ IncompleteVerification => 23,
+ CannotDecrypt => 29,
+ PasswordNotHumanReadable => 31,
+ UnsupportedOption => 37,
+ BadData => 41,
+ ExpectedText => 53,
+ OutputExists => 59,
+ MissingInput => 61,
+ KeyIsProtected => 67,
+ UnsupportedSubcommand => 69,
+ UnsupportedSpecialPrefix => 71,
+ AmbiguousInput => 73,
+ }
+ }
+}
+
+/// Prints the error and causes, if any.
+pub fn print_error_chain(err: &anyhow::Error) {
+ eprintln!(" {}", err);
+ err.chain().skip(1).for_each(|cause| eprintln!(" because: {}", cause));
+}
diff --git a/sop/src/main.rs b/sop/src/main.rs
new file mode 100644
index 00000000..e6826577
--- /dev/null
+++ b/sop/src/main.rs
@@ -0,0 +1,780 @@
+//! An implementation of the Stateless OpenPGP Command Line Interface
+//! using Sequoia.
+//!
+//! This implements a subset of the [Stateless OpenPGP Command Line
+//! Interface] using the Sequoia OpenPGP implementation.
+//!
+//! [Stateless OpenPGP Command Line Interface]: https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/
+
+use std::collections::HashMap;
+use std::convert::TryFrom;
+use std::io::{self, Read, Write};
+
+use anyhow::Context;
+use structopt::StructOpt;
+
+use sequoia_openpgp as openpgp;
+use openpgp::{
+ armor,
+ Cert,
+ Fingerprint,
+ KeyID,
+ KeyHandle,
+ Packet,
+};
+use openpgp::crypto::{self, Password, SessionKey};
+use openpgp::fmt::hex;
+use openpgp::types::*;
+use openpgp::packet::{key, signature, Key, PKESK, SKESK};
+use openpgp::parse::{Parse, PacketParser, PacketParserResult, stream::*};
+use openpgp::policy::Policy;
+use openpgp::cert::prelude::*;
+use openpgp::serialize::{
+ Serialize,
+ stream::*,
+ stream::padding::{Padder, padme},
+};
+use openpgp::policy::StandardPolicy;
+
+mod errors;
+use errors::{Error, print_error_chain};
+type Result<T> = anyhow::Result<T>;
+
+mod cli;
+use cli::{
+ SOP, SignAs, EncryptAs, ArmorKind,
+ load_file, create_file, load_certs, frob_passwords,
+};
+mod dates;
+
+fn main() {
+ use std::process::exit;
+
+ match real_main() {
+ Ok(()) => (),
+ Err(e) => {
+ print_error_chain(&e);
+ if let Ok(e) = e.downcast::<Error>() {
+ exit(e.into())
+ }
+ exit(1);
+ },
+ }
+}
+
+fn real_main() -> Result<()> {
+ let p = &StandardPolicy::default();
+
+ match SOP::from_args() {
+ SOP::Version {} => {
+ println!("Sequoia-SOP {}", openpgp::VERSION);
+ },
+
+ SOP::GenerateKey { no_armor, mut userids, } => {
+ userids.reverse();
+ let mut builder = CertBuilder::general_purpose(None, userids.pop());
+ for u in userids {
+ builder = builder.add_userid(u);
+ }
+ let (cert, _) = builder.generate()?;
+
+ let mut sink = stdout(no_armor, armor::Kind::SecretKey)?;
+ cert.as_tsk().serialize(&mut sink)?;
+ sink.finalize()?;
+ },
+
+ SOP::ExtractCert { no_armor, } => {
+ let cert = Cert::from_reader(&mut io::stdin())?;
+ let mut sink = stdout(no_armor, armor::Kind::SecretKey)?;
+ cert.serialize(&mut sink)?;
+ sink.finalize()?;
+ },
+
+ SOP::Sign { no_armor, as_, keys, } => {
+ let mut data = Vec::new();
+ io::stdin().read_to_end(&mut data)?;
+ if let SignAs::Text = as_ {
+ if let Err(e) = std::str::from_utf8(&data) {
+ return Err(anyhow::Error::from(Error::ExpectedText))
+ .context(e.to_string());
+ }
+ }
+
+ let tsks = load_certs(keys)?;
+ if tsks.is_empty() {
+ return Err(anyhow::Error::from(Error::MissingArg))
+ .context("Expected at least one certificate");
+ }
+ let mut signers = Vec::new();
+ let mut hash_algos = vec![
+ HashAlgorithm::SHA512,
+ HashAlgorithm::SHA384,
+ HashAlgorithm::SHA256,
+ HashAlgorithm::SHA224,
+ HashAlgorithm::RipeMD,
+ HashAlgorithm::SHA1,
+ HashAlgorithm::MD5,
+ ];
+ for tsk in tsks {
+ let tsk = tsk.with_policy(p, None).map_err(|e| {
+ anyhow::Error::from(Error::CertCannotEncrypt) // XXX
+ .context(format!("Key {} not valid: {}", tsk, e))
+ })?;
+ if let Some(p) = tsk.preferred_hash_algorithms() {
+ hash_algos.retain(|a| p.contains(a));
+ }
+
+ let mut one = false;
+ for key in tsk.keys()
+ .secret()
+ .alive()
+ .revoked(false)
+ .for_signing()
+ .map(|ka| ka.key())
+ {
+ if key.secret().is_encrypted() {
+ return Err(Error::KeyIsProtected.into());
+ }
+ signers.push(key.clone().into_keypair()
+ .expect("not encrypted"));
+ one = true;
+ // Exactly one signature per supplied key.
+ break;
+ }
+
+ if ! one {
+ return Err(anyhow::Error::from(Error::CertCannotEncrypt))
+ .context(format!("Cert {} not capable of signing",
+ tsk));
+ }
+ }
+
+ let message = stdout(no_armor, armor::Kind::Signature)?;
+ let mut signer = Signer::with_template(
+ message, signers.pop().expect("at least one"),
+ signature::Builder::new(as_.into()))
+ .hash_algo(hash_algos.get(0).cloned().unwrap_or_default())?
+ .detached();
+ for s in signers {
+ signer = signer.add_signer(s);
+ }
+ let mut message = signer.build()?;
+ message.write_all(&data)?;
+ message.finalize()?;
+ },
+
+ SOP::Verify { not_before, not_after, signatures, certs, } => {
+ let certs = load_certs(certs)?;
+ let signatures = load_file(signatures)?;
+ let helper = VHelper::new(io::stdout(),
+ 1,
+ not_before.map(|d| d.into()),
+ not_after.map(|d| d.into()),
+ certs);
+ let mut v =
+ DetachedVerifier::from_reader(p, signatures, helper, None)?;
+ v.verify_reader(io::stdin())?;
+ },
+
+ SOP::Encrypt { no_armor, as_,