sop-0.8.2/.cargo_vcs_info.json0000644000000001360000000000100116440ustar { "git": { "sha1": "50c451b5b99fa96d9e39d22b689cae5a3bab8b3e" }, "path_in_vcs": "" }sop-0.8.2/.gitignore000064400000000000000000000000101046102023000124130ustar 00000000000000/target sop-0.8.2/.gitlab-ci.yml000064400000000000000000000012561046102023000130740ustar 00000000000000stages: - pre-check - build - test - deploy include: - component: "gitlab.com/sequoia-pgp/common-ci/sequoia-pipeline@main" variables: NO_SEQUOIA_CRYPTO: true pages: tags: - linux stage: deploy image: jampot.sequoia-pgp.org/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 sop-0.8.2/Cargo.lock0000644000000377220000000000100076320ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anstyle" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anyhow" version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cc" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets 0.52.5", ] [[package]] name = "clap" version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" dependencies = [ "clap_builder", "clap_derive", "once_cell", ] [[package]] name = "clap_builder" version = "4.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" dependencies = [ "anstyle", "clap_lex", "terminal_size", ] [[package]] name = "clap_complete" version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fc443334c81a804575546c5a8a79b4913b50e28d69232903604cada1de817ce" dependencies = [ "clap", ] [[package]] name = "clap_derive" version = "4.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" dependencies = [ "heck", "proc-macro2", "quote", "syn", ] [[package]] name = "clap_lex" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clap_mangen" version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbae9cbfdc5d4fa8711c09bd7b83f644cb48281ac35bf97af3e47b0675864bdf" dependencies = [ "clap", "roff", ] [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "errno" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", ] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "iana-time-zone" version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "io-lifetimes" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", "windows-sys 0.48.0", ] [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "linux-raw-sys" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memsec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa0916b001582d253822171bd23f4a0229d32b9507fae236f5da8cad515ba7c" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "proc-macro2" version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] [[package]] name = "roff" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "rustix" version = "0.37.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", "windows-sys 0.48.0", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "sop" version = "0.8.2" dependencies = [ "anyhow", "chrono", "clap", "clap_complete", "clap_mangen", "libc", "memsec", "serde", "serde_json", "thiserror", ] [[package]] name = "syn" version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "terminal_size" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ "rustix", "windows-sys 0.48.0", ] [[package]] name = "thiserror" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ "windows-targets 0.52.5", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets 0.52.5", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm 0.48.5", "windows_aarch64_msvc 0.48.5", "windows_i686_gnu 0.48.5", "windows_i686_msvc 0.48.5", "windows_x86_64_gnu 0.48.5", "windows_x86_64_gnullvm 0.48.5", "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ "windows_aarch64_gnullvm 0.52.5", "windows_aarch64_msvc 0.52.5", "windows_i686_gnu 0.52.5", "windows_i686_gnullvm", "windows_i686_msvc 0.52.5", "windows_x86_64_gnu 0.52.5", "windows_x86_64_gnullvm 0.52.5", "windows_x86_64_msvc 0.52.5", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" [[package]] name = "windows_i686_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" sop-0.8.2/Cargo.toml0000644000000042660000000000100076520ustar # 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.8.2" authors = ["Justus Winter "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false 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 [lib] name = "sop" path = "src/lib.rs" [dependencies.anyhow] version = "1" optional = true [dependencies.chrono] version = "0.4.10" optional = true [dependencies.clap] version = "4" features = [ "derive", "help", "std", "usage", "wrap_help", ] optional = true default-features = false [dependencies.clap_complete] version = "4" optional = true default-features = false [dependencies.clap_mangen] version = "0.2.19" optional = true [dependencies.libc] version = "0.2" optional = true [dependencies.memsec] version = ">=0.5" default-features = false [dependencies.serde] version = "1" features = ["derive"] [dependencies.serde_json] version = "1" [dependencies.thiserror] version = "1" [features] cli = [ "anyhow", "chrono", "clap", "clap_complete", "clap_mangen", "libc", ] cliv = [ "anyhow", "chrono", "clap", "clap_complete", "clap_mangen", "libc", ] default = [] [badges.gitlab] repository = "sequoia-pgp/sequoia" [badges.maintenance] status = "actively-developed" sop-0.8.2/Cargo.toml.orig000064400000000000000000000027251046102023000133310ustar 00000000000000[package] name = "sop" description = "Rust Interface for the Stateless OpenPGP Interface" version = "0.8.2" # Update LAST_UPDATE_TIMESTAMP in src/cli.rs !!! 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 = [ "derive", "help", "std", "usage", "wrap_help"] } clap_complete = { version = "4", optional = true, default-features = false } clap_mangen = { version = "0.2.19", optional = true } libc = { version = "0.2", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" [features] default = [] cli = ["anyhow", "chrono", "clap", "clap_complete", "clap_mangen", "libc"] cliv = ["anyhow", "chrono", "clap", "clap_complete", "clap_mangen", "libc"] [package.metadata.docs.rs] all-features = true sop-0.8.2/LICENSE.txt000064400000000000000000000017771046102023000122730ustar 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.8.2/NEWS000064400000000000000000000021341046102023000111330ustar 00000000000000 -*- org -*- #+TITLE: sop-rs NEWS – history of user-visible changes #+STARTUP: content hidestars * Changes in 0.8.2 ** Notable fixes - Include inline-verify in the verification-only subset. Fixes shell completions and manual page for the verification utility, and the availability of the inline-verify subcommand if the full utility is invoked in verification-only mode (like in a multi-call binary). * Changes in 0.8.1 ** Notable fixes - File descriptors given to the CLI are now checked at argument parse time. - Usage generation has been fixed. - Manual generation has been fixed. ** Notable changes - The CLI frontends and asset generation has been reworked. Please see through the `cli` module documentation for how to make best use of the new interface. ** New functionality - cli::write_shell_completions2 ** Deprecated functionality - cli::write_shell_completions * Changes in 0.8.0 ** Notable changes - Started NEWS file. - We target draft-dkg-openpgp-stateless-cli-11 now. sop-0.8.2/README.md000064400000000000000000000067651046102023000117310ustar 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"] [[bin]] path = "src/mainv.rs" required-features = ["cliv"] [features] cli = ["sop/cli"] cliv = ["sop/cliv"] ``` And creating `src/main.rs` and `src/mainv.rs` along the lines of: ```rust fn main() { sop::cli::main(&MySOPImplementation::default()); } ``` Note: If you don't need to tweak your implementation for the verification subset of SOP, you can also build both binaries from the same source. Note for packagers: Since features in Rust are additive, building both binaries in one cargo invocation (i.e. `cargo build --features=cli,cliv`) will enable the full SOP functionality in the SOPV binary. To avoid that, build both targets individually. sop-0.8.2/openpgp-policy.toml000064400000000000000000002404061046102023000143040ustar 00000000000000version = 0 commit_goodlist = [] [authorization.dvn] sign_commit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: 0CC0 11A7 C3DC 6EB4 7567 9BC9 AC0A BA31 866A 7E76 Comment: Devan Carpenter Comment: dvn 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 /g== =nP6D -----END PGP PUBLIC KEY BLOCK----- """ [authorization.justus] sign_commit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: D2F2 C5D4 5BE9 FDE6 A4EE 0AAF 3185 5247 6038 31FD Comment: Justus Winter (Code Signing Key) Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter Comment: Justus Winter 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 =uq6w -----END PGP PUBLIC KEY BLOCK----- """ [authorization.neal] sign_commit = true keyring = """ -----BEGIN PGP PUBLIC KEY BLOCK----- Comment: F717 3B3C 7C68 5CD9 ECC4 191B 74E4 45BA 0E15 C957 Comment: Neal H. Walfield (Code Signing Key) Comment: Neal H. Walfield Comment: Neal H. Walfield Comment: Neal H. Walfield Comment: Neal H. Walfield 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 HCL+AFMw7YtSwqT2TxbLYPc/7bU+pWAe1YT6pk0OLZhuRzyG3UuZaMVFHcp26sLP rGROgdo4R79MUUDMLYkuYKRcTrdi4bceZlmIpJeUt+SY8zcAP3m4epeEOu3vrJER 1xglFmeNDEcjD32iTJHlkGcyHw+NWhl6l8LvFov4ZVyW4pXiYjnjbOHq8ukbPIDP eafMkishG18ekELGoGuLpDqnPYG9AYXiy03D6ZwduLI770bMkG5zhKzDksIGQ8x0 JUPvtroSlgNQwe1PFlDS7c2bIs3JqwCpuJClEEqKkvg7samKXWNCO23vl9ucmOwk xMm2w4v09WooFlG28hSsH+FL+ROEU2Px1MJbUahReTbsho828NstqZ46UWmcWe9q 9YDwnHBj2sKEy47f2mZ3ZoR1wchRIxze7dcPLUTPMoEyjAFKzsoozpE+Ah/hUwIj PSEK0kfPaQx4jR9QKCwTnMrHNs16nVUM9zhXLNlwb2rdG6Y0dAyBllLvE4UHAF8x 3Gv/6O3xC/PU6LrAUitH0mViV+GHHn3jWV7oO3NmV27x+Vw1DqZO9XZMPqmgRoRz MEQXzSROZWFsIEguIFdhbGZpZWxkIDxuZWFsQHdhbGZpZWxkLm9yZz7CwWgEEwEK AD4CGwMICwkIBw0MCwoFFQoJCAsCHgECF4ACGQEWIQSPF3dxGKM92pukjmKqyzJD YwBS2QUCZCarHgUJEORDtQAKCRCqyzJDYwBS2YLaDp0fMZrwfLGqC8LJiRfI+HwV +29E7EWaJmOF6S24sewr1t//2Vz4mc41E6bDprHsHte4JEmzyCnXnXlPaobyONWU IbYcnM8723myJml0Du4EvvvJPUieMiwFWzRdmFxc/eQ+6aeTTUknNJKVhY18/qsA pvYTIkvmlePgLQVGvpnYqQ1ElgMf768KJ4+lVMmIT2+fHlLe5RyJRlZcjVryV5H9 fOy+TQqhLLoTFhxAiJBACYIUCglGgjTQejivXXRXjhvMpvlBzSxwI9iZGZUL4FE1 kjK99epoictN9iv1O5XTZYTqkdBf1PD79GicHAbG6Io8F07xR/CijZl27l6rLb55 9/33Iswi6Ma8mdnmeTJ1d+io9XbtAGhPZqKxM2XtKbVO8uJli6YCqeEpVP+/Wy2c g4RSj42b5yrplbqVvnbddx8ZYCm5SXD6Pb/ZrrUmKlayuM2UnMs9+snIGG/SMRUv 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 =MESu -----END PGP PUBLIC KEY BLOCK----- """ sop-0.8.2/src/cli.rs000064400000000000000000001541421046102023000123470ustar 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. //! //! Via the "cliv" feature, it can also produce a smaller standalone //! binary that only implements the sopv subset. //! //! To use, add this snippet to `Cargo.toml`: //! //! ```toml //! [[bin]] //! name = "my-sop" //! required-features = ["cli"] //! //! [[bin]] //! name = "my-sopv" //! required-features = ["cliv"] //! //! [features] //! cli = ["sop/cli"] //! cliv = ["sop/cliv"] //! ``` //! //! And create `src/bin/my-sop.rs` and `src/bin/my-sopv.rs`, //! containing, respectively, something along the lines of: //! //! ```rust,ignore //! fn main() { //! // Be clever and inspect argv[0]. //! let argv0 = std::env::args_os().next(); //! let variant = if argv0 //! .map(|s| s.to_string_lossy().contains("my-sopv")) //! .unwrap_or(false) //! { //! // Restrict to the verification subset. //! sop::cli::Variant::Verification //! } else { //! sop::cli::Variant::Full //! }; //! //! sop::cli::main(&mut MySOPImplementation::default(), //! sop::cli::Variant::Verification); //! } //! ``` //! //! And for the verification subset: //! //! ```rust,ignore //! fn main() { //! sop::cli::main(&mut MySOPImplementation::default(), //! sop::cli::Variant::Verification); //! } //! ``` //! //! Note: If you don't need to tweak your implementation for the //! verification subset of SOP, you can also build both binaries from //! the same source. //! //! ## Generating shell completions and manual pages //! //! 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 //! use std::{ //! env, //! error::Error, //! fs, //! path::PathBuf, //! }; //! //! use sop::cli::Variant; //! //! #[path = "src/version.rs"] //! mod version; //! //! fn main() { //! #[cfg(feature = "cli")] //! cli("sqop", Variant::Full).unwrap(); //! //! #[cfg(feature = "cliv")] //! cli("sqopv", Variant::Verification).unwrap(); //! } //! //! /// Writes shell completions and man pages. //! fn cli(name: &'static str, variant: Variant) -> Result<(), Box> { //! let version = version::Version::with_name(name); //! //! sop::cli::write_shell_completions2( //! variant, name, asset_out_dir("shell-completions")?)?; //! sop::cli::write_man_pages( //! variant, &version, "Sequoia PGP", //! asset_out_dir("man-pages")?)?; //! //! Ok(()) //! } //! //! /// Variable name to control the asset out directory with. //! const ASSET_OUT_DIR: &str = "ASSET_OUT_DIR"; //! //! /// Returns the directory to write the given assets to. //! #[allow(dead_code)] //! fn asset_out_dir(asset: &str) -> Result> { //! println!("cargo:rerun-if-env-changed={}", ASSET_OUT_DIR); //! let outdir: PathBuf = //! env::var_os(ASSET_OUT_DIR).unwrap_or_else( //! || env::var_os("OUT_DIR").expect("OUT_DIR not set")).into(); //! if outdir.exists() && ! outdir.is_dir() { //! return Err( //! format!("{}={:?} is not a directory", ASSET_OUT_DIR, outdir).into()); //! } //! //! let path = outdir.join(asset); //! fs::create_dir_all(&path)?; //! Ok(path) //! } //! ``` //! //! # 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::{ Save, Load, SOP, }; #[cfg(feature = "cli")] use crate::{ ArmorLabel, EncryptAs, InlineSignAs, Password, SignAs, }; /// The last meaningful update of the command line interface. /// /// This will be used as-is in the generated manual pages. pub const LAST_UPDATE_TIMESTAMP: &'static str = // !!! Update this with every release changing the interface. !!! // // date +"%B %Y" "February 2025"; #[derive(Debug, Parser)] #[clap(about = "An implementation of the Stateless OpenPGP Command Line Interface")] #[clap(disable_version_flag(true))] struct Cli { /// Emit verbose output for debugging. #[clap(long = "debug", global = true)] debug: bool, #[clap(subcommand)] operation: O, } /// Variant of the SOP specification. #[derive(Clone, Copy)] pub enum Variant { /// The full functionality. Full, /// The verification subset. Verification, } impl Cli { fn with_version<'v, V>(v: &V, variant: Variant) -> Result where V: crate::ops::Version<'v> + ?Sized { // Do a little dance to supply name and version to clap. let (name, version, about) = { let v = v.frontend()?; ( v.name.clone(), v.version.clone(), format!("An implementation of the {}\ Stateless OpenPGP Command \ Line Interface using {} {}", match variant { Variant::Full => "", Variant::Verification => "verification-only subset of the ", }, v.name, v.version), ) }; Ok(Cli::::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)) } } /// All the operations of the verification subset of SOP. #[derive(Debug, Parser)] enum VerificationSubset { /// Prints version information. /// /// Invoked without arguments, returns name and version of the SOP /// implementation. #[clap(display_order = 100)] #[cfg(any(feature = "cliv", feature = "cli"))] Version { /// Returns name and version of the primary underlying OpenPGP /// toolkit. #[clap(long, conflicts_with("extended"), conflicts_with("sop_spec"), conflicts_with("sopv"))] 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"), conflicts_with("sopv"))] extended: bool, /// Returns the latest version of the SOP spec that is /// implemented. #[clap(long, conflicts_with("backend"), conflicts_with("extended"), conflicts_with("sopv"))] sop_spec: bool, /// Returns "1.0\n" if the sopv subset is implemented. #[clap(long, conflicts_with("backend"), conflicts_with("extended"), conflicts_with("sop_spec"))] sopv: bool, }, /// Verifies Detached Signatures. #[clap(display_order = 500)] #[cfg(any(feature = "cli", feature = "cliv"))] 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, }, /// Verifies Inline-Signed Messages. #[clap(display_order = 751)] #[cfg(any(feature = "cli", feature = "cliv"))] 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, }, } /// All the operations of the full SOP. #[derive(Debug, Parser)] enum Operation { /// #[command(flatten)] VerificationSubset(VerificationSubset), /// Emits a list of profiles supported by the identified /// subcommand. #[clap(display_order = 150)] #[cfg(feature = "cli")] ListProfiles { subcommand: String, }, /// Generates a Secret Key. #[clap(display_order = 200)] #[cfg(feature = "cli")] 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)] #[cfg(feature = "cli")] 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)] #[cfg(feature = "cli")] 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 = 230)] #[cfg(feature = "cli")] ExtractCert { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, }, /// Keep a Secret Key Up-To-Date. #[clap(display_order = 240)] #[cfg(feature = "cli")] UpdateKey { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, /// Don't make the updated key encryption-capable if it isn't /// already. #[clap(long)] signing_only: bool, /// Don't advertise support for capabilities that aren't /// already advertised by the key. #[clap(long)] no_added_capabilities: bool, /// Unlock the keys with these passwords. #[clap(long, number_of_values = 1)] with_key_password: Vec, /// Merge updates into the key. #[clap(long, number_of_values = 1)] merge_certs: Vec, }, /// Merge OpenPGP Certificates. #[clap(display_order = 250)] #[cfg(feature = "cli")] MergeCerts { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, /// Merge updates into the certs. certs: Vec, }, /// Certify OpenPGP Certificate User IDs. #[clap(display_order = 300)] #[cfg(feature = "cli")] CertifyUserid { /// Don't ASCII-armor output. #[clap(long)] no_armor: bool, /// Certify the specified user IDs. #[clap(long, required = true, number_of_values = 1)] userid: Vec, /// Unlock the keys with these passwords. #[clap(long, number_of_values = 1)] with_key_password: Vec, /// Don't require self-signatures on the user IDs to be /// certified. #[clap(long)] no_require_self_sig: bool, /// Create certifications using these keys. keys: Vec, }, /// Validate a User ID in an OpenPGP Certificate. #[clap(display_order = 320)] #[cfg(feature = "cli")] ValidateUserid { /// Treat USERID as an e-mail address, and matched only /// against the e-mail address part of each correctly bound /// User ID. #[clap(long)] addr_spec_only: bool, /// Evaluate the validity of the User ID at the specified /// time, not at the current time. #[clap(long)] validate_at: Option, /// The User ID to validate. userid: String, /// Authority OpenPGP certificates, i.e. trust roots. #[clap(required = true)] certs: Vec, }, /// Creates Detached Signatures. #[clap(display_order = 400)] #[cfg(feature = "cli")] 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, }, /// Encrypts a Message. #[clap(display_order = 600)] #[cfg(feature = "cli")] 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)] #[cfg(feature = "cli")] 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)] #[cfg(feature = "cli")] 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)] #[cfg(feature = "cli")] Dearmor { }, /// Splits Signatures from an Inline-Signed Message. #[clap(display_order = 750)] #[cfg(feature = "cli")] InlineDetach { /// Don't ASCII-armor the signatures. #[clap(long)] no_armor: bool, /// Write Signatures here. #[clap(long)] signatures_out: String, }, /// Creates Inline-Signed Messages. #[clap(display_order = 752)] #[cfg(feature = "cli")] 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. #[deprecated(note="Use write_shell_completions2.")] pub fn write_shell_completions(binary_name: B, out_path: P) -> io::Result<()> where B: AsRef, P: AsRef, { let variant = if binary_name.as_ref().ends_with("v") { Variant::Verification } else { Variant::Full }; write_shell_completions2(variant, binary_name, out_path) } /// Generates shell completions for the given variant. pub fn write_shell_completions2(variant: Variant, 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)?; const SHELLS: &[Shell] = &[ Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell, Shell::Elvish, ]; match variant { Variant::Verification => { let mut clap = Cli::::command(); for shell in SHELLS { generate_to(*shell, &mut clap, binary_name, out_path)?; } }, Variant::Full => { let mut clap = Cli::::command(); for shell in SHELLS { generate_to(*shell, &mut clap, binary_name, out_path)?; } }, } println!("cargo:warning={}: shell completions written to {}", binary_name, out_path.display()); Ok(()) } /// Generates man pages. pub fn write_man_pages(variant: Variant, version: &V, author: A, out_path: P) -> Result<()> where V: for <'a> crate::ops::Version<'a>, A: ToString, P: AsRef, { let out_path = out_path.as_ref(); std::fs::create_dir_all(&out_path)?; match variant { Variant::Verification => { let cmd = Cli::::with_version(version, variant)? .author(Box::leak(author.to_string().into_boxed_str()) as &str); generate_to(cmd, LAST_UPDATE_TIMESTAMP, out_path)?; }, Variant::Full => { let cmd = Cli::::with_version(version, variant)? .author(Box::leak(author.to_string().into_boxed_str()) as &str); generate_to(cmd, LAST_UPDATE_TIMESTAMP, out_path)?; }, } println!("cargo:warning={}: man pages written to {}", version.frontend()?.name, out_path.display()); Ok(()) } /// Like clap's version, but modifies the man page's source and date. fn generate_to( cmd: clap::Command, date: &str, out_dir: impl AsRef, ) -> Result<(), std::io::Error> { fn generate( cmd: clap::Command, source: &str, date: &str, out_dir: &std::path::Path, ) -> Result<(), std::io::Error> { for cmd in cmd.get_subcommands().filter(|s| !s.is_hide_set()).cloned() { generate(cmd, source, date, out_dir)?; } clap_mangen::Man::new(cmd) .source(source) // Always use the top-level source. .date(date) // Set the last modification timestamp. .generate_to(out_dir)?; Ok(()) } // Use the top-level source for all man pages. let source = format!( "{} {}", cmd.get_name(), cmd.get_version().unwrap_or_default() ); let mut cmd = cmd.disable_help_subcommand(true); cmd.build(); generate(cmd, &source, date, out_dir.as_ref()) } /// Implements the SOP command-line interface. pub fn main<'s, S, C, K, Sigs>(sop: &'s mut S, variant: Variant) -> ! 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, variant) { 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 mut S, variant: Variant) -> 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, { // If we are the full version, but are invoked as the verification // subset, we first parse the arguments using the verification // subset parser. #[cfg(feature = "cli")] if let Variant::Verification = variant { let app = Cli::::with_version( sop.version()?.as_ref(), Variant::Verification)?; let matches = match app.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()), }; }, }; Cli::::from_arg_matches(&matches)?; // If we get here, the syntax checked out. We now just fall // through to the full parser, which accepts a superset of // what we just parsed. } let app = Cli::::with_version(sop.version()?.as_ref(), variant)?; 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()), }; }, }; let cli = Cli::from_arg_matches(&matches)?; sop.debug(cli.debug); match cli.operation { Operation::VerificationSubset(VerificationSubset::Version { backend, extended, sop_spec, sopv, }) => { let version = sop.version()?; match (backend, extended, sop_spec, sopv) { (false, false, false, false) => { let v = version.frontend()?; println!("{} {}", v.name, v.version); }, (true, false, false, false) => { let v = version.backend()?; println!("{} {}", v.name, v.version); }, (false, true, false, false) => { let v = version.frontend()?; println!("{} {}", v.name, v.version); println!("sop-rs {}", env!("CARGO_PKG_VERSION")); println!("{}", version.extended()?); }, (false, false, true, false) => { println!("{}", sop.spec_version()); }, (false, false, false, true) => { println!("{}", sop.sopv_version()?); }, _ => unreachable!("flags are mutually exclusive"), } }, #[cfg(feature = "cli")] 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); } }, #[cfg(feature = "cli")] 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_stdout(! no_armor)?; }, #[cfg(feature = "cli")] 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_stdin(sop)?)? .to_stdout(! no_armor)?; } #[cfg(feature = "cli")] 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_stdin(sop)?)? .to_stdout(! no_armor)?; } #[cfg(feature = "cli")] Operation::ExtractCert { no_armor, } => { sop.extract_cert()? .keys(&K::from_stdin(sop)?)? .to_stdout(! no_armor)?; }, #[cfg(feature = "cli")] Operation::UpdateKey { no_armor, signing_only, no_added_capabilities, with_key_password, merge_certs, } => { let mut op = sop.update_key()?; if signing_only { op = op.signing_only(); } if no_added_capabilities { op = op.no_added_capabilities(); } for p in with_key_password { op = op.with_key_password(get_password(p)?)?; } for (name, mut stream) in load_files(merge_certs)? { op = op.merge_updates(&C::from_reader(sop, &mut stream, Some(name))?)?; } let keys = op.update(&K::from_stdin(sop)?)?; keys.to_stdout(! no_armor)?; }, #[cfg(feature = "cli")] Operation::MergeCerts { no_armor, certs, } => { let mut op = sop.merge_certs()?; for (name, mut stream) in load_files(certs)? { op = op.merge_updates(&C::from_reader(sop, &mut stream, Some(name))?)?; } let certs = op.merge(&C::from_stdin(sop)?)?; certs.to_stdout(! no_armor)?; }, #[cfg(feature = "cli")] Operation::CertifyUserid { no_armor, userid, with_key_password, no_require_self_sig, keys, } => { let mut op = sop.certify_userid()?; if no_require_self_sig { op = op.no_require_self_sig(); } for u in userid { op = op.userid(u); } for p in with_key_password { op = op.with_key_password(get_password(p)?)?; } for (name, mut stream) in load_files(keys)? { op = op.keys(&K::from_reader(sop, &mut stream, Some(name))?)?; } let certs = op.certify(&C::from_stdin(sop)?)?; certs.to_stdout(! no_armor)?; }, #[cfg(feature = "cli")] Operation::ValidateUserid { addr_spec_only, validate_at, userid, certs, } => { let mut op = sop.validate_userid()?; if let Some(t) = validate_at { op = op.validate_at(t.into())?; } for (name, mut stream) in load_files(certs)? { op = op.trust_roots(&C::from_reader(sop, &mut stream, Some(name))?)?; } op = op.target_certs(&C::from_stdin(sop)?)?; if addr_spec_only { op.email(&userid)?; } else { op.userid(&userid)?; } }, #[cfg(feature = "cli")] 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 (name, mut stream) in load_files(keys)? { op = op.keys(&K::from_reader(sop, &mut stream, Some(name))?)?; } 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_stdout(! no_armor)?; }, #[cfg(any(feature = "cli", feature = "cliv"))] Operation::VerificationSubset(VerificationSubset::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 (name, mut stream) in load_files(certs)? { op = op.certs(&C::from_reader(sop, &mut stream, Some(name))?)?; } let (name, mut stream) = load_file(signatures)?; let verifications = op.signatures(&Sigs::from_reader(sop, &mut stream, Some(name))?)? .data(&mut io::stdin())?; for v in verifications { println!("{}", v); } }, #[cfg(feature = "cli")] 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 (name, mut stream) in load_files(sign_with)? { op = op.sign_with_keys(&K::from_reader(sop, &mut stream, Some(name))?)?; } for pw in with_password.into_iter().map(get_password) { op = op.with_password(pw?)?; } for (name, mut stream) in load_files(certs)? { op = op.with_certs(&C::from_reader(sop, &mut stream, Some(name))?)?; } 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()); } } }, #[cfg(feature = "cli")] 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 (_name, 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 (name, mut stream) in load_files(keys)? { op = op.with_keys(&K::from_reader(sop, &mut stream, Some(name))?)?; } for (name, mut stream) in load_files(verify_with)? { op = op.verify_with_certs(&C::from_reader(sop, &mut stream, Some(name))?)?; } 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()); } } }, #[cfg(feature = "cli")] Operation::Armor { label, } => { #[allow(deprecated)] let op = sop.armor()?.label(label); op.data(&mut io::stdin())? .to_writer(&mut io::stdout())?; }, #[cfg(feature = "cli")] Operation::Dearmor {} => { let op = sop.dearmor()?; op.data(&mut io::stdin())? .to_writer(&mut io::stdout())?; }, #[cfg(feature = "cli")] 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)?; }, #[cfg(any(feature = "cli", feature = "cliv"))] Operation::VerificationSubset(VerificationSubset::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 (name, mut stream) in load_files(certs)? { op = op.certs(&C::from_reader(sop, &mut stream, Some(name))?)?; } let verifications = op.message(&mut io::stdin())?.to_writer(&mut io::stdout())?; for v in verifications { writeln!(&mut verifications_out, "{}", v)?; } }, #[cfg(feature = "cli")] 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 (name, mut stream) in load_files(keys)? { op = op.keys(&K::from_reader(sop, &mut stream, Some(name))?)?; } 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("@") } /// Checks whether the given descriptor is valid. #[cfg(unix)] fn check_fd(fd: std::os::fd::RawFd) -> Result<()> { unsafe { // Try to dup(2) it. let dup = libc::dup(fd); if dup > 0 { libc::close(dup); Ok(()) } else { Err(std::io::Error::last_os_error().into()) } } } /// Loads the given (special) file. fn load_file>(file: S) -> Result<(String, Box)> { let f = file.as_ref(); let file_name = f.into(); 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)?; check_fd(fd)?; let f = unsafe { std::fs::File::from_raw_fd(fd) }; return Ok((file_name, 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((file_name, 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((file_name, Box::new(io::Cursor::new(value.into_bytes())))); } } return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix)); } std::fs::File::open(f) .map(|f| -> (String, Box) { (file_name, 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. #[cfg(feature = "cli")] fn get_password>(location: S) -> Result { let mut stream = load_file(location)?.1; 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. #[cfg(feature = "cli")] fn get_password_unchecked>(location: S) -> Result { let mut stream = load_file(location)?.1; 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)?; check_fd(fd)?; 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. #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct Timestamp(DateTime); impl std::str::FromStr for Timestamp { 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| Timestamp(t)) } } impl From for std::time::SystemTime { fn from(t: Timestamp) -> 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 { /// Unspecified failure. #[error("Unspecified failure")] UnspecifiedFailure, /// 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, /// The primary key of a KEYS object is too weak or revoked. #[error("The primary key of a KEYS object is too weak or revoked")] PrimaryKeyBad, /// The CERTS object has no matching User ID. #[error("The CERTS object has no matching User ID")] CertUseridNoMatch, /// An I/O operation failed. #[error(transparent)] IoError(#[from] io::Error), } impl From for Error { fn from(e: crate::Error) -> Self { use crate::Error as E; use Error as CE; match e { E::UnspecifiedFailure => CE::UnspecifiedFailure, 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::PrimaryKeyBad => CE::PrimaryKeyBad, E::CertUseridNoMatch => CE::CertUseridNoMatch, E::IoError(e) => CE::IoError(e), } } } impl From for i32 { fn from(e: Error) -> Self { use Error::*; match e { UnspecifiedFailure => 1, 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, PrimaryKeyBad => 103, CertUseridNoMatch => 107, 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.8.2/src/errors.rs000064400000000000000000000072421046102023000131120ustar 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 { /// Unspecified failure. #[error("Unspecified failure")] UnspecifiedFailure, /// 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, /// The primary key of a KEYS object is too weak or revoked. #[error("The primary key of a KEYS object is too weak or revoked")] PrimaryKeyBad, /// The CERTS object has no matching User ID. #[error("The CERTS object has no matching User ID")] CertUseridNoMatch, /// An IO error occurred. #[error(transparent)] 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.8.2/src/lib.rs000064400000000000000000000704201046102023000123420ustar 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(any(feature = "cli", feature = "cliv"))] pub mod cli; /// Loads objects like certs and keys. pub trait Load<'s, S: SOP<'s>> { /// Loads objects like certs and keys from stdin. fn from_stdin(sop: &'s S) -> Result where Self: Sized, { Self::from_reader(sop, &mut io::stdin(), Some("/dev/stdin".into())) } /// Loads objects like certs and keys from the given file. fn from_file

(sop: &'s S, path: P) -> Result where Self: Sized, P: AsRef, { let path = path.as_ref(); Self::from_reader(sop, &mut std::fs::File::open(path)?, Some(path.display().to_string())) } /// Loads objects like certs and keys from the given reader. fn from_reader(sop: &'s S, source: &mut (dyn io::Read + Send + Sync), source_name: Option) -> 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), None) } /// Returns the source name, if any. fn source_name(&self) -> Option<&str>; } /// Saves objects like certs and keys. pub trait Save { /// Writes objects like certs and keys to stdout. fn to_stdout(&self, armored: bool) -> Result<()> { self.to_writer(armored, &mut io::stdout()) } /// 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>; /// Controls debugging. /// /// If enabled, implementations may emit debugging information in /// any way they see fit, most commonly by printing to stderr. fn debug(&mut self, enable: bool) { let _ = enable; } /// 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-11" } /// Completeness of the sopv subset /// /// If all the features of the sopv subset are implemented, an /// implementation should return Ok("1.0") fn sopv_version(&'s self) -> Result<&'static str> { Err(Error::UnsupportedOption) } /// 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(&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_file(sop, "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_file(sop, "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_file(sop, "alice.secret")?; /// /// let alice_public = sop.extract_cert()? /// .keys(&alice_secret)?; /// # Ok(()) } /// ``` fn extract_cert(&'s self) -> Result + 's>>; /// Keeps a Secret Key Up-To-Date. /// /// This update will "fix" everything that the implementation /// knows how to fix to bring each Transferable Secret Key up to /// reasonable modern practice. Each Transferable Secret Key /// output must be capable of signing, and (unless --signing-only /// is provided) capable of decryption. The primary key of each /// Transferable Secret Key will not be changed in any way that /// affects its fingerprint. /// /// One important aspect of sop update-key is how it handles /// advertisement of support for various OpenPGP capabilities /// (algorithms, mechanisms, etc). All capabilities that the /// implementation knows it does not support, or knows to be weak /// and/or deprecated MUST be removed from the output Transferable /// Secret Keys. This includes unknown/deprecated flags in the /// Features subpacket, and any unknown/deprecated algorithm IDs /// in algorithm preferences subpackets. For example, an /// implementation compliant with [RFC9580] will never emit a /// Transferable Secret Key with a Preferred Hash Preferences /// subpacket that explicitly indicates support for MD5, /// RIPEMD160, or SHA1. fn update_key(&'s self) -> Result + 's>>; /// Merge OpenPGP Certificates. /// /// This can be used, for example, to absorb a third-party /// certification into a certificate, or to update a certificate's /// feature advertisements without losing local annotations. fn merge_certs(&'s self) -> Result + 's>>; /// Certify OpenPGP Certificate User IDs. /// /// With each Transferable Secret Key provided, add a third-party /// certification to the provided certificates, and /// emit the updated OpenPGP certificates. fn certify_userid(&'s self) -> Result + 's>>; /// Validate a User ID in an OpenPGP Certificate. /// /// Given a set of authority OpenPGP certificates, succeed if and /// only if all provided OpenPGP certificates are correctly bound /// by at least one valid signature from one authority to the User /// ID or email address in question. fn validate_userid(&'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_file(sop, "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_file(sop, "alice.public")?; /// let sig = /// Sigs::from_file(sop, "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_file(sop, "alice.secret")?; /// let bob_public = /// Certs::from_file(sop, "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_file(sop, "alice.public")?; /// let bob_secret = /// Keys::from_file(sop, "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_file(sop, "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_file(sop, "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.8.2/src/ops.rs000064400000000000000000000644521046102023000124050ustar 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::update_key`]. pub trait UpdateKey<'s, S: SOP<'s>, Certs: Save, Keys: Load<'s, S>> { /// Don't make the updated key encryption-capable if it isn't /// already. fn signing_only(self: Box) -> Box + 's>; /// Don't advertise support for capabilities that aren't already /// advertised by the key. fn no_added_capabilities(self: Box) -> Box + '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>>; /// Merges updates into the keys. fn merge_updates(self: Box, updates: &Certs) -> Result + 's>>; /// Updates the keys. fn update(self: Box, keys: &Keys) -> Result; } /// Builder for [`SOP::merge_certs`]. pub trait MergeCerts<'s, S: SOP<'s>, Certs: Load<'s, S> + Save> { /// Merges updates into the certs. fn merge_updates(self: Box, updates: &Certs) -> Result + 's>>; /// Merges the certs. fn merge(self: Box, certs: &Certs) -> Result; } /// Builder for [`SOP::certify_userid`]. pub trait CertifyUserID<'s, S: SOP<'s>, Certs: Save, Keys: Load<'s, S>> { /// Certifies the given User ID. fn userid(self: Box, userid: String) -> Box + '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>>; /// Don't require self-signatures on the user IDs to be /// certified. fn no_require_self_sig(self: Box) -> Box + 's>; /// Create certifications using these keys. fn keys(self: Box, keys: &Keys) -> Result + 's>>; /// Create the certifications. fn certify(self: Box, certs: &Certs)-> Result; } /// Builder for [`SOP::validate_userid`]. pub trait ValidateUserID<'s, S: SOP<'s>, Certs: Load<'s, S>> { /// Supplies authority OpenPGP certificates, i.e. trust roots. fn trust_roots(self: Box, certs: &Certs) -> Result + 's>>; /// Tests that these certificates have the given User ID. fn target_certs(self: Box, certs: &Certs) -> Result + 's>>; /// Evaluate the validity of the User ID at the specified time, /// not at the current time. fn validate_at(self: Box, at: SystemTime) -> Result + 's>>; /// Validates an User ID. fn userid(self: Box, userid: &str) -> Result<()>; /// Validates an email address. /// /// Matched only against the e-mail address part of each correctly /// bound User ID. fn email(self: Box, email: &str) -> 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, Hash)] pub struct Verification { creation_time: SystemTime, signing_key_fingerprint: String, signing_cert_fingerprint: String, signature_mode: SignatureMode, description: Description, } /// A successful signature verification. #[derive(Debug, Hash)] #[derive(serde::Serialize)] pub struct Description { #[serde(skip_serializing_if = "Vec::is_empty")] signers: Vec, #[serde(skip_serializing_if = "Option::is_none")] comment: Option, #[serde(skip_serializing_if = "serde_json::Value::is_null")] ext: serde_json::Value, } #[cfg(any(feature = "cli", feature = "cliv"))] 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(), serde_json::to_string(&self.description) .unwrap_or_else(|e| e.to_string())) } } 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, description: Description { signers: Default::default(), comment: message.into().map(Into::into), ext: Default::default(), }, }) } /// Adds an issuer name. /// /// This is used by the SOP cli to name the certificates that were /// used to verify the signature with. pub fn add_signer(&mut self, name: N) { self.description.signers.push(name.to_string()); } /// Sets arbitrary, implementation-defined data to the /// verification object. pub fn set_extension_info>(&mut self, ext: E) { self.description.ext = ext.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.description.comment.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(any(feature = "cli", feature = "cliv"))] 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.8.2/src/plumbing.rs000064400000000000000000000024301046102023000134050ustar 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 + '_>; }