sop-0.7.0/.cargo_vcs_info.json0000644000000001360000000000100116410ustar { "git": { "sha1": "e1f640354da9d53880d021c50cb738da98ec4301" }, "path_in_vcs": "" }sop-0.7.0/.gitignore000064400000000000000000000000231046102023000124140ustar 00000000000000/target Cargo.lock sop-0.7.0/.gitlab-ci.yml000064400000000000000000000034621046102023000130720ustar 00000000000000stages: - pre-check - test - deploy codespell: tags: - linux stage: pre-check image: 192.168.122.1:5000/sequoia-pgp/build-docker-image/trixie:latest before_script: - codespell --version script: - > codespell --disable-colors -L "crate,ede,iff,mut,nd,te,uint,KeyServer,keyserver,Keyserver,keyservers,Keyservers,keypair,keypairs,KeyPair,fpr,dedup" -S "*.bin,*.gpg,*.pgp,./.git,data,highlight.js,*/target,Makefile" src after_script: [] trixie: tags: - linux stage: test image: 192.168.122.1:5000/sequoia-pgp/build-docker-image/trixie:latest script: - if [ -d target ]; then find target | wc --lines; du -sh target; fi - if [ -d cargo ]; then find cargo | wc --lines; du -sh cargo; fi - rustc --version - cargo --version - cargo test --all-features - du -sh target - du -sh cargo windows-msvc: tags: - win - win2019 stage: test image: 192.168.122.1:5000/sequoia-pgp/build-docker-image/windows-msvc before_script: - rustup override set stable - rustc --version --verbose - cargo --version script: - cargo test --all-features after_script: [] pages: tags: - linux stage: deploy image: 192.168.122.1:5000/sequoia-pgp/build-docker-image/trixie:latest script: - if [ -d target ]; then find target | wc --lines; du -sh target; fi - if [ -d cargo ]; then find cargo | wc --lines; du -sh cargo; fi - rustc --version - cargo --version - cargo doc --no-deps --features=cli - mv target/doc public - echo "/sop-rs/ /sop-rs/sop/index.html 302" > public/_redirects artifacts: paths: - public only: - main cache: paths: - Cargo.lock - target/ - cargo/ variables: CARGO_HOME: $CI_PROJECT_DIR/cargo CARGO_FLAGS: --color always CARGO_INCREMENTAL: 0 sop-0.7.0/Cargo.toml0000644000000032720000000000100076430ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "sop" version = "0.7.0" authors = ["Justus Winter "] description = "Rust Interface for the Stateless OpenPGP Interface" homepage = "https://sequoia-pgp.org/" documentation = "https://docs.rs/sop" readme = "README.md" keywords = [ "cryptography", "openpgp", "pgp", "encryption", "signing", ] categories = [ "cryptography", "authentication", "email", "api-bindings", "command-line-utilities", ] license = "MIT" repository = "https://gitlab.com/sequoia-pgp/sop-rs" [package.metadata.docs.rs] all-features = true [dependencies.anyhow] version = "1" optional = true [dependencies.chrono] version = "0.4.10" optional = true [dependencies.clap] version = "4" features = [ "std", "derive", "help", "wrap_help", ] optional = true default-features = false [dependencies.clap_complete] version = "4" optional = true default-features = false [dependencies.memsec] version = ">=0.5" default-features = false [dependencies.thiserror] version = "1" [features] cli = [ "anyhow", "chrono", "clap", "clap_complete", ] default = [] [badges.gitlab] repository = "sequoia-pgp/sequoia" [badges.maintenance] status = "actively-developed" sop-0.7.0/Cargo.toml.orig000064400000000000000000000022231046102023000133170ustar 00000000000000[package] name = "sop" description = "Rust Interface for the Stateless OpenPGP Interface" version = "0.7.0" authors = ["Justus Winter "] documentation = "https://docs.rs/sop" homepage = "https://sequoia-pgp.org/" repository = "https://gitlab.com/sequoia-pgp/sop-rs" readme = "README.md" keywords = ["cryptography", "openpgp", "pgp", "encryption", "signing"] categories = ["cryptography", "authentication", "email", "api-bindings", "command-line-utilities"] license = "MIT" edition = "2021" [badges] gitlab = { repository = "sequoia-pgp/sequoia" } maintenance = { status = "actively-developed" } [dependencies] memsec = { version = ">=0.5", default-features = false } thiserror = "1" # These dependencies are for the CLI frontend. anyhow = { version = "1", optional = true } chrono = { version = "0.4.10", optional = true } clap = { version = "4", optional = true, default-features = false, features = ["std", "derive", "help", "wrap_help"] } clap_complete = { version = "4", optional = true, default-features = false } [features] default = [] cli = ["anyhow", "chrono", "clap", "clap_complete"] [package.metadata.docs.rs] all-features = true sop-0.7.0/LICENSE.txt000064400000000000000000000017771046102023000122700ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sop-0.7.0/README.md000064400000000000000000000060031046102023000117070ustar 00000000000000Rust Interface for the Stateless OpenPGP Interface ================================================== A set of types and traits formalizing the [Stateless OpenPGP Protocol]. Currently, SOP is only defined as a command line interface, but we are working on a [C Interface]. This interface is the Rust equivalent of the yet to be defined C API. To use this as a consumer, you will need a concrete implementation of the interface, such as [sequoia-sop]. [Stateless OpenPGP Protocol]: https://gitlab.com/dkg/openpgp-stateless-cli [C Interface]: https://gitlab.com/dkg/openpgp-stateless-cli/-/issues/32 [sequoia-sop]: https://docs.rs/sequoia-sop # Example use Given a reference to a [`SOP`] implementation, which is the main entry point for every SOP operation, generate keys, extract certs, sign, verify, encrypt, and decrypt: ```rust let alice_sec = sop.generate_key()? .userid("Alice Lovelace ") .generate()?; let alice_pgp = sop.extract_cert()? .keys(&alice_sec)?; let bob_sec = sop.generate_key()? .userid("Bob Babbage ") .generate()?; let bob_pgp = sop.extract_cert()? .keys(&bob_sec)?; let statement = b"Hello World :)"; let mut data = Cursor::new(&statement); let (_micalg, signature) = sop.sign()? .mode(ops::SignAs::Text) .keys(&alice_sec)? .data(&mut data)?; let verifications = sop.verify()? .certs(&alice_pgp)? .signatures(&signature)? .data(&mut Cursor::new(&statement))?; assert_eq!(verifications.len(), 1); let mut statement_cur = Cursor::new(&statement); let (_session_key, ciphertext) = sop.encrypt()? .sign_with_keys(&alice_sec)? .with_certs(&bob_pgp)? .plaintext(&mut statement_cur)? .to_vec()?; let mut ciphertext_cur = Cursor::new(&ciphertext); let (_, plaintext) = sop.decrypt()? .with_keys(&bob_sec)? .ciphertext(&mut ciphertext_cur)? .to_vec()?; assert_eq!(&plaintext, statement); ``` The above snippet is the equivalent of the following SOP command line example from the SOP spec: ```sh $ sop generate-key "Alice Lovelace " > alice.sec $ sop extract-cert < alice.sec > alice.pgp $ sop sign --as=text alice.sec < statement.txt > statement.txt.asc $ sop verify announcement.txt.asc alice.pgp < announcement.txt $ sop encrypt --sign-with=alice.sec bob.pgp < msg.eml > encrypted.asc $ sop decrypt alice.sec < ciphertext.asc > cleartext.out ``` # Notes for SOP implementers This section is for those who implement the interface using some OpenPGP implementation. Command Line Interface ---------------------- This crate contains an implementation of the Stateless OpenPGP Command Line Interface in terms of the Rust types and traits. Once you implemented the traits, you get the command line interface basically for free by adding this snippet to your `Cargo.toml`: ```toml [[bin]] path = "src/main.rs" required-features = ["cli"] [features] cli = ["sop/cli"] ``` And creating `src/main.rs` along the lines of: ```rust fn main() { sop::cli::main(&MySOPImplementation::default()); } ``` sop-0.7.0/src/cli.rs000064400000000000000000001126211046102023000123400ustar 00000000000000//! Command-line frontend for SOP. //! //! This is an implementation of the SOP Command-Line protocol in //! terms of the trait [`crate::SOP`], hence it can be shared between //! SOP implementations. //! //! To use, add this snippet to `Cargo.toml`: //! //! ```toml //! [[bin]] //! path = "src/main.rs" //! required-features = ["cli"] //! //! [features] //! cli = ["sop/cli"] //! ``` //! //! And create `src/main.rs` along the lines of: //! //! ```rust,ignore //! fn main() { //! sop::cli::main(&MySOPImplementation::default()); //! } //! ``` //! //! ## Generating shell completions //! //! To create shell completions, add this snippet to `Cargo.toml`: //! //! ```toml //! [build-dependencies] //! sop = "..." //! ``` //! //! And create `build.rs` along the lines of: //! //! ```rust,no_run //! #[cfg(feature = "cli")] //! fn main() { //! let outdir = std::env::var_os("CARGO_TARGET_DIR") //! .or(std::env::var_os("OUT_DIR")) //! .expect("cargo to set OUT_DIR"); //! sop::cli::write_shell_completions("sqop", outdir).unwrap(); //! } //! //! #[cfg(not(feature = "cli"))] //! fn main() {} //! ``` //! //! # Features and limitations //! //! - The special designator `@FD:` is only available on UNIX-like systems. //! //! - On Windows, certs and keys provided via the `@ENV:` special //! designator must be ASCII armored and well-formed UTF-8. use std::{ io::{self, Write}, path::Path, }; use anyhow::{Context, Result}; use chrono::{DateTime, NaiveTime, offset::Utc}; use clap::{CommandFactory, FromArgMatches, Parser}; use crate::{ ArmorLabel, EncryptAs, Save, Load, InlineSignAs, Password, SignAs, SOP, }; #[derive(Debug, Parser)] #[clap(about = "An implementation of the Stateless OpenPGP Command Line Interface")] #[clap(disable_version_flag(true))] enum Operation { /// Prints version information. /// /// Invoked without arguments, returns name and version of the SOP /// implementation. #[clap(display_order = 100)] Version { /// Returns name and version of the primary underlying OpenPGP /// toolkit. #[clap(long, conflicts_with("extended"), conflicts_with("sop_spec"))] backend: bool, /// Returns multiple lines of name and version information. /// /// The first line is the name and version of the SOP /// implementation, but the rest have no defined structure. #[clap(long, conflicts_with("backend"), conflicts_with("sop_spec"))] extended: bool, /// Returns the latest version of the SOP spec that is /// implemented. #[clap(long, conflicts_with("backend"), conflicts_with("extended"))] sop_spec: bool, }, /// Emits a list of profiles supported by the identified /// subcommand. #[clap(display_order = 150)] ListProfiles { subcommand: String, }, /// Generates a Secret Key. #[clap(display_order = 200)] GenerateKey { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, /// Select the profile to use for key generation. #[clap(long)] profile: Option, /// Create a signing-only key. #[clap(long)] signing_only: bool, /// Protect the newly generated key with the given password. #[clap(long)] with_key_password: Option, /// UserIDs for the generated key. userids: Vec, }, /// Updates a key's password. /// /// The output will be the same set of OpenPGP Transferable Secret /// Keys as the input, but with all secret key material locked /// according to the password indicated by the /// `--new-key-password` option (or, with no password at all, if /// `--new-key-password` is absent). Note that /// `--old-key-password` can be supplied multiple times, and each /// supplied password will be tried as a means to unlock any /// locked key material encountered. It will normalize a /// Transferable Secret Key to use a single password even if it /// originally had distinct passwords locking each of the subkeys. /// /// If any secret key packet is locked but cannot be unlocked with /// any of the supplied `--old-key-password` arguments, this /// subcommand should fail with `KEY_IS_PROTECTED`. #[clap(display_order = 210)] ChangeKeyPassword { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, /// The new password to lock the key with, or just unlock the /// key if the option is absent. #[clap(long)] new_key_password: Option, /// Unlock the keys with these passwords. #[clap(long, number_of_values = 1)] old_key_password: Vec, }, /// Creates a Revocation Certificate. /// /// Generate a revocation certificate for each Transferable Secret /// Key found. #[clap(display_order = 220)] RevokeKey { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, /// Unlock the keys with these passwords. #[clap(long, number_of_values = 1)] with_key_password: Vec, }, /// Extracts a Certificate from a Secret Key. #[clap(display_order = 300)] ExtractCert { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, }, /// Creates Detached Signatures. #[clap(display_order = 400)] Sign { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, /// Sign binary data or UTF-8 text. #[clap(default_value = "binary", long = "as")] as_: SignAs, /// Emit the digest algorithm used to the specified file. #[clap(long)] micalg_out: Option, /// Try to decrypt the signing KEYS with these passwords. #[clap(long, number_of_values = 1)] with_key_password: Vec, /// Keys for signing. keys: Vec, }, /// Verifies Detached Signatures. #[clap(display_order = 500)] Verify { /// Consider signatures before this date invalid. #[clap(long)] not_before: Option, /// Consider signatures after this date invalid. #[clap(long)] not_after: Option, /// Signatures to verify. signatures: String, /// Certs for verification. certs: Vec, }, /// Encrypts a Message. #[clap(display_order = 600)] Encrypt { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, /// Select the profile to use for encryption. #[clap(long)] profile: Option, /// Encrypt binary data or UTF-8 text. #[clap(default_value = "binary", long = "as")] as_: EncryptAs, /// Encrypt with passwords. #[clap(long, number_of_values = 1)] with_password: Vec, /// Keys for signing. #[clap(long, number_of_values = 1)] sign_with: Vec, /// Try to decrypt the signing KEYS with these passwords. #[clap(long, number_of_values = 1)] with_key_password: Vec, /// Write the session key here. #[clap(long)] session_key_out: Option, /// Encrypt for these certs. certs: Vec, }, /// Decrypts a Message. #[clap(display_order = 700)] Decrypt { /// Write the session key here. #[clap(long)] session_key_out: Option, /// Try to decrypt with this session key. #[clap(long, number_of_values = 1)] with_session_key: Vec, /// Try to decrypt with this password. #[clap(long, number_of_values = 1)] with_password: Vec, /// Write verification result here. #[clap(long, alias("verify-out"))] verifications_out: Option, /// Certs for verification. #[clap(long, number_of_values = 1)] verify_with: Vec, /// Consider signatures before this date invalid. #[clap(long)] verify_not_before: Option, /// Consider signatures after this date invalid. #[clap(long)] verify_not_after: Option, /// Try to decrypt the encryption KEYS with these passwords. #[clap(long, number_of_values = 1)] with_key_password: Vec, /// Try to decrypt with these keys. keys: Vec, }, /// Converts binary OpenPGP data to ASCII. #[clap(display_order = 800)] Armor { /// Indicates the kind of data. #[clap(long, default_value = "auto", hide(true))] label: ArmorLabel, }, /// Converts ASCII OpenPGP data to binary. #[clap(display_order = 900)] Dearmor { }, /// Splits Signatures from an Inline-Signed Message. #[clap(display_order = 750)] InlineDetach { /// Don't ASCII-armor the signatures. #[clap(long)] no_armor: bool, /// Write Signatures here. #[clap(long)] signatures_out: String, }, /// Verifies Inline-Signed Messages. #[clap(display_order = 751)] InlineVerify { /// Consider signatures before this date invalid. #[clap(long)] not_before: Option, /// Consider signatures after this date invalid. #[clap(long)] not_after: Option, /// Write verification result here. #[clap(long)] verifications_out: Option, /// Certs for verification. certs: Vec, }, /// Creates Inline-Signed Messages. #[clap(display_order = 752)] InlineSign { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, /// Sign binary data, UTF-8 text, or using the Cleartext /// Signature Framework. #[clap(default_value = "binary", long = "as")] as_: InlineSignAs, /// Try to decrypt the signing KEYS with these passwords. #[clap(long, number_of_values = 1)] with_key_password: Vec, /// Keys for signing. keys: Vec, }, /// Unsupported subcommand. #[clap(external_subcommand)] Unsupported(Vec), } /// Generates shell completions. pub fn write_shell_completions(binary_name: B, out_path: P) -> io::Result<()> where B: AsRef, P: AsRef, { use clap_complete::{generate_to, Shell}; let binary_name = binary_name.as_ref(); let out_path = out_path.as_ref(); std::fs::create_dir_all(&out_path)?; let mut clap = Operation::command(); for shell in &[Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell, Shell::Elvish] { let path = generate_to(*shell, &mut clap, binary_name, out_path)?; println!("cargo:warning=completion file is generated: {:?}", path); } Ok(()) } /// Implements the SOP command-line interface. pub fn main<'s, S, C, K, Sigs>(sop: &'s S) -> ! where S: SOP<'s, Certs = C, Keys = K, Sigs = Sigs>, C: Load<'s, S> + Save, K: Load<'s, S> + Save, Sigs: Load<'s, S> + Save, { use std::process::exit; match real_main::(sop) { Ok(()) => exit(0), Err(e) => { print_error_chain(&e); let e = match e.downcast::() { Ok(e) => exit(Error::from(e).into()), Err(e) => e, }; let e = match e.downcast::() { Ok(e) => exit(e.into()), Err(e) => e, }; // XXX: Would be nice to at least inform developers that // we return a generic error here. let _ = e; exit(1); }, } } fn real_main<'s, S, C, K, Sigs>(sop: &'s S) -> Result<()> where S: SOP<'s, Certs = C, Keys = K, Sigs = Sigs>, C: Load<'s, S> + Save, K: Load<'s, S> + Save, Sigs: Load<'s, S> + Save, { // Do a little dance to supply name and version to clap. let v = sop.version()?.frontend()?; let name = format!("SOP {}", v.name); let version = v.version.clone(); let about = format!("An implementation of the Stateless OpenPGP Command \ Line Interface using {} {}", v.name, v.version); let app = Operation::command() .name(Box::leak(name.into_boxed_str()) as &str) .about(Box::leak(about.into_boxed_str()) as &str) .version(Box::leak(version.into_boxed_str()) as &str); let matches = match app.clone().try_get_matches() { Ok(v) => v, Err(e) => { use clap::error::ErrorKind; return match e.kind() { ErrorKind::DisplayHelp => { e.exit() }, ErrorKind::UnknownArgument => Err(anyhow::Error::from(Error::UnsupportedOption) .context(format!("{}", e))), _ => Err(e.into()), }; }, }; match Operation::from_arg_matches(&matches)? { Operation::Version { backend, extended, sop_spec } => { let version = sop.version()?; match (backend, extended, sop_spec) { (false, false, false) => { let v = version.frontend()?; println!("{} {}", v.name, v.version); }, (true, false, false) => { let v = version.backend()?; println!("{} {}", v.name, v.version); }, (false, true, false) => { let v = version.frontend()?; println!("{} {}", v.name, v.version); println!("sop-rs {}", env!("CARGO_PKG_VERSION")); println!("{}", version.extended()?); }, (false, false, true) => { println!("{}", sop.spec_version()); }, _ => unreachable!("flags are mutually exclusive"), } }, Operation::ListProfiles { subcommand, } => { let profiles = match subcommand.as_str() { "generate-key" => sop.generate_key()?.list_profiles(), "encrypt" => sop.encrypt()?.list_profiles(), _ => return Err(Error::UnsupportedProfile.into()), }; for (p, d) in profiles { println!("{}: {}", p, d); } }, Operation::GenerateKey { no_armor, profile, signing_only, with_key_password, userids, } => { let mut op = sop.generate_key()?; if let Some(p) = profile { op = op.profile(&p)?; } if signing_only { op = op.signing_only(); } if let Some(p) = with_key_password { op = op.with_key_password(get_password(p)?)?; } for u in userids { op = op.userid(&u); } op.generate()? .to_writer(! no_armor, &mut io::stdout())?; }, Operation::ChangeKeyPassword { no_armor, new_key_password, old_key_password, } => { let mut op = sop.change_key_password()?; if let Some(p) = new_key_password { op = op.new_key_password(get_password(p)?)?; } for p in old_key_password { op = op.old_key_password(get_password(p)?)?; } op.keys(&K::from_reader(sop, &mut io::stdin())?)? .to_writer(! no_armor, &mut io::stdout())?; } Operation::RevokeKey { no_armor, with_key_password, } => { let mut op = sop.revoke_key()?; for p in with_key_password { op = op.with_key_password(get_password(p)?)?; } op.keys(&K::from_reader(sop, &mut io::stdin())?)? .to_writer(! no_armor, &mut io::stdout())?; } Operation::ExtractCert { no_armor, } => { sop.extract_cert()? .keys(&K::from_reader(sop, &mut io::stdin())?)? .to_writer(! no_armor, &mut io::stdout())?; }, Operation::Sign { no_armor, as_, micalg_out, with_key_password, keys, } => { let mut op = sop.sign()?.mode(as_); for p in with_key_password { op = op.with_key_password(get_password_unchecked(p)?)?; } for mut stream in load_files(keys)? { op = op.keys(&K::from_reader(sop, &mut stream)?)?; } let (micalg, sigs) = op.data(&mut io::stdin())?; if let Some(path) = micalg_out { let mut sink = create_file(path)?; write!(sink, "{}", micalg)?; } sigs.to_writer(! no_armor, &mut io::stdout())?; }, Operation::Verify { not_before, not_after, signatures, certs, } => { let mut op = sop.verify()?; if let Some(t) = not_before { op = op.not_before(t.into()); } if let Some(t) = not_after { op = op.not_after(t.into()); } for mut stream in load_files(certs)? { op = op.certs(&C::from_reader(sop, &mut stream)?)?; } let verifications = op.signatures(&Sigs::from_reader(sop, &mut load_file(signatures)?)?)? .data(&mut io::stdin())?; for v in verifications { println!("{}", v); } }, Operation::Encrypt { no_armor, profile, as_, with_password, with_key_password, sign_with, session_key_out, certs, } => { let session_key_out: Option> = if let Some(f) = session_key_out { Some(Box::new(create_file(f)?)) } else { None }; let mut op = sop.encrypt()?.mode(as_); if no_armor { op = op.no_armor(); } if let Some(p) = profile { op = op.profile(&p)?; } for p in with_key_password { op = op.with_key_password(get_password_unchecked(p)?)?; } for mut stream in load_files(sign_with)? { op = op.sign_with_keys(&K::from_reader(sop, &mut stream)?)?; } for pw in with_password.into_iter().map(get_password) { op = op.with_password(pw?)?; } for mut stream in load_files(certs)? { op = op.with_certs(&C::from_reader(sop, &mut stream)?)?; } let session_key = op.plaintext(&mut io::stdin())?.to_writer(&mut io::stdout())?; if let Some(mut sko) = session_key_out { if let Some(sk) = session_key { writeln!(sko, "{}", sk)?; } else { return Err(Error::UnsupportedSubcommand.into()); } } }, Operation::Decrypt { session_key_out, with_session_key, with_password, verifications_out, verify_with, verify_not_before, verify_not_after, with_key_password, keys, } => { let session_key_out: Option> = if let Some(f) = session_key_out { Some(Box::new(create_file(f)?)) } else { None }; if verifications_out.is_none() != verify_with.is_empty() { return Err(anyhow::Error::from(Error::IncompleteVerification)) .context("--verifications-out and --verify-with \ must both be given"); } let mut verify_out: Box = if let Some(f) = verifications_out { Box::new(create_file(f)?) } else { Box::new(io::sink()) }; let mut op = sop.decrypt()?; for mut stream in load_files(with_session_key)? { let mut sk = String::new(); stream.read_to_string(&mut sk)?; op = op.with_session_key(sk.parse()?)?; } for pw in with_password.into_iter().map(get_password_unchecked) { op = op.with_password(pw?)?; } for p in with_key_password { op = op.with_key_password(get_password_unchecked(p)?)?; } for mut stream in load_files(keys)? { op = op.with_keys(&K::from_reader(sop, &mut stream)?)?; } for mut stream in load_files(verify_with)? { op = op.verify_with_certs(&C::from_reader(sop, &mut stream)?)?; } if let Some(t) = verify_not_before { op = op.verify_not_before(t.into()); } if let Some(t) = verify_not_after { op = op.verify_not_after(t.into()); } let (session_key, verifications) = op.ciphertext(&mut io::stdin())?.to_writer(&mut io::stdout())?; for v in verifications { writeln!(verify_out, "{}", v)?; } if let Some(mut sko) = session_key_out { if let Some(sk) = session_key { writeln!(sko, "{}", sk)?; } else { return Err(Error::UnsupportedSubcommand.into()); } } }, Operation::Armor { label, } => { #[allow(deprecated)] let op = sop.armor()?.label(label); op.data(&mut io::stdin())? .to_writer(&mut io::stdout())?; }, Operation::Dearmor {} => { let op = sop.dearmor()?; op.data(&mut io::stdin())? .to_writer(&mut io::stdout())?; }, Operation::InlineDetach { no_armor, signatures_out, } => { let op = sop.inline_detach()?; let mut signatures_out = create_file(signatures_out)?; let signatures = op.message(&mut io::stdin())?.to_writer(&mut io::stdout())?; signatures.to_writer(! no_armor, &mut signatures_out)?; }, Operation::InlineVerify { not_before, not_after, verifications_out, certs, } => { let mut op = sop.inline_verify()?; if let Some(t) = not_before { op = op.not_before(t.into()); } if let Some(t) = not_after { op = op.not_after(t.into()); } let mut verifications_out: Box = if let Some(f) = verifications_out { Box::new(create_file(f)?) } else { Box::new(io::sink()) }; for mut stream in load_files(certs)? { op = op.certs(&C::from_reader(sop, &mut stream)?)?; } let verifications = op.message(&mut io::stdin())?.to_writer(&mut io::stdout())?; for v in verifications { writeln!(&mut verifications_out, "{}", v)?; } }, Operation::InlineSign { no_armor, as_, with_key_password, keys, } => { let mut op = sop.inline_sign()?.mode(as_); if no_armor { op = op.no_armor(); } for p in with_key_password { op = op.with_key_password(get_password_unchecked(p)?)?; } for mut stream in load_files(keys)? { op = op.keys(&K::from_reader(sop, &mut stream)?)?; } op.data(&mut io::stdin())?.to_writer(&mut io::stdout())?; }, Operation::Unsupported(args) => { return Err(anyhow::Error::from(Error::UnsupportedSubcommand)) .context(format!("Subcommand {} is not supported", args[0])); }, } Ok(()) } fn is_special_designator>(file: S) -> bool { file.as_ref().starts_with("@") } /// Loads the given (special) file. fn load_file>(file: S) -> Result> { 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)); } #[cfg(unix)] { if f.starts_with("@FD:") && f[4..].chars().all(|c| c.is_ascii_digit()) { use std::os::unix::io::{RawFd, FromRawFd}; let fd: RawFd = f[4..].parse() .map_err(|_| Error::UnsupportedSpecialPrefix)?; let f = unsafe { std::fs::File::from_raw_fd(fd) }; return Ok(Box::new(f)); } if f.starts_with("@ENV:") { use std::os::unix::ffi::OsStringExt; let key = &f[5..]; let value = std::env::var_os(key) .ok_or(Error::UnsupportedSpecialPrefix)?; // Prevent leak to child processes. std::env::remove_var(key); return Ok(Box::new(io::Cursor::new(value.into_vec()))); } } #[cfg(windows)] { if f.starts_with("@ENV:") { let key = &f[5..]; let value = std::env::var(key) .map_err(|_| Error::UnsupportedSpecialPrefix)?; // Prevent leak to child processes. std::env::remove_var(key); return Ok(Box::new(io::Cursor::new(value.into_bytes()))); } } return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix)); } std::fs::File::open(f) .map(|f| -> Box { Box::new(f) }) .map_err(|_| Error::MissingInput) .context(format!("Failed to open file {:?}", f)) } /// Gets the password from the given location. /// /// Use this for passwords given to create artifacts. /// /// Passwords are indirect data types, meaning that they are /// loaded from files or special designators. fn get_password>(location: S) -> Result { let mut stream = load_file(location)?; let mut pw = Vec::new(); stream.read_to_end(&mut pw)?; Ok(Password::new(pw)?) } /// Gets the password from the given location. /// /// Use this for passwords given to consume artifacts. /// /// Passwords are indirect data types, meaning that they are /// loaded from files or special designators. fn get_password_unchecked>(location: S) -> Result { let mut stream = load_file(location)?; let mut pw = Vec::new(); stream.read_to_end(&mut pw)?; Ok(Password::new_unchecked(pw)) } /// Creates the given (special) file. fn create_file>(file: S) -> Result { 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)); } #[cfg(unix)] { if f.starts_with("@FD:") && f[4..].chars().all(|c| c.is_ascii_digit()) { use std::os::unix::io::{RawFd, FromRawFd}; let fd: RawFd = f[4..].parse() .map_err(|_| Error::UnsupportedSpecialPrefix)?; let f = unsafe { std::fs::File::from_raw_fd(fd) }; return Ok(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 the (special) files. fn load_files(files: Vec) -> Result>> { files.iter().map(load_file).collect() } /// Parses the given string depicting a ISO 8601 timestamp, rounding down. #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct NotBefore(DateTime); impl std::str::FromStr for NotBefore { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s { // XXX: parse "-" to None once we figure out how to do that // with clap. "now" => Ok(Utc::now()), _ => parse_iso8601(s, NaiveTime::from_hms_opt(0, 0, 0).unwrap()), }.map(|t| NotBefore(t)) } } impl From for std::time::SystemTime { fn from(t: NotBefore) -> std::time::SystemTime { t.0.into() } } /// Parses the given string depicting a ISO 8601 timestamp, rounding up. #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct NotAfter(DateTime); impl std::str::FromStr for NotAfter { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s { // XXX: parse "-" to None once we figure out how to do that // with clap. "now" => Ok(Utc::now()), _ => parse_iso8601(s, NaiveTime::from_hms_opt(23, 59, 59).unwrap()), }.map(|t| NotAfter(t)) } } impl From for std::time::SystemTime { fn from(t: NotAfter) -> std::time::SystemTime { t.0.into() } } /// Parses the given string depicting a ISO 8601 timestamp. fn parse_iso8601(s: &str, pad_date_with: chrono::NaiveTime) -> Result> { // 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_opt(0, 0, 0).unwrap(); 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 } /// Errors defined by the Stateless OpenPGP Command-Line Protocol. #[derive(thiserror::Error, Debug)] 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, /// Key not signature-capable (e.g., expired, revoked, /// unacceptable usage flags) (`sop sign` and `sop encrypt` with /// `--sign-with`). #[error("Key not signing-capable")] KeyCannotSign, /// 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, /// Options were supplied that are incompatible with each other. #[error("Options were supplied that are incompatible with each other")] IncompatibleOptions, /// The requested profile is unsupported (sop generate-key, sop /// encrypt), or the indicated subcommand does not accept profiles /// (sop list-profiles). #[error("Profile not supported")] UnsupportedProfile, /// An I/O operation failed. #[error("I/O operation failed")] IoError(#[source] io::Error), } impl From for Error { fn from(e: crate::Error) -> Self { use crate::Error as E; use Error as CE; match e { E::NoSignature => CE::NoSignature, E::UnsupportedAsymmetricAlgo => CE::UnsupportedAsymmetricAlgo, E::CertCannotEncrypt => CE::CertCannotEncrypt, E::KeyCannotSign => CE::KeyCannotSign, E::MissingArg => CE::MissingArg, E::IncompleteVerification => CE::IncompleteVerification, E::CannotDecrypt => CE::CannotDecrypt, E::PasswordNotHumanReadable => CE::PasswordNotHumanReadable, E::UnsupportedOption => CE::UnsupportedOption, E::BadData => CE::BadData, E::ExpectedText => CE::ExpectedText, E::OutputExists => CE::OutputExists, E::MissingInput => CE::MissingInput, E::KeyIsProtected => CE::KeyIsProtected, E::AmbiguousInput => CE::AmbiguousInput, E::IncompatibleOptions => CE::IncompatibleOptions, E::NotImplemented => CE::UnsupportedSubcommand, E::UnsupportedProfile => CE::UnsupportedProfile, E::IoError(e) => CE::IoError(e), } } } impl From 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, KeyCannotSign => 79, IncompatibleOptions => 83, UnsupportedProfile => 89, IoError(_) => 1, } } } /// Prints the error and causes, if any. fn print_error_chain(err: &anyhow::Error) { eprintln!(" {}", err); err.chain().skip(1).for_each(|cause| eprintln!(" because: {}", cause)); } sop-0.7.0/src/errors.rs000064400000000000000000000064471046102023000131150ustar 00000000000000//! Errors for this crate. /// SOP errors. /// /// These are the errors [defined] by the Stateless OpenPGP Protocol. /// /// [defined]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-failure-modes #[derive(thiserror::Error, Debug)] 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, /// Key not signature-capable (e.g., expired, revoked, /// unacceptable usage flags) (`sop sign` and `sop encrypt` with /// `--sign-with`). #[error("Key not signing-capable")] KeyCannotSign, /// 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 with any of the --with-key-password options. #[error("A KEY input is protected with a password and unlocking failed")] KeyIsProtected, /// 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, /// Options were supplied that are incompatible with each other. #[error("Options were supplied that are incompatible with each other")] IncompatibleOptions, /// Operation not implemented. #[error("Operation not implemented")] NotImplemented, /// The requested profile is unsupported (sop generate-key, sop /// encrypt), or the indicated subcommand does not accept profiles /// (sop list-profiles). #[error("Profile not supported")] UnsupportedProfile, /// An IO error occurred. #[error("IO error")] IoError(#[from] std::io::Error), } /// Errors during parsing of SOP string representations. /// /// For types with a defined string representation, we implement /// [`std::str::FromStr`] for parsing. This type is used to report /// errors during parsing. #[derive(thiserror::Error, Debug)] #[error("Invalid argument: {}", _0)] pub struct ParseError(pub String); sop-0.7.0/src/lib.rs000064400000000000000000000613341046102023000123430ustar 00000000000000//! A Rust implementation of the Stateless OpenPGP Interface. //! //! This crate defines an interface that is the Rust equivalent of the //! [draft 08] of the Stateless OpenPGP Command Line Interface. Note //! that you need an concrete implementation of this interface (such //! as [`sequoia-sop`]) in order to use it. //! //! [draft 08]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html //! [`sequoia-sop`]: https://docs.rs/sequoia-sop //! //! # Examples //! //! Given a reference to a [`SOP`] implementation, which is the main //! entry point for every SOP operation, generate keys, extract certs, //! sign, verify, encrypt, and decrypt: //! //! ```rust //! # use sop::*; use std::io::Cursor; //! # fn sop_examples<'s, S: SOP<'s> + 's>(sop: &'s S) -> Result<()> { //! let alice_sec = sop.generate_key()? //! .userid("Alice Lovelace ") //! .generate()?; //! let alice_pgp = sop.extract_cert()? //! .keys(&alice_sec)?; //! //! let bob_sec = sop.generate_key()? //! .userid("Bob Babbage ") //! .generate()?; //! let bob_pgp = sop.extract_cert()? //! .keys(&bob_sec)?; //! //! let statement = b"Hello World :)"; //! let mut data = Cursor::new(&statement); //! let (_micalg, signature) = sop.sign()? //! .mode(ops::SignAs::Text) //! .keys(&alice_sec)? //! .data(&mut data)?; //! //! let verifications = sop.verify()? //! .certs(&alice_pgp)? //! .signatures(&signature)? //! .data(&mut Cursor::new(&statement))?; //! assert_eq!(verifications.len(), 1); //! //! let mut statement_cur = Cursor::new(&statement); //! let (_session_key, ciphertext) = sop.encrypt()? //! .sign_with_keys(&alice_sec)? //! .with_certs(&bob_pgp)? //! .plaintext(&mut statement_cur)? //! .to_vec()?; //! //! let mut ciphertext_cur = Cursor::new(&ciphertext); //! let (_, plaintext) = sop.decrypt()? //! .with_keys(&bob_sec)? //! .ciphertext(&mut ciphertext_cur)? //! .to_vec()?; //! assert_eq!(&plaintext, statement); //! # Ok(()) } //! ``` //! //! The above snippet is the equivalent of the following SOP command //! line example from the SOP spec: //! //! ```sh //! $ sop generate-key "Alice Lovelace " > alice.sec //! $ sop extract-cert < alice.sec > alice.pgp //! //! $ sop sign --as=text alice.sec < statement.txt > statement.txt.asc //! $ sop verify announcement.txt.asc alice.pgp < announcement.txt //! //! $ sop encrypt --sign-with=alice.sec bob.pgp < msg.eml > encrypted.asc //! $ sop decrypt alice.sec < ciphertext.asc > cleartext.out //! ``` use std::{ fmt, io, }; pub mod ops; use ops::*; pub mod errors; use errors::*; pub mod plumbing; #[cfg(feature = "cli")] pub mod cli; /// Loads objects like certs and keys. pub trait Load<'s, S: SOP<'s>> { /// Loads objects like certs and keys from the given reader. fn from_reader(sop: &'s S, source: &mut (dyn io::Read + Send + Sync)) -> Result where Self: Sized; /// Loads objects like certs and keys from the given byte slice. fn from_bytes(sop: &'s S, source: &[u8]) -> Result where Self: Sized, { Self::from_reader(sop, &mut io::Cursor::new(source)) } } /// Saves objects like certs and keys. pub trait Save { /// Saves objects like certs and keys, writing them to the given writer. fn to_writer(&self, armored: bool, sink: &mut (dyn io::Write + Send + Sync)) -> Result<()>; /// Saves objects like certs and keys, writing them to a vector. fn to_vec(&self, armored: bool) -> Result> { let mut sink = vec![]; self.to_writer(armored, &mut sink)?; Ok(sink) } } /// Main entry point to the Stateless OpenPGP Interface. pub trait SOP<'s>: Sized { /// Secret keys. type Keys: Load<'s, Self> + Save + plumbing::SopRef<'s, Self>; /// Public keys. type Certs: Load<'s, Self> + Save + plumbing::SopRef<'s, Self>; /// Signatures. type Sigs: Load<'s, Self> + Save + plumbing::SopRef<'s, Self>; /// Gets SOP version information. /// /// The default implementation returns the version of the spec /// that this framework supports. This should be fine for most /// implementations. However, implementations may chose to /// override this function to return a more nuanced response. fn spec_version(&'s self) -> &'static str { "~draft-dkg-openpgp-stateless-cli-08" } /// Gets version information. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; /// # fn sop_examples<'s, S: SOP<'s> + 's>(sop: &'s S) -> Result<()> { /// // Prints the name of the SOP implementation. /// println!("{}", sop.version()?.frontend()?); /// /// // Prints the name of the underlying OpenPGP implementation. /// println!("{}", sop.version()?.backend()?); /// /// // Prints extended version information. /// println!("{}", sop.version()?.extended()?); /// # Ok(()) } /// ``` fn version(&'s self) -> Result>; /// Generates a Secret Key. /// /// Customize the operation using the builder [`GenerateKey`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; /// # fn sop_examples<'s, S: SOP<'s> + 's>(sop: &'s S) -> Result<()> { /// let alice_sec = sop.generate_key()? /// .userid("Alice Lovelace ") /// .generate()?; /// # Ok(()) } /// ``` fn generate_key(&'s self) -> Result + 's>>; /// Updates a key's password. /// /// Customize the operation using the builder [`ChangeKeyPassword`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S, Certs, Keys>(sop: &'s S) -> Result<()> /// # where /// # S: SOP<'s, Certs = Certs, Keys = Keys>, /// # Certs: Load<'s, S> + Save, /// # Keys: Load<'s, S> + Save, /// # { /// let alice_secret = /// Keys::from_reader(sop, &mut File::open("alice.secret")?)?; /// /// let alice_updated_secret = sop.change_key_password()? /// .old_key_password(Password::new_unchecked(b"hunter2".to_vec()))? /// .new_key_password(Password::new(b"jaeger2".to_vec())?)? /// .keys(&alice_secret)?; /// # Ok(()) } /// ``` fn change_key_password(&'s self) -> Result + 's>>; /// Creates a Revocation Certificate. /// /// Customize the operation using the builder [`RevokeKey`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S, Certs, Keys>(sop: &'s S) -> Result<()> /// # where /// # S: SOP<'s, Certs = Certs, Keys = Keys>, /// # Certs: Load<'s, S> + Save, /// # Keys: Load<'s, S> + Save, /// # { /// let alice_secret = /// Keys::from_reader(sop, &mut File::open("alice.secret")?)?; /// /// let alice_revoked = sop.revoke_key()? /// .with_key_password(Password::new_unchecked(b"hunter2".to_vec()))? /// .keys(&alice_secret)?; /// # Ok(()) } /// ``` fn revoke_key(&'s self) -> Result + 's>>; /// Extracts a Certificate from a Secret Key. /// /// Customize the operation using the builder [`ExtractCert`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S, Certs, Keys>(sop: &'s S) -> Result<()> /// # where /// # S: SOP<'s, Certs = Certs, Keys = Keys>, /// # Certs: Load<'s, S> + Save, /// # Keys: Load<'s, S> + Save, /// # { /// let alice_secret = /// Keys::from_reader(sop, &mut File::open("alice.secret")?)?; /// /// let alice_public = sop.extract_cert()? /// .keys(&alice_secret)?; /// # Ok(()) } /// ``` fn extract_cert(&'s self) -> Result + 's>>; /// Creates Detached Signatures. /// /// Customize the operation using the builder [`Sign`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S, Certs, Keys>(sop: &'s S) -> Result<()> /// # where /// # S: SOP<'s, Certs = Certs, Keys = Keys>, /// # Certs: Load<'s, S> + Save, /// # Keys: Load<'s, S> + Save, /// # { /// let alice_secret = /// Keys::from_reader(sop, &mut File::open("alice.secret")?)?; /// /// let (_micalg, sig) = sop.sign()? /// .keys(&alice_secret)? /// .data(&mut Cursor::new(&b"Hello World :)"))?; /// # Ok(()) } /// ``` fn sign(&'s self) -> Result + 's>>; /// Verifies Detached Signatures. /// /// Customize the operation using the builder [`Verify`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S, Certs, Keys, Sigs>(sop: &'s S) -> Result<()> /// # where /// # S: SOP<'s, Certs = Certs, Keys = Keys, Sigs = Sigs>, /// # Certs: Load<'s, S> + Save, /// # Keys: Load<'s, S> + Save, /// # Sigs: Load<'s, S> + Save, /// # { /// let alice_public = /// Certs::from_reader(sop, &mut File::open("alice.public")?)?; /// let sig = /// Sigs::from_reader(sop, &mut File::open("data.asc")?)?; /// /// let verifications = sop.verify()? /// .certs(&alice_public)? /// .signatures(&sig)? /// .data(&mut Cursor::new(&b"Hello World :)"))?; /// let valid_signatures = ! verifications.is_empty(); /// # Ok(()) } /// ``` fn verify(&'s self) -> Result + 's>>; /// Encrypts a Message. /// /// Customize the operation using the builder [`Encrypt`]. /// /// # Examples /// /// Encrypts a message for Bob, and signs it using Alice's key. /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S, Certs, Keys>(sop: &'s S) -> Result<()> /// # where /// # S: SOP<'s, Certs = Certs, Keys = Keys>, /// # Certs: Load<'s, S> + Save, /// # Keys: Load<'s, S> + Save, /// # { /// let alice_secret = /// Keys::from_reader(sop, &mut File::open("alice.secret")?)?; /// let bob_public = /// Certs::from_reader(sop, &mut File::open("bob.public")?)?; /// /// let (_session_key, ciphertext) = sop.encrypt()? /// .sign_with_keys(&alice_secret)? /// .with_certs(&bob_public)? /// .plaintext(&mut Cursor::new(&b"Hello World :)"))? /// .to_vec()?; /// # Ok(()) } /// ``` fn encrypt(&'s self) -> Result + 's>>; /// Decrypts a Message. /// /// Customize the operation using the builder [`Decrypt`]. /// /// # Examples /// /// Decrypts a message encrypted for Bob, and verifies Alice's /// signature on it. /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S, Certs, Keys>(sop: &'s S) -> Result<()> /// # where /// # S: SOP<'s, Certs = Certs, Keys = Keys>, /// # Certs: Load<'s, S> + Save, /// # Keys: Load<'s, S> + Save, /// # { /// let alice_public = /// Certs::from_reader(sop, &mut File::open("alice.public")?)?; /// let bob_secret = /// Keys::from_reader(sop, &mut File::open("bob.secret")?)?; /// /// let ((_session_key, verifications), plaintext) = sop.decrypt()? /// .verify_with_certs(&alice_public)? /// .with_keys(&bob_secret)? /// .ciphertext(&mut File::open("ciphertext.pgp")?)? /// .to_vec()?; /// let valid_signatures = ! verifications.is_empty(); /// # Ok(()) } /// ``` fn decrypt(&'s self) -> Result + 's>>; /// Converts binary OpenPGP data to ASCII. /// /// By default, SOP operations emit ASCII-Armored data. But, /// occasionally it can be useful to explicitly armor data. /// /// Customize the operation using the builder [`Armor`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S: SOP<'s> + 's>(sop: &'s S) -> Result<()> { /// let (_, alice_secret_asc) = sop.armor()? /// .data(&mut File::open("alice.secret.bin")?)? /// .to_vec()?; /// assert!(alice_secret_asc.starts_with(b"-----BEGIN PGP PRIVATE KEY BLOCK-----")); /// # Ok(()) } /// ``` fn armor(&'s self) -> Result>; /// Converts ASCII OpenPGP data to binary. /// /// By default, SOP operations emit ASCII-Armored data, but this /// behavior can be changed at export time. Nevertheless, /// occasionally it can be useful to explicitly dearmor data. /// /// Customize the operation using the builder [`Dearmor`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S: SOP<'s> + 's>(sop: &'s S) -> Result<()> { /// let (_, alice_secret_bin) = sop.dearmor()? /// .data(&mut File::open("alice.secret.asc")?)? /// .to_vec()?; /// assert!(! alice_secret_bin.starts_with(b"-----BEGIN PGP PRIVATE KEY BLOCK-----")); /// # Ok(()) } /// ``` fn dearmor(&'s self) -> Result>; /// Splits Signatures from an Inline-Signed Message. /// /// Note: The signatures are not verified, this merely transforms /// an inline-signed message into a detached signature, which in /// turn can be verified using [`SOP::verify`]. /// /// Customize the operation using the builder [`InlineDetach`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S: SOP<'s> + 's>(sop: &'s S) -> Result<()> { /// let (signatures, data) = sop.inline_detach()? /// .message(&mut File::open("inline-signed.pgp")?)? /// .to_vec()?; /// # Ok(()) } /// ``` fn inline_detach(&'s self) -> Result + 's>>; /// Verifies an Inline-Signed Message. /// /// Customize the operation using the builder [`InlineVerify`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S, Certs, Keys>(sop: &'s S) -> Result<()> /// # where /// # S: SOP<'s, Certs = Certs, Keys = Keys>, /// # Certs: Load<'s, S> + Save, /// # Keys: Load<'s, S> + Save, /// # { /// let alice_public = /// Certs::from_reader(sop, &mut File::open("alice.public")?)?; /// /// let (verifications, data) = sop.inline_verify()? /// .certs(&alice_public)? /// .message(&mut File::open("inline-signed.pgp")?)? /// .to_vec()?; /// let valid_signatures = ! verifications.is_empty(); /// # Ok(()) } /// ``` fn inline_verify(&'s self) -> Result + 's>>; /// Creates an Inline-Signed Message. /// /// Customize the operation using the builder [`InlineSign`]. /// /// # Examples /// /// ```rust /// # use sop::*; use std::io::Cursor; use std::fs::File; /// # fn sop_examples<'s, S, Certs, Keys>(sop: &'s S) -> Result<()> /// # where /// # S: SOP<'s, Certs = Certs, Keys = Keys>, /// # Certs: Load<'s, S> + Save, /// # Keys: Load<'s, S> + Save, /// # { /// let alice_secret = /// Keys::from_reader(sop, &mut File::open("alice.secret")?)?; /// /// let (inline_signed_asc) = sop.inline_sign()? /// .keys(&alice_secret)? /// .data(&mut Cursor::new(&b"Hello World :)"))? /// .to_vec()?; /// # Ok(()) } /// ``` fn inline_sign(&'s self) -> Result + 's>>; } /// A password. /// /// See [Passwords are Human-Readable] in the SOP spec. /// /// [Passwords are Human-Readable]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-passwords-are-human-readabl pub struct Password(Box<[u8]>); impl Password { /// Returns a `Password` that is guaranteed to be human-readable. /// /// Use this function when you get a password from the user to /// generate an artifact with (see [Generating Material with /// Human-Readable Passwords]). /// /// [Generating Material with Human-Readable Passwords]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-generating-material-with-hu pub fn new(password: Vec) -> Result { // Securely erase the password. fn securely_erase(mut p: Vec) { unsafe { memsec::memzero(p.as_mut_ptr(), p.len()); } } let mut s = String::from_utf8(password) .map_err(|e| { securely_erase(e.into_bytes()); Error::PasswordNotHumanReadable })?; // Check for leading whitespace. if s.trim_start().len() != s.len() { securely_erase(s.into_bytes()); return Err(Error::PasswordNotHumanReadable); } // Trim trailing whitespace. s.truncate(s.trim_end().len()); // Check for odd whitespace. if s.chars().any(|c| c.is_whitespace() && c != ' ') { securely_erase(s.into_bytes()); return Err(Error::PasswordNotHumanReadable); } // XXX: Check that the password is in Unicode Normal Form C, // but I don't think that is possible with Rust's stdlib. Ok(Password(s.into_bytes().into())) } /// Returns a `Password` without further checking. /// /// Use this function when you get a password from the user that /// is used when consuming an artifact (see [Consuming /// Password-protected Material]). /// /// [Consuming Password-protected Material]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-consuming-password-protecte pub fn new_unchecked(password: Vec) -> Password { Password(password.into()) } } impl plumbing::PasswordsAreHumanReadable for Password { fn normalized(&self) -> &[u8] { // First, let's hope it is UTF-8. if let Ok(p) = std::str::from_utf8(&self.0) { p.trim_end().as_bytes() } else { // As a best effort for now, drop ASCII-whitespace from // the end. let mut p = &self.0[..]; while ! p.is_empty() && p[p.len() - 1].is_ascii_whitespace() { p = &p[..p.len() - 1]; } p } } fn variants(&self) -> Box + '_> { Box::new(std::iter::once(self.normalized()) .filter_map(move |normalized| { if normalized.len() < self.0.len() { Some(normalized) } else { None } }) .chain(std::iter::once(&self.0[..]))) } } impl Drop for Password { fn drop(&mut self) { unsafe { memsec::memzero(self.0.as_mut_ptr(), self.0.len()); } } } /// A session key. pub struct SessionKey { algorithm: u8, key: Box<[u8]>, } impl SessionKey { /// Creates a new session key object. pub fn new(algorithm: A, key: K) -> Result where A: Into, K: AsRef<[u8]>, { // XXX: Maybe sanity check key lengths. Ok(SessionKey { algorithm: algorithm.into(), key: key.as_ref().to_vec().into(), }) } /// Returns the symmetric algorithm octet. pub fn algorithm(&self) -> u8 { self.algorithm } /// Returns the session key. pub fn key(&self) -> &[u8] { &self.key } } impl fmt::Display for SessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}:", self.algorithm)?; for b in &self.key[..] { write!(f, "{:02X}", b)? } Ok(()) } } impl Drop for SessionKey { fn drop(&mut self) { unsafe { memsec::memzero(self.key.as_mut_ptr(), self.key.len()); } } } impl std::str::FromStr for SessionKey { type Err = ParseError; fn from_str(sk: &str) -> ParseResult { // The SOP format is: // // ":" // // We most likely will change the first field, so we split // from the end of the string using `rsplit`, which puts the // last segment first. This is rather unexpected. Reverse // it. let fields = sk.rsplit(':').rev().collect::>(); if fields.len() != 2 { return Err(ParseError(format!( "Expected two colon-separated fields, got {:?}", fields))); } let algo: u8 = fields[0].parse().map_err( |e| ParseError(format!("Failed to parse algorithm: {}", e)))?; let sk = from_hex(&fields[1], true)?; Self::new(algo, sk).map_err( |e| ParseError(format!("Bad session key: {}", e))) } } /// A helpful function for converting a hexadecimal string to binary. /// This function skips whitespace if `pretty` is set. fn from_hex(hex: &str, pretty: bool) -> ParseResult> { const BAD: u8 = 255u8; const X: u8 = 'x' as u8; let mut nibbles = hex.chars().filter_map(|x| { match x { '0' => Some(0u8), '1' => Some(1u8), '2' => Some(2u8), '3' => Some(3u8), '4' => Some(4u8), '5' => Some(5u8), '6' => Some(6u8), '7' => Some(7u8), '8' => Some(8u8), '9' => Some(9u8), 'a' | 'A' => Some(10u8), 'b' | 'B' => Some(11u8), 'c' | 'C' => Some(12u8), 'd' | 'D' => Some(13u8), 'e' | 'E' => Some(14u8), 'f' | 'F' => Some(15u8), 'x' | 'X' if pretty => Some(X), _ if pretty && x.is_whitespace() => None, _ => Some(BAD), } }).collect::>(); if pretty && nibbles.len() >= 2 && nibbles[0] == 0 && nibbles[1] == X { // Drop '0x' prefix. nibbles.remove(0); nibbles.remove(0); } if nibbles.iter().any(|&b| b == BAD || b == X) { // Not a hex character. return Err(ParseError("Invalid characters".into())); } // We need an even number of nibbles. if nibbles.len() % 2 != 0 { return Err(ParseError("Odd number of nibbles".into())); } let bytes = nibbles.chunks(2).map(|nibbles| { (nibbles[0] << 4) | nibbles[1] }).collect::>(); Ok(bytes) } /// Result specialization. pub type Result = std::result::Result; /// Convenience alias. type ParseResult = std::result::Result; #[cfg(test)] mod tests { use super::*; #[test] fn session_key_roundtrip() -> Result<()> { for algo in &[9, 13] { let sk = SessionKey::new( *algo, &[0xE1, 0x48, 0x97, 0x81, 0xAA, 0x22, 0xE1, 0xBF, 0x6E, 0x3E, 0x61, 0x74, 0x8C, 0x8D, 0x3F, 0x35, 0x50, 0x7C, 0x80, 0x9E, 0x95, 0x64, 0x86, 0x87, 0xC7, 0xE4, 0xB9, 0xAF, 0x86, 0x17, 0xD3, 0xAE])?; let sk_s = sk.to_string(); let sk_p: SessionKey = sk_s.parse().unwrap(); assert_eq!(sk.algorithm(), sk_p.algorithm()); assert_eq!(sk.key(), sk_p.key()); } Ok(()) } #[test] fn sign_as_roundtrip() -> Result<()> { use SignAs::*; for a in &[Text, Binary] { let s = a.to_string(); let b: SignAs = s.parse().unwrap(); assert_eq!(a, &b); } Ok(()) } #[test] fn encrypt_as_roundtrip() -> Result<()> { use EncryptAs::*; for a in &[Text, Binary] { let s = a.to_string(); let b: EncryptAs = s.parse().unwrap(); assert_eq!(a, &b); } Ok(()) } #[test] fn armor_label_roundtrip() -> Result<()> { use ArmorLabel::*; for a in &[Auto, Sig, Key, Cert, Message] { let s = a.to_string(); let b: ArmorLabel = s.parse().unwrap(); assert_eq!(a, &b); } Ok(()) } } sop-0.7.0/src/ops.rs000064400000000000000000000536071046102023000124020ustar 00000000000000//! Builders for the SOP operations. use std::{ fmt, io, time::SystemTime, }; use super::{ Error, ParseError, Load, Password, Result, SOP, Save, SessionKey, }; /// Builder for [`SOP::version`]. pub trait Version<'s> { /// Returns name and version of the SOP implementation. fn frontend(&self) -> Result; /// Returns name and version of the primary underlying OpenPGP /// toolkit. fn backend(&self) -> Result; /// Returns extended version information. /// /// The information has no defined structure and may contain any /// information deemed useful by the implementer. fn extended(&self) -> Result; } /// Represents a name and version tuple. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct VersionInfo { /// Name of the implementation, library, or additional component. pub name: String, /// Version string. pub version: String, } impl fmt::Display for VersionInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} {}", self.name, self.version) } } /// Builder for [`SOP::generate_key`]. pub trait GenerateKey<'s, S: SOP<'s>, Keys: Load<'s, S> + Save> { /// Lists profiles for this subcommand. fn list_profiles(&self) -> Vec<(String, String)> { vec![] } /// Selects a profile for this subcommand. /// /// Valid profiles can be queried using /// [`GenerateKey::list_profiles`]. fn profile(self: Box, _profile: &str) -> Result + 's>> { Err(Error::UnsupportedProfile) } /// Generates signing-only keys. fn signing_only(self: Box) -> Box + 's>; /// Protects the newly generated key with the given password. fn with_key_password(self: Box, password: Password) -> Result + 's>>; /// Adds a User ID. fn userid(self: Box, userid: &str) -> Box + 's>; /// Generates the OpenPGP key. fn generate(self: Box) -> Result; } /// Builder for [`SOP::change_key_password`]. pub trait ChangeKeyPassword<'s, S: SOP<'s>, Keys: Load<'s, S>> { /// Supplies the new password to lock the keys with. /// /// If this method is not invoked, the keys are unlocked. fn new_key_password(self: Box, password: Password) -> Result + 's>>; /// Supplies a password to unlock the keys with. /// /// All passwords are tried. If unlocking a key fails, the whole /// operation fails with [`Error::KeyIsProtected`]. fn old_key_password(self: Box, password: Password) -> Result + 's>>; /// Updates `keys`. fn keys(self: Box, keys: &Keys) -> Result; } /// Builder for [`SOP::revoke_key`]. pub trait RevokeKey<'s, S: SOP<'s>, Certs: Save, Keys: Load<'s, S>> { /// Supplies a password to unlock the keys with. /// /// All passwords are tried. If unlocking a key fails, the whole /// operation fails with [`Error::KeyIsProtected`]. fn with_key_password(self: Box, password: Password) -> Result + 's>>; /// Revokes `keys`. fn keys(self: Box, keys: &Keys) -> Result; } /// Builder for [`SOP::extract_cert`]. pub trait ExtractCert<'s, S: SOP<'s>, Certs: Save, Keys: Load<'s, S>> { /// Extracts the certs from `keys`. fn keys(self: Box, keys: &Keys) -> Result; } /// Builder for [`SOP::sign`]. pub trait Sign<'s, S: SOP<'s>, Keys: Load<'s, S>, Sigs: Save> { /// Sets signature mode. fn mode(self: Box, mode: SignAs) -> Box + 's>; /// Adds the signer keys. fn keys(self: Box, keys: &Keys) -> Result + 's>>; /// Adds a password to unlock the signing keys with. /// /// All supplied passwords will be used to try to unlock all /// signing keys. fn with_key_password(self: Box, password: Password) -> Result + 's>>; /// Signs data. fn data(self: Box, data: &mut (dyn io::Read + Send + Sync)) -> Result<(Micalg, Sigs)>; } /// Builder for [`SOP::verify`]. pub trait Verify<'s, S: SOP<'s>, Certs: Load<'s, S>, Sigs: Load<'s, S>> { /// Makes SOP consider signatures before this date invalid. fn not_before(self: Box, t: SystemTime) -> Box + 's>; /// Makes SOP consider signatures after this date invalid. fn not_after(self: Box, t: SystemTime) -> Box + 's>; /// Adds the verification certs. fn certs(self: Box, certs: &Certs) -> Result + 's>>; /// Provides the signatures. fn signatures<'sigs>(self: Box, signatures: &'sigs Sigs) -> Result> where 's: 'sigs; } /// Finalizes detached signature verification. pub trait VerifySignatures<'sigs> { /// Verifies the authenticity of `data`. fn data(self: Box, data: &mut (dyn io::Read + Send + Sync)) -> Result>; } /// Builder for [`SOP::encrypt`]. pub trait Encrypt<'s, S: SOP<'s>, Certs: Load<'s, S>, Keys: Load<'s, S>> { /// Disables armor encoding. fn no_armor(self: Box) -> Box + 's>; /// Lists profiles for this subcommand. fn list_profiles(&self) -> Vec<(String, String)> { vec![] } /// Selects a profile for this subcommand. /// /// Valid profiles can be queried using /// [`GenerateKey::list_profiles`]. fn profile(self: Box, _profile: &str) -> Result + 's>> { Err(Error::UnsupportedProfile) } /// Sets encryption mode. fn mode(self: Box, mode: EncryptAs) -> Box + 's>; /// Adds the signer keys. fn sign_with_keys(self: Box, keys: &Keys) -> Result + 's>>; /// Adds a password to unlock the signing keys with. /// /// All supplied passwords will be used to try to unlock all /// signing keys. fn with_key_password(self: Box, password: Password) -> Result + 's>>; /// Encrypts with the given password. fn with_password(self: Box, password: Password) -> Result + 's>>; /// Encrypts with the given certs. fn with_certs(self: Box, certs: &Certs) -> Result + 's>>; /// Encrypts the given data yielding the ciphertext. fn plaintext<'d>(self: Box, plaintext: &'d mut (dyn io::Read + Send + Sync)) -> Result> + 'd>> where 's: 'd; } /// Builder for [`SOP::decrypt`]. pub trait Decrypt<'s, S: SOP<'s>, Certs: Load<'s, S>, Keys: Load<'s, S>> { /// Makes SOP consider signatures before this date invalid. fn verify_not_before(self: Box, t: SystemTime) -> Box + 's>; /// Makes SOP consider signatures after this date invalid. fn verify_not_after(self: Box, t: SystemTime) -> Box + 's>; /// Adds the verification certs. fn verify_with_certs(self: Box, certs: &Certs) -> Result + 's>>; /// Tries to decrypt with the given session key. fn with_session_key(self: Box, sk: SessionKey) -> Result + 's>>; /// Tries to decrypt with the given password. fn with_password(self: Box, password: Password) -> Result + 's>>; /// Adds the decryption keys. fn with_keys(self: Box, key: &Keys) -> Result + 's>>; /// Adds a password to unlock the decryption keys with. /// /// All supplied passwords will be used to try to unlock all keys. fn with_key_password(self: Box, password: Password) -> Result + 's>>; /// Decrypts `ciphertext`, returning verification results and /// plaintext. fn ciphertext<'d>(self: Box, ciphertext: &'d mut (dyn io::Read + Send + Sync)) -> Result, Vec)> + 'd>> where 's: 'd; } /// Builder for [`SOP::armor`]. pub trait Armor<'s> { /// Overrides automatic detection of label. #[deprecated] fn label(self: Box, label: ArmorLabel) -> Box + 's>; /// Armors `data`. fn data<'d>(self: Box, data: &'d mut (dyn io::Read + Send + Sync)) -> Result> where 's: 'd; } /// Builder for [`SOP::dearmor`]. pub trait Dearmor<'s> { /// Dearmors `data`. fn data<'d>(self: Box, data: &'d mut (dyn io::Read + Send + Sync)) -> Result> where 's: 'd; } /// Builder for [`SOP::inline_detach`]. pub trait InlineDetach<'s, Sigs: Save> { /// Splits Signatures from the Inline-Signed Message. fn message<'d>(self: Box, data: &'d mut (dyn io::Read + Send + Sync)) -> Result + 'd>> where 's: 'd; } /// Builder for [`SOP::inline_verify`]. pub trait InlineVerify<'s, S: SOP<'s>, Certs: Load<'s, S>> { /// Makes SOP consider signatures before this date invalid. fn not_before(self: Box, t: SystemTime) -> Box + 's>; /// Makes SOP consider signatures after this date invalid. fn not_after(self: Box, t: SystemTime) -> Box + 's>; /// Adds the verification certs. fn certs(self: Box, certs: &Certs) -> Result + 's>>; /// Verifies an Inline-Signed Message. fn message<'d>(self: Box, data: &'d mut (dyn io::Read + Send + Sync)) -> Result> + 'd>> where 's: 'd; } /// Builder for [`SOP::inline_sign`]. pub trait InlineSign<'s, S: SOP<'s>, Keys: Load<'s, S>> { /// Disables armor encoding. fn no_armor(self: Box) -> Box + 's>; /// Sets signature mode. fn mode(self: Box, mode: InlineSignAs) -> Box + 's>; /// Adds the signer keys. fn keys(self: Box, keys: &Keys) -> Result + 's>>; /// Adds a password to unlock the signing keys with. /// /// All supplied passwords will be used to try to unlock all /// signing keys. fn with_key_password(self: Box, password: Password) -> Result + 's>>; /// Signs data. fn data<'d>(self: Box, data: &'d mut (dyn io::Read + Send + Sync)) -> Result> where 's: 'd; } /// An operation that returns a value ready to be executed. /// /// To execute the operation, either supply an [`std::io::Write`]r /// using [`Ready::to_writer`] to write the resulting data to, or use /// [`Ready::to_vec`] to write to a `Vec`. pub trait Ready { /// Executes the operation writing the result to `sink`. fn to_writer(self: Box, sink: &mut (dyn io::Write + Send + Sync)) -> Result; /// Executes the operation writing the result into a `Vec`. fn to_vec(self: Box) -> Result<(T, Vec)> { let mut v = Vec::new(); let r = self.to_writer(&mut v)?; Ok((r, v)) } } /// A successful signature verification. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Verification { creation_time: SystemTime, signing_key_fingerprint: String, signing_cert_fingerprint: String, signature_mode: SignatureMode, message: Option, } #[cfg(feature = "cli")] impl fmt::Display for Verification { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {} {} {}{}", chrono::DateTime::::from(self.creation_time()) .format("%Y-%m-%dT%H:%M:%SZ"), self.signing_key_fingerprint(), self.signing_cert_fingerprint(), self.signature_mode(), if let Some(m) = self.message() { format!(" {}", m) } else { "".into() }) } } impl Verification { /// Creates a `Verification` object. pub fn new<'m, T, K, C, M>(creation_time: T, signing_key_fingerprint: K, signing_cert_fingerprint: C, signature_mode: SignatureMode, message: M) -> Result where T: Into, K: ToString, C: ToString, M: Into>, { fn normalize(s: String) -> Result { // XXX Ok(s) } let signing_key_fingerprint = normalize(signing_key_fingerprint.to_string())?; let signing_cert_fingerprint = normalize(signing_cert_fingerprint.to_string())?; Ok(Verification { creation_time: creation_time.into(), signing_key_fingerprint, signing_cert_fingerprint, signature_mode, message: message.into().map(Into::into), }) } /// Returns the signature's creation time. pub fn creation_time(&self) -> SystemTime { self.creation_time } /// Returns the fingerprint of the signing (sub)key. pub fn signing_key_fingerprint(&self) -> &str { &self.signing_key_fingerprint } /// Returns the fingerprint of the signing certificate. pub fn signing_cert_fingerprint(&self) -> &str { &self.signing_cert_fingerprint } /// Returns the signature mode. pub fn signature_mode(&self) -> SignatureMode { self.signature_mode } /// Returns a free-form message describing the verification. pub fn message(&self) -> Option<&str> { self.message.as_ref().map(AsRef::as_ref) } } /// Indicates the type of signature in a `Verification`. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum SignatureMode { Text, Binary, } #[cfg(feature = "cli")] impl fmt::Display for SignatureMode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { SignatureMode::Text => f.write_str("mode:text"), SignatureMode::Binary => f.write_str("mode:binary"), } } } /// Signature type. /// /// This is used by [`SOP::sign`] to select the signature type. See /// [`sop sign`]. /// /// [`sop sign`]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-sign-create-detached-signat #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum SignAs { Binary, Text, } impl Default for SignAs { fn default() -> Self { SignAs::Binary } } impl From for SignAs { fn from(a: EncryptAs) -> Self { match a { EncryptAs::Binary => SignAs::Binary, EncryptAs::Text => SignAs::Text, } } } impl std::str::FromStr for SignAs { type Err = ParseError; fn from_str(s: &str) -> std::result::Result { match s { "binary" => Ok(SignAs::Binary), "text" => Ok(SignAs::Text), _ => Err(ParseError(format!( "{:?}, 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"), } } } /// Inline Signature type. /// /// This is used by [`SOP::inline_sign`] to select the signature type. /// See [`sop inline-sign`]. /// /// [`sop inline-sign`]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-inline-sign-create-an-inlin #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum InlineSignAs { Binary, Text, ClearSigned, } impl Default for InlineSignAs { fn default() -> Self { InlineSignAs::Binary } } impl std::str::FromStr for InlineSignAs { type Err = ParseError; fn from_str(s: &str) -> std::result::Result { match s { "binary" => Ok(InlineSignAs::Binary), "text" => Ok(InlineSignAs::Text), "clearsigned" => Ok(InlineSignAs::ClearSigned), _ => Err(ParseError(format!( "{:?}, expected one of {{binary|text|clearsigned}}", s))), } } } impl fmt::Display for InlineSignAs { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { InlineSignAs::Binary => f.write_str("binary"), InlineSignAs::Text => f.write_str("text"), InlineSignAs::ClearSigned => f.write_str("clearsigned"), } } } /// Plaintext data format. /// /// This is used by [`SOP::encrypt`] to select the data format. See /// [`sop encrypt`]. /// /// [`sop encrypt`]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-encrypt-encrypt-a-message #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum EncryptAs { Binary, Text, } impl Default for EncryptAs { fn default() -> Self { EncryptAs::Binary } } impl std::str::FromStr for EncryptAs { type Err = ParseError; fn from_str(s: &str) -> std::result::Result { match s { "binary" => Ok(EncryptAs::Binary), "text" => Ok(EncryptAs::Text), _ => Err(ParseError(format!( "{}, expected one of {{binary|text}}", 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"), } } } /// The ASCII Armor Label. /// /// This is used by [`SOP::armor`] to control the framing that is /// emitted. See [`sop armor`]. /// /// [`sop armor`]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-armor-convert-binary-to-asc #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum ArmorLabel { Auto, Sig, Key, Cert, Message, } impl Default for ArmorLabel { fn default() -> Self { ArmorLabel::Auto } } impl std::str::FromStr for ArmorLabel { type Err = ParseError; fn from_str(s: &str) -> std::result::Result { match s { "auto" => Ok(ArmorLabel::Auto), "sig" => Ok(ArmorLabel::Sig), "key" => Ok(ArmorLabel::Key), "cert" => Ok(ArmorLabel::Cert), "message" => Ok(ArmorLabel::Message), _ => Err(ParseError(format!( "{:?}, expected one of \ {{auto|sig|key|cert|message}}", s))), } } } impl fmt::Display for ArmorLabel { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ArmorLabel::Auto => f.write_str("auto"), ArmorLabel::Sig => f.write_str("sig"), ArmorLabel::Key => f.write_str("key"), ArmorLabel::Cert => f.write_str("cert"), ArmorLabel::Message => f.write_str("message"), } } } /// Indicates the cryptographic digest used when making a signature. /// /// It is useful specifically when generating signed PGP/MIME objects, /// which want a `micalg=` parameter for the `multipart/signed` /// content type as described in section 5 of [RFC3156]. /// /// [RFC3156]: https://datatracker.ietf.org/doc/html/rfc3156 #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum Micalg { /// Rivest et.al. message digest 5 (deprecated). MD5, /// NIST Secure Hash Algorithm (deprecated). SHA1, /// RIPEMD-160 (deprecated). RipeMD, /// 256-bit version of SHA2. SHA256, /// 384-bit version of SHA2. SHA384, /// 512-bit version of SHA2. SHA512, /// 224-bit version of SHA2. SHA224, /// Unknown hash algorithm. Unknown(String), } impl From for Micalg { fn from(o: u8) -> Self { match o { 1 => Micalg::MD5, 2 => Micalg::SHA1, 3 => Micalg::RipeMD, 8 => Micalg::SHA256, 9 => Micalg::SHA384, 10 => Micalg::SHA512, 11 => Micalg::SHA224, u => Micalg::Unknown(format!("unknown-algo-{}", u)), } } } impl fmt::Display for Micalg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("pgp-")?; match self { Micalg::MD5 => f.write_str("md5"), Micalg::SHA1 => f.write_str("sha1"), Micalg::RipeMD => f.write_str("ripemd160"), Micalg::SHA256 => f.write_str("sha256"), Micalg::SHA384 => f.write_str("sha384"), Micalg::SHA512 => f.write_str("sha512"), Micalg::SHA224 => f.write_str("sha224"), Micalg::Unknown(a) => f.write_str(&a.to_lowercase()), } } } sop-0.7.0/src/plumbing.rs000064400000000000000000000024301046102023000134020ustar 00000000000000//! Pipes and fixtures, not generally useful. //! //! The functionality in this module is only useful for crates //! implementing the [`SOP`] interface, and very specialized consumers //! of it. It is likely that you don't need it. use crate::{ SOP, }; /// Returns a reference to SOP. pub trait SopRef<'s, S: SOP<'s>> { /// Returns a reference to SOP. fn sop(&self) -> &'s S; } pub trait PasswordsAreHumanReadable { /// Returns the normalized password. /// /// Use this function when you generate an artifact using a /// password (see [Generating Material with Human-Readable /// Passwords]). /// /// [Generating Material with Human-Readable Passwords]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-generating-material-with-hu fn normalized(&self) -> &[u8]; /// Returns the password in all possible variants. /// /// Use this function to try all possible variants /// (i.e. normalized, as-is, ..) when consuming an artifact (see /// [Consuming Password-protected Material]). /// /// [Consuming Password-protected Material]: https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html#name-consuming-password-protecte fn variants(&self) -> Box + '_>; }