From 936ae250e19a3601031ce89e50b53f77fb6db377 Mon Sep 17 00:00:00 2001 From: "Neal H. Walfield" Date: Tue, 14 Mar 2023 17:09:03 +0100 Subject: [PATCH] Add support for a persistant certificate store - Add support for a persistant certificate store using `sequoia-cert-store`. - Add `sq --no-cert-store` to disable the use of the certificate store. Add `sq --cert-store PATH` to use an alternate certificate store. - Add `sq import` to import a certificate into the certificate store. Add `sq export` to export certificates. - Modify `sq certify`, `sq encrypt`, and `sq verify` to lookup certificates in the certificate store, if it is configured. --- Cargo.lock | 224 +++++++++++++++- Cargo.toml | 5 +- NEWS | 33 +++ sq-subplot.md | 516 ++++++++++++++++++------------------- src/commands/certify.rs | 10 +- src/commands/decrypt.rs | 12 +- src/commands/export.rs | 189 ++++++++++++++ src/commands/import.rs | 69 +++++ src/commands/mod.rs | 36 ++- src/commands/sign.rs | 4 +- src/macros.rs | 14 + src/sq.rs | 306 +++++++++++++++++++++- src/sq_cli/encrypt.rs | 12 +- src/sq_cli/export.rs | 118 +++++++++ src/sq_cli/import.rs | 20 ++ src/sq_cli/mod.rs | 32 ++- src/sq_cli/verify.rs | 19 +- tests/sq-certify.rs | 104 +++++++- tests/sq-decrypt.rs | 4 + tests/sq-encrypt.rs | 90 +++++++ tests/sq-export.rs | 238 +++++++++++++++++ tests/sq-import.rs | 120 +++++++++ tests/sq-key-adopt.rs | 20 +- tests/sq-key-generate.rs | 3 +- tests/sq-packet-decrypt.rs | 4 + tests/sq-packet-dump.rs | 8 + tests/sq-revoke.rs | 3 +- tests/sq-sign.rs | 170 ++++++++++++ 28 files changed, 2068 insertions(+), 315 deletions(-) create mode 100644 NEWS create mode 100644 src/commands/export.rs create mode 100644 src/commands/import.rs create mode 100644 src/macros.rs create mode 100644 src/sq_cli/export.rs create mode 100644 src/sq_cli/import.rs create mode 100644 tests/sq-encrypt.rs create mode 100644 tests/sq-export.rs create mode 100644 tests/sq-import.rs diff --git a/Cargo.lock b/Cargo.lock index b08dd071..ac43b2f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -500,6 +500,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.4" @@ -535,6 +549,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.8" @@ -710,6 +734,15 @@ dependencies = [ "generic-array 0.14.5", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -720,6 +753,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -865,6 +909,27 @@ dependencies = [ "termcolor", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -880,6 +945,17 @@ dependencies = [ "instant", ] +[[package]] +name = "fd-lock" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef1a30ae415c3a691a4f41afddc2dbcd6d70baf338368d85ebc1e8ed92cedb9" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.45.0", +] + [[package]] name = "fehler" version = "1.0.0" @@ -1427,6 +1503,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "ipconfig" version = "0.3.0" @@ -1558,6 +1644,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.4.7" @@ -1863,9 +1955,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "opaque-debug" @@ -1879,6 +1971,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openpgp-cert-d" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaa2a2e4502a5daf19c5753250fc6e37daa1d06f866ec97cd5e3416e6d05883" +dependencies = [ + "anyhow", + "dirs", + "fd-lock", + "sha1collisiondetection", + "tempfile", + "thiserror", +] + [[package]] name = "openssl" version = "0.10.45" @@ -1981,7 +2087,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.34.0", ] [[package]] @@ -2494,6 +2600,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.36.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43abb88211988493c1abb44a70efa56ff0ce98f233b7b276146f1f3f7ba9644" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + [[package]] name = "rustversion" version = "1.0.6" @@ -2576,6 +2696,25 @@ dependencies = [ "sequoia-openpgp", ] +[[package]] +name = "sequoia-cert-store" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf234b5315e6fb459f72715198e6b86e6bdec0776c2bc2f2b35c83aedf343ee6" +dependencies = [ + "anyhow", + "crossbeam", + "dirs", + "num_cpus", + "once_cell", + "openpgp-cert-d", + "rayon", + "sequoia-net", + "sequoia-openpgp", + "thiserror", + "tokio", +] + [[package]] name = "sequoia-net" version = "0.26.0" @@ -2670,12 +2809,15 @@ dependencies = [ "chrono", "clap", "clap_complete", + "dirs", "fehler", "itertools 0.10.3", + "once_cell", "predicates", "roff", "rpassword", "sequoia-autocrypt", + "sequoia-cert-store", "sequoia-net", "sequoia-openpgp", "serde", @@ -3741,43 +3883,109 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "windows_x86_64_msvc 0.34.0", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + [[package]] name = "windows_aarch64_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + [[package]] name = "windows_i686_gnu" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + [[package]] name = "windows_i686_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + [[package]] name = "windows_x86_64_gnu" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + [[package]] name = "windows_x86_64_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + [[package]] name = "winreg" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 808dc408..f4fedbe3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,13 +30,16 @@ maintenance = { status = "actively-developed" } [dependencies] buffered-reader = { version = "1.0.0", default-features = false, features = ["compression-deflate"] } -sequoia-openpgp = { version = "1.1", default-features = false, features = ["compression-deflate"] } +dirs = "4" +sequoia-openpgp = { version = "1.13", default-features = false, features = ["compression-deflate"] } sequoia-autocrypt = { version = "0.25", default-features = false, optional = true } sequoia-net = { version = "0.26", default-features = false } anyhow = "1.0.18" chrono = "0.4.10" clap = { version = "3", features = ["derive", "env", "wrap_help"] } itertools = "0.10" +once_cell = "1.17" +sequoia-cert-store = "0.2" tempfile = "3.1" term_size = "0.3" tokio = { version = "1.13.1" } diff --git a/NEWS b/NEWS new file mode 100644 index 00000000..84a339dc --- /dev/null +++ b/NEWS @@ -0,0 +1,33 @@ + -*- org -*- +#+TITLE: sequoia-sq NEWS – history of user-visible changes +#+STARTUP: content hidestars + +* Changes in 0.29 +** New functionality + - `sq` now supports and implicitly uses a certificate store. By + default, `sq` uses the standard OpenPGP certificate directory. + This is located at `$HOME/.local/share/pgp.cert.d` on XDG + compliant systems. + - `sq --no-cert-store`: A new switch to disable the use of the + certificate store. + - `sq --cert-store`: A new option to use an alternate certificate + store. Currently, only OpenPGP certificate directories are + supported. + - `sq import`: A new command to import certificates into the + certificate store. + - `sq export`: A new command to export certificates from the + certificate store. + - `sq encrypt --recipient-cert`: A new option to specify a + recipient's certificate by fingerprint or key ID, which is then + looked up in the certificate store. + - `sq verify --signer-cert`: A new option to specify a signer's + certificate by fingerprint or key ID, which is then looked up in + the certificate store. + - `sq verify` now also implicitly looks for missing certificates in + the certificate store. But, unless they are explicitly named + using `--signer-cert`, they are not considered authenticated and + the verification will always fail. + - `sq certify`: If the certificate to certify is a fingerprint or + Key ID, then the corresponding certificate is looked up in the + certificate store. + * Started the NEWS file. diff --git a/sq-subplot.md b/sq-subplot.md index 04b56cdd..5e0f98e5 100644 --- a/sq-subplot.md +++ b/sq-subplot.md @@ -140,8 +140,8 @@ care of that. Here we merely verify that the new key looks OK. ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export key.pgp -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --userid Alice --export key.pgp +when I run sq --no-cert-store inspect key.pgp then stdout contains "Alice" then stdout contains "Expiration time: 20" then stdout contains "Key flags: certification" @@ -157,7 +157,7 @@ any user identifiers._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp +when I run sq --no-cert-store key generate --export key.pgp then file key.pgp contains "-----BEGIN PGP PRIVATE KEY BLOCK-----" ~~~ @@ -169,7 +169,7 @@ more than one user identifier._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --userid '' --export key.pgp +when I run sq --no-cert-store key generate --userid Alice --userid '' --export key.pgp then file key.pgp contains "Comment: Alice" then file key.pgp contains "Comment: " ~~~ @@ -184,8 +184,8 @@ Note that `sq` always creates a key usable for certification. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --cannot-sign --cannot-authenticate --cannot-encrypt -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --cannot-sign --cannot-authenticate --cannot-encrypt +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout doesn't contain "Key flags: signing" then stdout doesn't contain "Key flags: authentication" @@ -201,8 +201,8 @@ Note that `sq` always creates a key usable for certification. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --cannot-sign --cannot-authenticate -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --cannot-sign --cannot-authenticate +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout doesn't contain "Key flags: signing" then stdout doesn't contain "Key flags: authentication" @@ -216,8 +216,8 @@ for at-rest (storage) encryption._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --can-encrypt=storage -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --can-encrypt=storage +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout doesn't contain "transport encryption" then stdout contains "Key flags: data-at-rest encryption" @@ -230,8 +230,8 @@ for transport encryption._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --can-encrypt=transport -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --can-encrypt=transport +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout contains "Key flags: transport encryption" then stdout doesn't contain "data-at-rest encryption" @@ -244,8 +244,8 @@ for signing, and can't be used for encryption._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --cannot-encrypt --cannot-authenticate -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --cannot-encrypt --cannot-authenticate +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout contains "Key flags: signing" then stdout doesn't contain "Key flags: transport encryption, data-at-rest encryption" @@ -262,8 +262,8 @@ Note that `sq` always creates a key usable for certification. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --can-authenticate --cannot-sign --cannot-encrypt -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --can-authenticate --cannot-sign --cannot-encrypt +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout contains "Key flags: authentication" then stdout doesn't contain "Key flags: signing" @@ -280,8 +280,8 @@ Note that `sq` always creates a key usable for certification. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --cannot-sign -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --cannot-sign +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout contains "Key flags: authentication" then stdout contains "Key flags: transport encryption, data-at-rest encryption" @@ -298,8 +298,8 @@ Note that `sq` always creates a key usable for certification. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --cannot-authenticate -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --cannot-authenticate +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout contains "Key flags: transport encryption, data-at-rest encryption" then stdout contains "Key flags: signing" @@ -316,8 +316,8 @@ Note that `sq` always creates a key usable for certification. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --cannot-encrypt -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --cannot-encrypt +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout doesn't contain "Key flags: transport encryption, data-at-rest encryption" then stdout contains "Key flags: signing" @@ -335,8 +335,8 @@ Note that `sq` always creates a key usable for certification. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store inspect key.pgp then stdout contains "Key flags: certification" then stdout contains "Key flags: authentication" then stdout contains "Key flags: transport encryption, data-at-rest encryption" @@ -354,8 +354,8 @@ the default ever changes. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --cipher-suite=cv25519 -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --cipher-suite=cv25519 +when I run sq --no-cert-store inspect key.pgp then stdout contains "Public-key algo: EdDSA" then stdout contains "Public-key size: 256 bits" ~~~ @@ -366,8 +366,8 @@ _Requirement: We must be able to generate a 3072-bit RSA key._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --cipher-suite=rsa3k -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --cipher-suite=rsa3k +when I run sq --no-cert-store inspect key.pgp then stdout contains "Public-key algo: RSA" then stdout contains "Public-key size: 3072 bits" ~~~ @@ -378,8 +378,8 @@ _Requirement: We must be able to generate a 4096-bit RSA key._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --cipher-suite=rsa4k -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --cipher-suite=rsa4k +when I run sq --no-cert-store inspect key.pgp then stdout contains "Public-key algo: RSA" then stdout contains "Public-key size: 4096 bits" ~~~ @@ -396,10 +396,10 @@ cases. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp +when I run sq --no-cert-store key generate --export key.pgp then file key.pgp.rev contains "Comment: Revocation certificate for" -when I run sq key generate --export key2.pgp --rev-cert rev.pgp +when I run sq --no-cert-store key generate --export key2.pgp --rev-cert rev.pgp then file rev.pgp contains "Comment: Revocation certificate for" ~~~ @@ -411,8 +411,8 @@ We generate a key with defaults, and check the key expires. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store inspect key.pgp then stdout contains "Expiration time: 20" ~~~ @@ -430,10 +430,10 @@ inspect output is the last second of validity. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --expires=2038-01-19T03:14:07+00:00 -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --expires=2038-01-19T03:14:07+00:00 +when I run sq --no-cert-store inspect key.pgp then stdout contains "Expiration time: 2038-01-19 03:14" -when I run sq inspect --time 2038-01-20T00:00:00+00:00 key.pgp +when I run sq --no-cert-store inspect --time 2038-01-20T00:00:00+00:00 key.pgp then stdout contains "Invalid: The primary key is not live" ~~~ @@ -444,8 +444,8 @@ given time._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --expires-in=1y -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --expires-in=1y +when I run sq --no-cert-store inspect key.pgp then stdout contains "Expiration time: 20" ~~~ @@ -456,8 +456,8 @@ password._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store inspect key.pgp then stdout contains "Secret key: Unencrypted" ~~~ @@ -473,8 +473,8 @@ to feed `sq` a password as if the user typed it from a terminal. ~~~ given an installed sq -when I run sq key generate --export key.pgp --with-password -when I run sq inspect key.pgp +when I run sq --no-cert-store key generate --export key.pgp --with-password +when I run sq --no-cert-store inspect key.pgp then stdout contains "Secret key: Encrypted" ~~~ @@ -484,9 +484,9 @@ _Requirement: We must be able to generate a key and add a User ID to it._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp -when I run sq key userid add --userid "Juliet" --output new.pgp key.pgp -when I run sq inspect new.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key userid add --userid "Juliet" --output new.pgp key.pgp +when I run sq --no-cert-store inspect new.pgp then stdout contains "UserID: Juliet" ~~~ @@ -496,9 +496,9 @@ _Requirement: We must be able to generate a key with a User ID, and then strip t ~~~scenario given an installed sq -when I run sq key generate --userid "" --export key.pgp -when I run sq key userid strip --userid "" --output new.pgp key.pgp -when I run sq inspect new.pgp +when I run sq --no-cert-store key generate --userid "" --export key.pgp +when I run sq --no-cert-store key userid strip --userid "" --output new.pgp key.pgp +when I run sq --no-cert-store inspect new.pgp then stdout doesn't contain "UserID:" ~~~ @@ -516,8 +516,8 @@ output._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp -when I run sq key extract-cert key.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert key.pgp then stdout contains "-----BEGIN PGP PUBLIC KEY BLOCK-----" then stdout contains "-----END PGP PUBLIC KEY BLOCK-----" ~~~ @@ -530,9 +530,9 @@ file._ ~~~scenario given an installed sq -when I run sq key generate --export key.pgp --userid Alice -when I run sq key extract-cert key.pgp -o cert.pgp -when I run sq inspect cert.pgp +when I run sq --no-cert-store key generate --export key.pgp --userid Alice +when I run sq --no-cert-store key extract-cert key.pgp -o cert.pgp +when I run sq --no-cert-store inspect cert.pgp then stdout contains "OpenPGP Certificate." then stdout contains "Alice" ~~~ @@ -548,8 +548,8 @@ textual certificate. It could certainly be improved. ~~~scenario given an installed sq -when I run sq key generate --export key.pgp -when I run sq key extract-cert key.pgp --binary +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert key.pgp --binary then stdout doesn't contain "-----BEGIN PGP PUBLIC KEY BLOCK-----" then stdout doesn't contain "-----END PGP PUBLIC KEY BLOCK-----" ~~~ @@ -566,8 +566,8 @@ placeholder until Subplot learns a new trick. ~~~ given an installed sq -when I run sq key generate --export key.pgp -when I run sq key extract-cert < key.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert < key.pgp then stdout contains "-----BEGIN PGP PUBLIC KEY BLOCK-----" then stdout contains "-----END PGP PUBLIC KEY BLOCK-----" ~~~ @@ -592,10 +592,10 @@ This is for secret keys, with the output going to stdout in text form. ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring list ring.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring list ring.pgp then stdout contains "Alice" then stdout contains "Bob" ~~~ @@ -609,12 +609,12 @@ This is for secret keys, with the output going to a file in text form. ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp then file ring.pgp contains "-----BEGIN PGP PUBLIC KEY BLOCK-----" then file ring.pgp contains "-----END PGP PUBLIC KEY BLOCK-----" -when I run sq inspect ring.pgp +when I run sq --no-cert-store inspect ring.pgp then stdout contains "Transferable Secret Key." then stdout contains "Alice" then stdout contains "Bob" @@ -626,12 +626,12 @@ _Requirement: we can join two keys into a keyring in binary form._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp --binary +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp --binary when I try to run grep PGP ring.pgp then command fails -when I run sq inspect ring.pgp +when I run sq --no-cert-store inspect ring.pgp then stdout contains "Transferable Secret Key." then stdout contains "Alice" then stdout contains "Bob" @@ -647,15 +647,15 @@ so we don't change writing to stdout separately. ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq key extract-cert alice.pgp -o alice-cert.pgp -when I run sq key extract-cert bob.pgp -o bob-cert.pgp -when I run sq keyring join alice-cert.pgp bob-cert.pgp -o ring.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store key extract-cert alice.pgp -o alice-cert.pgp +when I run sq --no-cert-store key extract-cert bob.pgp -o bob-cert.pgp +when I run sq --no-cert-store keyring join alice-cert.pgp bob-cert.pgp -o ring.pgp when I run cat ring.pgp then stdout contains "-----BEGIN PGP PUBLIC KEY BLOCK-----" then stdout contains "-----END PGP PUBLIC KEY BLOCK-----" -when I run sq inspect ring.pgp +when I run sq --no-cert-store inspect ring.pgp then stdout doesn't contain "Transferable Secret Key." then stdout contains "OpenPGP Certificate." then stdout contains "Alice" @@ -677,11 +677,11 @@ certificates._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring filter --to-cert ring.pgp -o filtered.pgp -when I run sq inspect filtered.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring filter --to-cert ring.pgp -o filtered.pgp +when I run sq --no-cert-store inspect filtered.pgp then stdout contains "OpenPGP Certificate." then stdout doesn't contain "Transferable Secret Key." then stdout contains "Alice" @@ -695,10 +695,10 @@ file._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring filter --to-cert ring.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring filter --to-cert ring.pgp then stdout contains "-----BEGIN PGP PUBLIC KEY BLOCK-----" then stdout contains "-----END PGP PUBLIC KEY BLOCK-----" ~~~ @@ -709,10 +709,10 @@ _Requirement: we can get filter output in binary form._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring filter --binary --to-cert ring.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring filter --binary --to-cert ring.pgp then stdout doesn't contain "-----BEGIN PGP PUBLIC KEY BLOCK-----" ~~~ @@ -723,9 +723,9 @@ criteria._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --userid Bob --export alice.pgp -when I run sq keyring filter --prune-certs --name Alice alice.pgp -o filtered.pgp -when I run sq inspect filtered.pgp +when I run sq --no-cert-store key generate --userid Alice --userid Bob --export alice.pgp +when I run sq --no-cert-store keyring filter --prune-certs --name Alice alice.pgp -o filtered.pgp +when I run sq --no-cert-store inspect filtered.pgp then stdout contains "Alice" then stdout doesn't contain "Bob" ~~~ @@ -737,11 +737,11 @@ specific user id._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring filter --userid Alice ring.pgp -o filtered.pgp -when I run sq inspect filtered.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring filter --userid Alice ring.pgp -o filtered.pgp +when I run sq --no-cert-store inspect filtered.pgp then stdout contains "Alice" then stdout doesn't contain "Bob" ~~~ @@ -753,11 +753,11 @@ specific user ids._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring filter --userid Alice --userid Bob ring.pgp -o filtered.pgp -when I run sq inspect filtered.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring filter --userid Alice --userid Bob ring.pgp -o filtered.pgp +when I run sq --no-cert-store inspect filtered.pgp then stdout contains "Alice" then stdout contains "Bob" ~~~ @@ -769,11 +769,11 @@ part of a user ids._ ~~~scenario given an installed sq -when I run sq key generate --userid 'Alice ' --export alice.pgp -when I run sq key generate --userid 'Bob ' --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring filter --name Alice ring.pgp -o filtered.pgp -when I run sq inspect filtered.pgp +when I run sq --no-cert-store key generate --userid 'Alice ' --export alice.pgp +when I run sq --no-cert-store key generate --userid 'Bob ' --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring filter --name Alice ring.pgp -o filtered.pgp +when I run sq --no-cert-store inspect filtered.pgp then stdout contains "Alice" then stdout doesn't contain "Bob" ~~~ @@ -785,11 +785,11 @@ several names as part of the user id._ ~~~scenario given an installed sq -when I run sq key generate --userid 'Alice ' --export alice.pgp -when I run sq key generate --userid 'Bob ' --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring filter --name Alice --name Bob ring.pgp -o filtered.pgp -when I run sq inspect filtered.pgp +when I run sq --no-cert-store key generate --userid 'Alice ' --export alice.pgp +when I run sq --no-cert-store key generate --userid 'Bob ' --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring filter --name Alice --name Bob ring.pgp -o filtered.pgp +when I run sq --no-cert-store inspect filtered.pgp then stdout contains "Alice" then stdout contains "Bob" ~~~ @@ -801,11 +801,11 @@ part of a user ids._ ~~~scenario given an installed sq -when I run sq key generate --userid 'Alice ' --export alice.pgp -when I run sq key generate --userid 'Bob ' --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring filter --domain example.com ring.pgp -o filtered.pgp -when I run sq inspect filtered.pgp +when I run sq --no-cert-store key generate --userid 'Alice ' --export alice.pgp +when I run sq --no-cert-store key generate --userid 'Bob ' --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring filter --domain example.com ring.pgp -o filtered.pgp +when I run sq --no-cert-store inspect filtered.pgp then stdout contains "Alice" then stdout doesn't contain "Bob" ~~~ @@ -817,11 +817,11 @@ several names as part of the user id._ ~~~scenario given an installed sq -when I run sq key generate --userid 'Alice ' --export alice.pgp -when I run sq key generate --userid 'Bob ' --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring filter --domain example.com --domain sequoia-pgp.org ring.pgp -o filtered.pgp -when I run sq inspect filtered.pgp +when I run sq --no-cert-store key generate --userid 'Alice ' --export alice.pgp +when I run sq --no-cert-store key generate --userid 'Bob ' --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring filter --domain example.com --domain sequoia-pgp.org ring.pgp -o filtered.pgp +when I run sq --no-cert-store inspect filtered.pgp then stdout contains "Alice" then stdout contains "Bob" ~~~ @@ -837,7 +837,7 @@ _Requirement: If we ask for an unsupported major output version, we get an error ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp when I try to run sq --output-version=9999 keyring list alice.pgp then command fails when I try to run env SQ_OUTPUT_VERSION=9999 sq keyring list alice.pgp @@ -850,7 +850,7 @@ _Requirement: If we ask for an unsupported output minor version, we get an error ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp when I try to run sq --output-version=0.9999 keyring list alice.pgp then command fails ~~~ @@ -861,7 +861,7 @@ _Requirement: If we ask for an unsupported output patch version, we get an error ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp when I try to run sq --output-version=0.0.9999 keyring list alice.pgp then command fails ~~~ @@ -872,10 +872,10 @@ _Requirement: we can list the keys in a keyring._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring list ring.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring list ring.pgp then stdout contains "Alice" then stdout contains "Bob" ~~~ @@ -886,16 +886,16 @@ _Requirement: we can list the keys in a keyring in a JSON format._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --userid '' --export alice.pgp -when I run sq inspect alice.pgp +when I run sq --no-cert-store key generate --userid Alice --userid '' --export alice.pgp +when I run sq --no-cert-store inspect alice.pgp then I remember the fingerprint as ALICE_FINGERPRINT -when I run sq key generate --userid Bob --userid '' --export bob.pgp -when I run sq inspect bob.pgp +when I run sq --no-cert-store key generate --userid Bob --userid '' --export bob.pgp +when I run sq --no-cert-store inspect bob.pgp then I remember the fingerprint as BOB_FINGERPRINT -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq --output-format=json keyring list ring.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store --output-format=json keyring list ring.pgp then stdout, as JSON, matches pattern keyring-list-pattern.json when I run env SQ_OUTPUT_FORMAT=json sq keyring list ring.pgp @@ -933,8 +933,8 @@ _Requirement: we can list the keys in a key file._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq keyring list alice.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store keyring list alice.pgp then stdout contains "Alice" then stdout doesn't contain "Bob" ~~~ @@ -945,8 +945,8 @@ _Requirement: we can list all user ids._ ~~~scenario given an installed sq -when I run sq key generate --userid Alice --userid Bob --export alice.pgp -when I run sq keyring list alice.pgp --all-userids +when I run sq --no-cert-store key generate --userid Alice --userid Bob --export alice.pgp +when I run sq --no-cert-store keyring list alice.pgp --all-userids then stdout contains "Alice" then stdout contains "Bob" ~~~ @@ -971,10 +971,10 @@ is a placeholder. ~~~ given an installed sq -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq keyring join alice.pgp bob.pgp -o ring.pgp -when I run sq keyring split ring.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store keyring join alice.pgp bob.pgp -o ring.pgp +when I run sq --no-cert-store keyring split ring.pgp then the resulting files match alice,pgp and bob.pgp ~~~ @@ -996,9 +996,9 @@ in cleartext, just in case. ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export key.pgp -when I run sq key extract-cert -o cert.pgp key.pgp -when I run sq encrypt --recipient-file cert.pgp hello.txt +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert -o cert.pgp key.pgp +when I run sq --no-cert-store encrypt --recipient-file cert.pgp hello.txt then stdout contains "-----BEGIN PGP MESSAGE-----" then stdout doesn't contain "hello, world" ~~~ @@ -1015,9 +1015,9 @@ in cleartext, just in case. ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export key.pgp -when I run sq key extract-cert -o cert.pgp key.pgp -when I run sq encrypt --binary --recipient-file cert.pgp hello.txt +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert -o cert.pgp key.pgp +when I run sq --no-cert-store encrypt --binary --recipient-file cert.pgp hello.txt then stdout doesn't contain "-----BEGIN PGP MESSAGE-----" then stdout doesn't contain "hello, world" ~~~ @@ -1037,10 +1037,10 @@ files, etc). ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export key.pgp -when I run sq key extract-cert -o cert.pgp key.pgp -when I run sq encrypt -o x.pgp --recipient-file cert.pgp hello.txt -when I run sq decrypt -o output.txt --recipient-file key.pgp x.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert -o cert.pgp key.pgp +when I run sq --no-cert-store encrypt -o x.pgp --recipient-file cert.pgp hello.txt +when I run sq --no-cert-store decrypt -o output.txt --recipient-file key.pgp x.pgp then files hello.txt and output.txt match ~~~ @@ -1053,17 +1053,17 @@ recipients at a time._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export alice.pgp -when I run sq key extract-cert -o alice-cert.pgp alice.pgp -when I run sq key generate --export bob.pgp -when I run sq key extract-cert -o bob-cert.pgp bob.pgp +when I run sq --no-cert-store key generate --export alice.pgp +when I run sq --no-cert-store key extract-cert -o alice-cert.pgp alice.pgp +when I run sq --no-cert-store key generate --export bob.pgp +when I run sq --no-cert-store key extract-cert -o bob-cert.pgp bob.pgp -when I run sq encrypt --recipient-file alice-cert.pgp --recipient-file bob-cert.pgp hello.txt -o x.pgp +when I run sq --no-cert-store encrypt --recipient-file alice-cert.pgp --recipient-file bob-cert.pgp hello.txt -o x.pgp -when I run sq decrypt --recipient-file alice.pgp -o alice.txt x.pgp +when I run sq --no-cert-store decrypt --recipient-file alice.pgp -o alice.txt x.pgp then files hello.txt and alice.txt match -when I run sq decrypt --recipient-file bob.pgp -o bob.txt x.pgp +when I run sq --no-cert-store decrypt --recipient-file bob.pgp -o bob.txt x.pgp then files hello.txt and bob.txt match ~~~ @@ -1076,12 +1076,12 @@ same time._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export alice.pgp -when I run sq key extract-cert -o alice-cert.pgp alice.pgp +when I run sq --no-cert-store key generate --export alice.pgp +when I run sq --no-cert-store key extract-cert -o alice-cert.pgp alice.pgp -when I run sq encrypt --recipient-file alice-cert.pgp --signer-file alice.pgp hello.txt -o x.pgp +when I run sq --no-cert-store encrypt --recipient-file alice-cert.pgp --signer-file alice.pgp hello.txt -o x.pgp -when I run sq decrypt --recipient-file alice.pgp -o alice.txt x.pgp --signer-file alice-cert.pgp +when I run sq --no-cert-store decrypt --recipient-file alice.pgp -o alice.txt x.pgp --signer-file alice-cert.pgp then files hello.txt and alice.txt match ~~~ @@ -1095,12 +1095,12 @@ there should be no output._ given an installed sq given file hello.txt given file empty -when I run sq key generate --export alice.pgp -when I run sq key extract-cert -o alice-cert.pgp alice.pgp -when I run sq key generate --export bob.pgp -when I run sq key extract-cert -o bob-cert.pgp bob.pgp +when I run sq --no-cert-store key generate --export alice.pgp +when I run sq --no-cert-store key extract-cert -o alice-cert.pgp alice.pgp +when I run sq --no-cert-store key generate --export bob.pgp +when I run sq --no-cert-store key extract-cert -o bob-cert.pgp bob.pgp -when I run sq encrypt --recipient-file alice-cert.pgp --signer-file alice.pgp hello.txt -o x.pgp +when I run sq --no-cert-store encrypt --recipient-file alice-cert.pgp --signer-file alice.pgp hello.txt -o x.pgp when I try to run sq decrypt --recipient-file alice.pgp -o alice.txt x.pgp --signer-file bob-cert.pgp then exit code is 1 @@ -1122,18 +1122,18 @@ _Requirement: We can certify a user identity on a key._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key extract-cert alice.pgp -o alice-cert.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq key extract-cert bob.pgp -o bob-cert.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key extract-cert alice.pgp -o alice-cert.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store key extract-cert bob.pgp -o bob-cert.pgp -when I run sq inspect bob-cert.pgp +when I run sq --no-cert-store inspect bob-cert.pgp then stdout doesn't contain "Certifications:" -when I run sq certify alice.pgp bob-cert.pgp Bob -o cert.pgp +when I run sq --no-cert-store certify alice.pgp bob-cert.pgp Bob -o cert.pgp then file cert.pgp contains "-----BEGIN PGP PUBLIC KEY BLOCK-----" then file cert.pgp contains "-----END PGP PUBLIC KEY BLOCK-----" -when I run sq inspect cert.pgp +when I run sq --no-cert-store inspect cert.pgp then stdout contains "Certifications: 1," ~~~ @@ -1144,18 +1144,18 @@ _Requirement: We can certify a user identity on a key._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key extract-cert alice.pgp -o alice-cert.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq key extract-cert bob.pgp -o bob-cert.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key extract-cert alice.pgp -o alice-cert.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store key extract-cert bob.pgp -o bob-cert.pgp -when I run sq inspect bob-cert.pgp +when I run sq --no-cert-store inspect bob-cert.pgp then stdout doesn't contain "Certifications:" -when I run sq certify alice.pgp bob-cert.pgp Bob -o cert.pgp --binary +when I run sq --no-cert-store certify alice.pgp bob-cert.pgp Bob -o cert.pgp --binary when I run cat cert.pgp then stdout doesn't contain "-----BEGIN PGP PUBLIC KEY BLOCK-----" -when I run sq inspect cert.pgp +when I run sq --no-cert-store inspect cert.pgp then stdout contains "Certifications: 1," ~~~ @@ -1175,8 +1175,8 @@ stdout in ASCII armor form._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export key.pgp -when I run sq sign --signer-file key.pgp hello.txt +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store sign --signer-file key.pgp hello.txt then stdout contains "-----BEGIN PGP MESSAGE-----" then stdout contains "-----END PGP MESSAGE-----" ~~~ @@ -1189,8 +1189,8 @@ stdout in binary form._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export key.pgp -when I run sq sign --signer-file key.pgp hello.txt --binary +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store sign --signer-file key.pgp hello.txt --binary then stdout doesn't contain "-----BEGIN PGP MESSAGE-----" then stdout doesn't contain "-----END PGP MESSAGE-----" ~~~ @@ -1203,8 +1203,8 @@ file._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export key.pgp -when I run sq sign --signer-file key.pgp hello.txt -o signed.txt +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store sign --signer-file key.pgp hello.txt -o signed.txt then file signed.txt contains "-----BEGIN PGP MESSAGE-----" then file signed.txt contains "-----END PGP MESSAGE-----" ~~~ @@ -1216,10 +1216,10 @@ _Requirement: We can sign a file and verify the signature._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export key.pgp -when I run sq key extract-cert key.pgp -o cert.pgp -when I run sq sign --signer-file key.pgp hello.txt -o signed.txt -when I run sq verify --signer-file cert.pgp signed.txt +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert key.pgp -o cert.pgp +when I run sq --no-cert-store sign --signer-file key.pgp hello.txt -o signed.txt +when I run sq --no-cert-store verify --signer-file cert.pgp signed.txt then stdout contains "hello, world" ~~~ @@ -1235,19 +1235,19 @@ not enough, when we need two. ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key extract-cert alice.pgp -o alice-cert.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq key extract-cert bob.pgp -o bob-cert.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key extract-cert alice.pgp -o alice-cert.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store key extract-cert bob.pgp -o bob-cert.pgp -when I run sq sign --signer-file alice.pgp hello.txt -o signed1.txt +when I run sq --no-cert-store sign --signer-file alice.pgp hello.txt -o signed1.txt when I try to run sq verify --signer-file alice-cert.pgp --signer-file bob-cert.pgp --signatures=2 signed1.txt then exit code is 1 -when I run sq sign --append --signer-file bob.pgp signed1.txt -o signed2.txt -when I run sq verify --signer-file alice-cert.pgp --signer-file bob-cert.pgp --signatures=1 signed2.txt +when I run sq --no-cert-store sign --append --signer-file bob.pgp signed1.txt -o signed2.txt +when I run sq --no-cert-store verify --signer-file alice-cert.pgp --signer-file bob-cert.pgp --signatures=1 signed2.txt then stdout contains "hello, world" -when I run sq verify --signer-file alice-cert.pgp --signer-file bob-cert.pgp --signatures=2 signed2.txt +when I run sq --no-cert-store verify --signer-file alice-cert.pgp --signer-file bob-cert.pgp --signatures=2 signed2.txt then stdout contains "hello, world" ~~~ @@ -1265,9 +1265,9 @@ the file by definition can't be valid anymore. given an installed sq given file hello.txt given file sed-in-place -when I run sq key generate --export key.pgp -when I run sq key extract-cert key.pgp -o cert.pgp -when I run sq sign --signer-file key.pgp hello.txt -o signed.txt +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert key.pgp -o cert.pgp +when I run sq --no-cert-store sign --signer-file key.pgp hello.txt -o signed.txt when I run bash sed-in-place 3d signed.txt when I try to run sq verify --signer-file cert.pgp signed.txt then command fails @@ -1291,14 +1291,14 @@ included in a readable form._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export key.pgp -when I run sq key extract-cert key.pgp -o cert.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert key.pgp -o cert.pgp -when I run sq sign --cleartext-signature --signer-file key.pgp hello.txt -o signed.txt +when I run sq --no-cert-store sign --cleartext-signature --signer-file key.pgp hello.txt -o signed.txt then file signed.txt contains "-----BEGIN PGP SIGNED MESSAGE-----" then file signed.txt contains "hello, world" then file signed.txt contains "-----END PGP SIGNATURE-----" -when I run sq verify --signer-file cert.pgp signed.txt +when I run sq --no-cert-store verify --signer-file cert.pgp signed.txt then stdout contains "hello, world" ~~~ @@ -1312,10 +1312,10 @@ verified._ given an installed sq given file hello.txt given file sed-in-place -when I run sq key generate --export key.pgp -when I run sq key extract-cert key.pgp -o cert.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert key.pgp -o cert.pgp -when I run sq sign --cleartext-signature --signer-file key.pgp hello.txt -o signed.txt +when I run sq --no-cert-store sign --cleartext-signature --signer-file key.pgp hello.txt -o signed.txt when I run bash sed-in-place s/hello/HELLO/ signed.txt when I try to run sq verify --signer-file cert.pgp signed.txt then exit code is 1 @@ -1329,13 +1329,13 @@ data it signs._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --export key.pgp -when I run sq key extract-cert key.pgp -o cert.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert key.pgp -o cert.pgp -when I run sq sign --detached --signer-file key.pgp hello.txt -o sig.txt +when I run sq --no-cert-store sign --detached --signer-file key.pgp hello.txt -o sig.txt then file sig.txt contains "-----BEGIN PGP SIGNATURE-----" then file sig.txt contains "-----END PGP SIGNATURE-----" -when I run sq verify --detached=sig.txt --signer-file=cert.pgp hello.txt +when I run sq --no-cert-store verify --detached=sig.txt --signer-file=cert.pgp hello.txt then stdout doesn't contain "hello, world" then exit code is 0 ~~~ @@ -1350,10 +1350,10 @@ modified, the signature can't be verified._ given an installed sq given file hello.txt given file sed-in-place -when I run sq key generate --export key.pgp -when I run sq key extract-cert key.pgp -o cert.pgp +when I run sq --no-cert-store key generate --export key.pgp +when I run sq --no-cert-store key extract-cert key.pgp -o cert.pgp -when I run sq sign --detached --signer-file key.pgp hello.txt -o sig.txt +when I run sq --no-cert-store sign --detached --signer-file key.pgp hello.txt -o sig.txt when I run bash sed-in-place s/hello/HELLO/ hello.txt when I try to run sq verify --detached=sig.txt --signer-file=cert.pgp hello.txt then exit code is 1 @@ -1368,14 +1368,14 @@ message._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key extract-cert alice.pgp -o alice-cert.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq key extract-cert bob.pgp -o bob-cert.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key extract-cert alice.pgp -o alice-cert.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store key extract-cert bob.pgp -o bob-cert.pgp -when I run sq sign --signer-file alice.pgp hello.txt -o signed1.txt -when I run sq sign --signer-file bob.pgp --append signed1.txt -o signed2.txt -when I run sq verify signed2.txt --signer-file alice-cert.pgp --signer-file bob-cert.pgp +when I run sq --no-cert-store sign --signer-file alice.pgp hello.txt -o signed1.txt +when I run sq --no-cert-store sign --signer-file bob.pgp --append signed1.txt -o signed2.txt +when I run sq --no-cert-store verify signed2.txt --signer-file alice-cert.pgp --signer-file bob-cert.pgp then stdout contains "hello, world" then stderr contains "2 good signatures" ~~~ @@ -1388,15 +1388,15 @@ twice separately._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key extract-cert alice.pgp -o alice-cert.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq key extract-cert bob.pgp -o bob-cert.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key extract-cert alice.pgp -o alice-cert.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store key extract-cert bob.pgp -o bob-cert.pgp -when I run sq sign --signer-file alice.pgp hello.txt -o signed1.txt -when I run sq sign --signer-file bob.pgp hello.txt -o signed2.txt -when I run sq sign --merge=signed2.txt signed1.txt -o merged.txt -when I run sq verify merged.txt --signer-file alice-cert.pgp --signer-file bob-cert.pgp +when I run sq --no-cert-store sign --signer-file alice.pgp hello.txt -o signed1.txt +when I run sq --no-cert-store sign --signer-file bob.pgp hello.txt -o signed2.txt +when I run sq --no-cert-store sign --merge=signed2.txt signed1.txt -o merged.txt +when I run sq --no-cert-store verify merged.txt --signer-file alice-cert.pgp --signer-file bob-cert.pgp then stdout contains "hello, world" then stderr contains "2 good signatures" ~~~ @@ -1409,14 +1409,14 @@ signatures, as if as a notary._ ~~~scenario given an installed sq given file hello.txt -when I run sq key generate --userid Alice --export alice.pgp -when I run sq key extract-cert alice.pgp -o alice-cert.pgp -when I run sq key generate --userid Bob --export bob.pgp -when I run sq key extract-cert bob.pgp -o bob-cert.pgp +when I run sq --no-cert-store key generate --userid Alice --export alice.pgp +when I run sq --no-cert-store key extract-cert alice.pgp -o alice-cert.pgp +when I run sq --no-cert-store key generate --userid Bob --export bob.pgp +when I run sq --no-cert-store key extract-cert bob.pgp -o bob-cert.pgp -when I run sq sign --signer-file alice.pgp hello.txt -o signed.txt -when I run sq sign --signer-file bob.pgp --notarize signed.txt -o notarized.txt -when I run sq verify notarized.txt --signer-file alice-cert.pgp --signer-file bob-cert.pgp +when I run sq --no-cert-store sign --signer-file alice.pgp hello.txt -o signed.txt +when I run sq --no-cert-store sign --signer-file bob.pgp --notarize signed.txt -o notarized.txt +when I run sq --no-cert-store verify notarized.txt --signer-file alice-cert.pgp --signer-file bob-cert.pgp then stdout contains "hello, world" then stderr contains "Good level 1 notarization from" then stderr contains "2 good signatures" @@ -1438,7 +1438,7 @@ stdout._ ~~~scenario given an installed sq given file hello.txt -when I run sq armor hello.txt +when I run sq --no-cert-store armor hello.txt then stdout contains "-----BEGIN PGP ARMORED FILE-----" then stdout contains "-----END PGP ARMORED FILE-----" ~~~ @@ -1452,7 +1452,7 @@ named file._ given an installed sq given file hello.txt given file hello.asc -when I run sq armor hello.txt -o hello.out +when I run sq --no-cert-store armor hello.txt -o hello.out then files hello.asc and hello.out match ~~~ @@ -1465,17 +1465,17 @@ the label we choose._ ~~~scenario given an installed sq given file hello.txt -when I run sq armor hello.txt --label auto +when I run sq --no-cert-store armor hello.txt --label auto then stdout contains "-----BEGIN PGP ARMORED FILE-----" -when I run sq armor hello.txt --label message +when I run sq --no-cert-store armor hello.txt --label message then stdout contains "-----BEGIN PGP MESSAGE-----" -when I run sq armor hello.txt --label cert +when I run sq --no-cert-store armor hello.txt --label cert then stdout contains "-----BEGIN PGP PUBLIC KEY BLOCK-----" -when I run sq armor hello.txt --label key +when I run sq --no-cert-store armor hello.txt --label key then stdout contains "-----BEGIN PGP PRIVATE KEY BLOCK-----" -when I run sq armor hello.txt --label sig +when I run sq --no-cert-store armor hello.txt --label sig then stdout contains "-----BEGIN PGP SIGNATURE-----" -when I run sq armor hello.txt --label file +when I run sq --no-cert-store armor hello.txt --label file then stdout contains "-----BEGIN PGP ARMORED FILE-----" ~~~ @@ -1487,7 +1487,7 @@ stdout._ ~~~scenario given an installed sq given file hello.asc -when I run sq dearmor hello.asc +when I run sq --no-cert-store dearmor hello.asc then stdout contains "hello, world" ~~~ @@ -1500,7 +1500,7 @@ a named file._ given an installed sq given file hello.txt given file hello.asc -when I run sq dearmor hello.asc -o hello.out +when I run sq --no-cert-store dearmor hello.asc -o hello.out then files hello.txt and hello.out match ~~~ @@ -1512,8 +1512,8 @@ back._ ~~~scenario given an installed sq given file hello.txt -when I run sq armor hello.txt -o hello.tmp -when I run sq dearmor hello.tmp -o hello.out +when I run sq --no-cert-store armor hello.txt -o hello.tmp +when I run sq --no-cert-store dearmor hello.tmp -o hello.out then files hello.txt and hello.out match ~~~ @@ -1546,10 +1546,10 @@ email address, and a subdirectory named after the email domain. ~~~scenario given an installed sq -when I run sq wkd url me@example.com +when I run sq --no-cert-store wkd url me@example.com then stdout contains "https://openpgpkey.example.com/.well-known/openpgpkey/example.com/hu/s8y7oh5xrdpu9psba3i5ntk64ohouhga?l=me" -when I run sq --output-format=json wkd url me@example.com +when I run sq --no-cert-store --output-format=json wkd url me@example.com then stdout, as JSON, matches pattern wkd.json ~~~ @@ -1573,10 +1573,10 @@ The direct URL lacks the subdomain and subdirectory of an advanced one. ~~~scenario given an installed sq -when I run sq wkd direct-url me@example.com +when I run sq --no-cert-store wkd direct-url me@example.com then stdout contains "https://example.com/.well-known/openpgpkey/hu/s8y7oh5xrdpu9psba3i5ntk64ohouhga?l=me" -when I run sq --output-format=json wkd url me@example.com +when I run sq --no-cert-store --output-format=json wkd url me@example.com then stdout, as JSON, matches pattern wkd.json ~~~ @@ -1590,9 +1590,9 @@ email address, and a subdirectory named after the email domain. ~~~scenario given an installed sq -when I run sq wkd url Joe.Doe@Example.ORG +when I run sq --no-cert-store wkd url Joe.Doe@Example.ORG then stdout contains "https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe" -when I run sq wkd direct-url Joe.Doe@Example.ORG +when I run sq --no-cert-store wkd direct-url Joe.Doe@Example.ORG then stdout contains "https://example.org/.well-known/openpgpkey/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe" ~~~ diff --git a/src/commands/certify.rs b/src/commands/certify.rs index 1a1c62d0..25ec0838 100644 --- a/src/commands/certify.rs +++ b/src/commands/certify.rs @@ -3,6 +3,7 @@ use std::time::{SystemTime, Duration}; use anyhow::Context; use sequoia_openpgp as openpgp; +use openpgp::KeyHandle; use openpgp::Result; use openpgp::cert::prelude::*; use openpgp::packet::prelude::*; @@ -10,6 +11,7 @@ use openpgp::packet::signature::subpacket::NotationDataFlags; use openpgp::parse::Parse; use openpgp::serialize::Serialize; use openpgp::types::SignatureType; +use openpgp::types::KeyFlags; use crate::Config; use crate::parse_duration; @@ -29,7 +31,13 @@ pub fn certify(config: Config, c: certify::Command) let certifier = Cert::from_file(certifier)?; let private_key_store = c.private_key_store; - let cert = Cert::from_file(cert)?; + // XXX: Change this interface: it's dangerous to guess whether an + // identifier is a file or a key handle. + let cert = if let Ok(kh) = cert.parse::() { + config.lookup_one(&kh, Some(KeyFlags::empty().set_certification()), true)? + } else { + Cert::from_file(cert)? + }; let trust_depth: u8 = c.depth; let trust_amount: u8 = c.amount; diff --git a/src/commands/decrypt.rs b/src/commands/decrypt.rs index 2e45c544..bb0a97be 100644 --- a/src/commands/decrypt.rs +++ b/src/commands/decrypt.rs @@ -89,8 +89,8 @@ impl PrivateKey for RemotePrivateKey { } } -struct Helper<'a> { - vhelper: VHelper<'a>, +struct Helper<'a, 'certdb> { + vhelper: VHelper<'a, 'certdb>, secret_keys: HashMap>, key_identities: HashMap, key_hints: HashMap, @@ -99,8 +99,8 @@ struct Helper<'a> { dumper: Option, } -impl<'a> Helper<'a> { - fn new(config: &Config<'a>, private_key_store: Option<&str>, +impl<'a, 'certdb> Helper<'a, 'certdb> { + fn new(config: &'a Config<'certdb>, private_key_store: Option<&str>, signatures: usize, certs: Vec, secrets: Vec, session_keys: Vec, dump_session_key: bool, dump: bool) @@ -182,7 +182,7 @@ impl<'a> Helper<'a> { } } -impl<'a> VerificationHelper for Helper<'a> { +impl<'a, 'certdb> VerificationHelper for Helper<'a, 'certdb> { fn inspect(&mut self, pp: &PacketParser) -> Result<()> { if let Some(dumper) = self.dumper.as_mut() { dumper.packet(&mut io::stderr(), @@ -201,7 +201,7 @@ impl<'a> VerificationHelper for Helper<'a> { } } -impl<'a> DecryptionHelper for Helper<'a> { +impl<'a, 'certdb> DecryptionHelper for Helper<'a, 'certdb> { fn decrypt(&mut self, pkesks: &[PKESK], skesks: &[SKESK], sym_algo: Option, mut decrypt: D) -> openpgp::Result> diff --git a/src/commands/export.rs b/src/commands/export.rs new file mode 100644 index 00000000..36ad481c --- /dev/null +++ b/src/commands/export.rs @@ -0,0 +1,189 @@ +use std::collections::HashSet; + +use anyhow::Context; + +use sequoia_openpgp as openpgp; +use openpgp::{ + armor, + Result, + serialize::Serialize, +}; + +use sequoia_cert_store as cert_store; +use cert_store::Store; +use cert_store::store::UserIDQueryParams; + +use crate::{ + Config, + print_error_chain, +}; + +use crate::sq_cli::export; + +pub fn dispatch(config: Config, cmd: export::Command) -> Result<()> { + let cert_store = config.cert_store_or_else()?; + + let mut userid_query = Vec::new(); + let mut die = false; + + for userid in cmd.userid.into_iter() { + let q = UserIDQueryParams::new(); + userid_query.push((q, userid)); + } + + for pattern in cmd.grep.into_iter() { + let mut q = UserIDQueryParams::new(); + q.set_anchor_start(false) + .set_anchor_end(false) + .set_ignore_case(true); + userid_query.push((q, pattern)); + } + + for email in cmd.email.into_iter() { + match UserIDQueryParams::is_email(&email) { + Ok(email) => { + let mut q = UserIDQueryParams::new(); + q.set_email(true); + userid_query.push((q, email)); + } + Err(err) => { + let err = err.context(format!( + "Invalid value for --email: {:?}", email)); + print_error_chain(&err); + die = true; + } + } + } + + for domain in cmd.domain.into_iter() { + match UserIDQueryParams::is_domain(&domain) { + Ok(domain) => { + let mut q = UserIDQueryParams::new(); + q.set_email(true) + .set_anchor_start(false); + userid_query.push((q, format!("@{}", domain))); + } + Err(err) => { + let err = err.context(format!( + "Invalid value for --domain: {:?}", domain)); + print_error_chain(&err); + die = true; + } + } + } + + if die { + return Err(anyhow::anyhow!("Invalid arguments.")); + } + + let mut sink = config.create_or_stdout_pgp( + None, cmd.binary, armor::Kind::PublicKey)?; + + let mut exported_something = false; + + if cmd.cert.is_empty() && cmd.key.is_empty() && userid_query.is_empty() { + // Export everything. + for cert in cert_store.certs() { + // Turn parse errors into warnings: we want users to be + // able to recover as much of their data as possible. + let result = cert.to_cert() + .with_context(|| { + format!("Parsing {} from certificate directory", + cert.fingerprint()) + }); + match result { + Ok(cert) => cert.export(&mut sink)?, + Err(err) => { + print_error_chain(&err); + continue; + } + } + } + + // If we have nothing and we export nothing, that is fine. + exported_something = true; + } else { + // There are two possible approaches when there are multiple + // search criteria: we iterate overall the certificates and + // check each one individually, or we execute each query and + // merge the results. The former makes more sense when most + // of the certificates will be selected, but that is rarely + // the case in practice. Further, some backends, like the + // KeyServer backend, don't support iteration. So, we execute + // each query and merge the results. + + let mut exported = HashSet::new(); + + for kh in cmd.cert.iter() { + if let Ok(certs) = cert_store.lookup_by_cert(kh) { + for cert in certs { + if exported.insert(cert.fingerprint()) { + exported_something = true; + cert.export(&mut sink)?; + } + } + } + } + + for kh in cmd.key.iter() { + if let Ok(certs) = cert_store.lookup_by_key(kh) { + for cert in certs { + if exported.get(&cert.fingerprint()).is_some() { + // Already exported this one. + continue; + } + + if cert.key_handle().aliases(kh) { + // When matching the primary key, we don't + // need a valid self signature. + exported_something = true; + cert.export(&mut sink)?; + exported.insert(cert.fingerprint()); + } else { + // But, when matching a subkey, we do. + if let Ok(vc) = cert.with_policy(&config.policy, None) { + if vc.keys().subkeys().any(|ka| { + ka.key_handle().aliases(kh) + }) + { + exported_something = true; + cert.export(&mut sink)?; + exported.insert(cert.fingerprint()); + } + } + } + } + } + } + + for (q, pattern) in userid_query.iter() { + if let Ok(certs) = cert_store.select_userid(q, pattern) { + for cert in certs { + if exported.get(&cert.fingerprint()).is_some() { + // Already exported this one. + continue; + } + + // Matching User IDs need a valid self signature. + if let Ok(vc) = cert.with_policy(&config.policy, None) { + if vc.userids().any(|ua| { + q.check(ua.userid(), pattern) + }) { + exported_something = true; + cert.export(&mut sink)?; + exported.insert(cert.fingerprint()); + } + } + } + } + } + } + + sink.finalize().context("Failed to export certificates")?; + + if exported_something { + Ok(()) + } else { + Err(anyhow::anyhow!("Search terms did not match any certificates")) + } +} diff --git a/src/commands/import.rs b/src/commands/import.rs new file mode 100644 index 00000000..3f6b5899 --- /dev/null +++ b/src/commands/import.rs @@ -0,0 +1,69 @@ +use std::borrow::Cow; + +use sequoia_openpgp as openpgp; +use openpgp::{ + cert::raw::RawCertParser, + Result, + parse::Parse, +}; + +use sequoia_cert_store as cert_store; +use cert_store::LazyCert; +use cert_store::StoreUpdate; + +use crate::{ + Config, + open_or_stdin, +}; + +use crate::sq_cli::import; + +pub fn dispatch<'store>(mut config: Config<'store>, cmd: import::Command) + -> Result<()> +{ + let inputs = if cmd.input.is_empty() { + vec![ "-".to_string() ] + } else { + cmd.input + }; + + let mut stats = cert_store::store::MergePublicCollectStats::new(); + + let inner = || -> Result<()> { + for input in inputs.into_iter() { + let input = open_or_stdin( + if input == "-" { None } else { Some(&input) })?; + let raw_certs = RawCertParser::from_reader(input)?; + + let cert_store = config.cert_store_mut_or_else()?; + + for raw_cert in raw_certs { + let cert = match raw_cert { + Ok(raw_cert) => LazyCert::from(raw_cert), + Err(err) => { + eprintln!("Error parsing input: {}", err); + stats.errors += 1; + continue; + } + }; + + let fingerprint = cert.fingerprint(); + if let Err(err) = cert_store.update_by(Cow::Owned(cert), &mut stats) { + eprintln!("Error importing {}: {}", fingerprint, err); + stats.errors += 1; + continue; + } + } + } + + Ok(()) + }; + + let result = inner(); + + eprintln!("Imported {} new certificates, updated {} certificates, \ + {} certificates unchanged, {} errors.", + stats.new, stats.updated, stats.unchanged, stats.errors); + + Ok(result?) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 8c23366d..fc5c5360 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -32,6 +32,9 @@ use openpgp::policy::Policy; use openpgp::types::KeyFlags; use openpgp::types::RevocationStatus; +use sequoia_cert_store as cert_store; +use cert_store::Store; + use crate::{ Config, }; @@ -55,6 +58,8 @@ pub mod key; pub mod merge_signatures; pub use self::merge_signatures::merge_signatures; pub mod keyring; +pub mod import; +pub mod export; pub mod net; pub mod certify; @@ -440,9 +445,9 @@ pub fn encrypt(opts: EncryptOpts) -> Result<()> { Ok(()) } -struct VHelper<'a> { +struct VHelper<'a, 'store> { #[allow(dead_code)] - config: Config<'a>, + config: &'a Config<'store>, signatures: usize, certs: Option>, labels: HashMap, @@ -455,12 +460,12 @@ struct VHelper<'a> { broken_signatures: usize, } -impl<'a> VHelper<'a> { - fn new(config: &Config<'a>, signatures: usize, +impl<'a, 'store> VHelper<'a, 'store> { + fn new(config: &'a Config<'store>, signatures: usize, certs: Vec) -> Self { VHelper { - config: config.clone(), + config: config, signatures, certs: Some(certs), labels: HashMap::new(), @@ -571,9 +576,9 @@ impl<'a> VHelper<'a> { } } -impl<'a> VerificationHelper for VHelper<'a> { - fn get_certs(&mut self, _ids: &[openpgp::KeyHandle]) -> Result> { - let certs = self.certs.take().unwrap(); +impl<'a, 'store> VerificationHelper for VHelper<'a, 'store> { + fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> Result> { + let mut certs = self.certs.take().unwrap(); // Get all keys. let seen: HashSet<_> = certs.iter() .flat_map(|cert| { @@ -583,6 +588,21 @@ impl<'a> VerificationHelper for VHelper<'a> { // Explicitly provided keys are trusted. self.trusted = seen; + // Look up the ids in the certificate store. + + // Avoid initializing the certificate store if we don't actually + // need to. + if ! ids.is_empty() { + if let Ok(Some(cert_store)) = self.config.cert_store() { + for id in ids.iter() { + if let Ok(c) = cert_store.lookup_by_key(id) { + certs.extend( + c.into_iter().filter_map(|c| c.as_cert().ok())); + } + } + } + } + Ok(certs) } diff --git a/src/commands/sign.rs b/src/commands/sign.rs index 5c0daf70..c5f2d89e 100644 --- a/src/commands/sign.rs +++ b/src/commands/sign.rs @@ -23,8 +23,8 @@ use crate::{ Config, }; -pub struct SignOpts<'a> { - pub config: Config<'a>, +pub struct SignOpts<'a, 'certdb> { + pub config: Config<'certdb>, pub private_key_store: Option<&'a str>, pub input: &'a mut (dyn io::Read + Sync + Send), pub output_path: Option<&'a str>, diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 00000000..8da45983 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,14 @@ +macro_rules! platform { + { unix => { $($unix:tt)* }, windows => { $($windows:tt)* }, } => { + if cfg!(unix) { + #[cfg(unix)] { $($unix)* } + #[cfg(not(unix))] { unreachable!() } + } else if cfg!(windows) { + #[cfg(windows)] { $($windows)* } + #[cfg(not(windows))] { unreachable!() } + } else { + #[cfg(not(any(unix, windows)))] compile_error!("Unsupported platform"); + unreachable!() + } + } +} diff --git a/src/sq.rs b/src/sq.rs index a838e7a1..f31c4328 100644 --- a/src/sq.rs +++ b/src/sq.rs @@ -5,6 +5,8 @@ #![doc = include_str!(concat!(env!("OUT_DIR"), "/sq-usage.md"))] use anyhow::Context as _; + +use std::borrow::Borrow; use std::fs::OpenOptions; use std::io; use std::path::{Path, PathBuf}; @@ -12,11 +14,13 @@ use std::str::FromStr; use std::time::Duration; use chrono::{DateTime, offset::Utc}; use itertools::Itertools; +use once_cell::unsync::OnceCell; use buffered_reader::{BufferedReader, Dup, File, Generic, Limitor}; use sequoia_openpgp as openpgp; use openpgp::{ + KeyHandle, Result, }; use openpgp::{armor, Cert}; @@ -28,11 +32,18 @@ use openpgp::packet::signature::subpacket::NotationDataFlags; use openpgp::serialize::{Serialize, stream::{Message, Armorer}}; use openpgp::cert::prelude::*; use openpgp::policy::StandardPolicy as P; +use openpgp::types::KeyFlags; + +use sequoia_cert_store as cert_store; +use cert_store::Store; +use cert_store::store::StoreError; use clap::FromArgMatches; use crate::sq_cli::packet; use sq_cli::SqSubcommands; +#[macro_use] mod macros; + mod sq_cli; mod man; mod commands; @@ -43,8 +54,9 @@ pub use output::{wkd::WkdUrlVariant, Model, OutputFormat, OutputVersion}; fn open_or_stdin(f: Option<&str>) -> Result>> { match f { - Some(f) => Ok(Box::new(File::open(f) - .context("Failed to open input file")?)), + Some(f) => Ok(Box::new( + File::open(f) + .with_context(|| format!("Failed to open {}", f))?)), None => Ok(Box::new(Generic::new(io::stdin(), None))), } } @@ -307,7 +319,6 @@ fn emit_unstable_cli_warning() { Use with caution in scripts.\n"); } -#[derive(Clone)] pub struct Config<'a> { force: bool, output_format: OutputFormat, @@ -315,9 +326,11 @@ pub struct Config<'a> { policy: P<'a>, /// Have we emitted the warning yet? unstable_cli_warning_emitted: bool, + cert_store_path: Option, + cert_store: Option>>, } -impl Config<'_> { +impl<'store> Config<'store> { /// Opens the file (or stdout) for writing data that is safe for /// non-interactive use. /// @@ -385,6 +398,255 @@ impl Config<'_> { } } } + + /// Returns the cert store. + /// + /// If the cert store is disabled, returns `Ok(None)`. If it is not yet + /// open, opens it. + fn cert_store(&self) -> Result>> { + let cert_store = if let Some(cert_store) = self.cert_store.as_ref() { + cert_store + } else { + // The cert store is disabled. + return Ok(None); + }; + + if let Some(cert_store) = cert_store.get() { + // The cert store is already initialized, return it. + return Ok(Some(cert_store)); + } + + let create_dirs = |path: &Path| -> Result<()> { + use std::fs::DirBuilder; + + let mut b = DirBuilder::new(); + b.recursive(true); + + // Create the parent with the normal umask. + if let Some(parent) = path.parent() { + // Note: since recursive is turned on, it is not an + // error if the directory exists, which is exactly + // what we want. + b.create(parent) + .with_context(|| { + format!("Creating the directory {:?}", parent) + })?; + } + + // Create path with more restrictive permissions. + platform!{ + unix => { + use std::os::unix::fs::DirBuilderExt; + b.mode(0o700); + }, + windows => { + }, + } + + b.create(path) + .with_context(|| { + format!("Creating the directory {:?}", path) + })?; + + Ok(()) + }; + + // We need to initialize the cert store. + let pathbuf; + let path = if let Some(path) = self.cert_store_path.as_ref() { + path + } else { + // XXX: openpgp-cert-d doesn't yet export this: + // https://gitlab.com/sequoia-pgp/pgp-cert-d/-/issues/34 + // Remove this when it does. + pathbuf = dirs::data_dir() + .expect("Unsupported platform") + .join("pgp.cert.d"); + &pathbuf + }; + + let instance = create_dirs(path) + .and_then(|_| cert_store::CertStore::open(path)) + .with_context(|| { + format!("While opening the certificate store at {:?}", + path) + })?; + + let _ = cert_store.set(instance); + Ok(Some(self.cert_store + .as_ref().expect("enabled") + .get().expect("just configured"))) + } + + /// Returns the cert store. + /// + /// If the cert store is disabled, returns an error. + fn cert_store_or_else(&self) -> Result<&cert_store::CertStore<'store>> { + self.cert_store().and_then(|cert_store| cert_store.ok_or_else(|| { + anyhow::anyhow!("Operation requires a certificate store, \ + but the certificate store is disabled") + })) + } + + /// Returns a mutable reference to the cert store. + /// + /// If the cert store is disabled, returns None. If it is not yet + /// open, opens it. + fn cert_store_mut(&mut self) + -> Result>> + { + // self.cert_store() will do any required initialization, but + // it will return an immutable reference. + self.cert_store()?; + + if let Some(cert_store) = self.cert_store.as_mut() { + Ok(cert_store.get_mut()) + } else { + Ok(None) + } + } + + /// Returns a mutable reference to the cert store. + /// + /// If the cert store is disabled, returns an error. + fn cert_store_mut_or_else(&mut self) -> Result<&mut cert_store::CertStore<'store>> { + self.cert_store_mut().and_then(|cert_store| cert_store.ok_or_else(|| { + anyhow::anyhow!("Operation requires a certificate store, \ + but the certificate store is disabled") + })) + } + + /// Looks up an identifier. + /// + /// This matches on both the primary key and the subkeys. + /// + /// If `keyflags` is not `None`, then only returns certificates + /// where the matching key has at least one of the specified key + /// flags. If `or_by_primary` is set, then certificates with the + /// specified key handle and a subkey with the specified flags + /// also match. + /// + /// If `allow_ambiguous` is true, then all matching certificates + /// are returned. Otherwise, if an identifier matches multiple + /// certificates an error is returned. + /// + /// An error is also returned if any of the identifiers does not + /// match at least one certificate. + fn lookup<'a, I>(&self, khs: I, + keyflags: Option, + or_by_primary: bool, + allow_ambiguous: bool) + -> Result> + where I: IntoIterator, + I::Item: Borrow, + { + let mut results = Vec::new(); + + for kh in khs { + let kh = kh.borrow(); + match self.cert_store_or_else()?.lookup_by_key(&kh) { + Err(err) => { + let err = anyhow::Error::from(err); + return Err(err.context( + format!("Failed to load {} from certificate store", kh) + )); + } + Ok(certs) => { + let mut certs = certs.into_iter() + .filter_map(|cert| { + match cert.as_cert() { + Ok(cert) => Some(cert), + Err(err) => { + let err = err.context( + format!("Failed to parse {} as loaded \ + from certificate store", kh)); + print_error_chain(&err); + None + } + } + }) + .collect::>(); + + if let Some(keyflags) = keyflags.as_ref() { + certs.retain(|cert| { + // XXX: Respect any subcommand-specific + // reference time. + let vc = match cert.with_policy(&self.policy, None) { + Ok(vc) => vc, + Err(err) => { + let err = err.context( + format!("{} is not valid according \ + to the current policy, ignoring", + kh)); + print_error_chain(&err); + return false; + } + }; + + let checked_id = or_by_primary + && vc.key_handle().aliases(kh); + + for ka in vc.keys() { + if checked_id || ka.key_handle().aliases(kh) { + if &ka.key_flags().unwrap_or(KeyFlags::empty()) + & keyflags + != KeyFlags::empty() + { + return true; + } + } + } + + if checked_id { + eprintln!("Error: {} does not have a key with \ + the required capabilities ({:?})", + cert.keyid(), keyflags); + } else { + eprintln!("Error: The subkey {} (cert: {}) \ + does not the required capabilities \ + ({:?})", + kh, cert.keyid(), keyflags); + } + return false; + }) + } + + if ! allow_ambiguous && certs.len() > 1 { + return Err(anyhow::anyhow!( + "{} is ambiguous; it matches: {}", + kh, + certs.into_iter() + .map(|cert| cert.fingerprint().to_string()) + .collect::>() + .join(", "))); + } + + if certs.len() == 0 { + return Err(StoreError::NotFound(kh.clone()).into()); + } + + results.extend(certs); + } + } + } + + Ok(results) + } + + /// Looks up a certificate. + /// + /// Like `lookup`, but looks up a certificate, which must be + /// uniquely identified by `kh` and `keyflags`. + fn lookup_one(&self, kh: &KeyHandle, + keyflags: Option, or_by_primary: bool) + -> Result + { + self.lookup(std::iter::once(kh), keyflags, or_by_primary, false) + .map(|certs| { + assert_eq!(certs.len(), 1); + certs.into_iter().next().expect("have one") + }) + } } // TODO: Use `derive`d command structs. No more values_of @@ -425,6 +687,12 @@ fn main() -> Result<()> { output_version, policy: policy.clone(), unstable_cli_warning_emitted: false, + cert_store_path: c.cert_store.clone(), + cert_store: if c.no_cert_store { + None + } else { + Some(OnceCell::new()) + }, }; match c.subcommand { @@ -474,9 +742,16 @@ fn main() -> Result<()> { command.dump, command.hex)?; }, SqSubcommands::Encrypt(command) => { - let recipients = load_certs( - command.recipients_cert_file.iter().map(|s| s.as_ref()), - )?; + let mut recipients = load_certs( + command.recipients_file.iter().map(|s| s.as_ref()))?; + recipients.extend( + config.lookup(command.recipients_cert, + Some(KeyFlags::empty() + .set_storage_encryption() + .set_transport_encryption()), + true, + false) + .context("--recipient-cert")?); let mut input = open_or_stdin(command.io.input.as_deref())?; let output = config.create_or_stdout_pgp( @@ -555,7 +830,14 @@ fn main() -> Result<()> { }; let signatures = command.signatures; // TODO ugly adaptation to load_certs' signature, fix later - let certs = load_certs(command.sender_cert_file.iter().map(|s| s.as_ref()))?; + let mut certs = load_certs( + command.sender_file.iter().map(|s| s.as_ref()))?; + certs.extend( + config.lookup(command.sender_certs, + Some(KeyFlags::empty().set_signing()), + true, + false) + .context("--sender-cert")?); commands::verify(config, &mut input, detached.as_mut().map(|r| r as &mut (dyn io::Read + Sync + Send)), &mut output, signatures, certs)?; @@ -634,6 +916,14 @@ fn main() -> Result<()> { commands::keyring::dispatch(config, command)? }, + SqSubcommands::Import(command) => { + commands::import::dispatch(config, command)? + }, + + SqSubcommands::Export(command) => { + commands::export::dispatch(config, command)? + }, + SqSubcommands::Packet(command) => match command.subcommand { packet::Subcommands::Dump(command) => { let mut input = open_or_stdin(command.io.input.as_deref())?; diff --git a/src/sq_cli/encrypt.rs b/src/sq_cli/encrypt.rs index a90a9495..4804d909 100644 --- a/src/sq_cli/encrypt.rs +++ b/src/sq_cli/encrypt.rs @@ -1,5 +1,8 @@ use clap::{ArgEnum, Parser}; +use sequoia_openpgp as openpgp; +use openpgp::KeyHandle; + use crate::sq_cli::types::{IoArgs, Time}; #[derive(Parser, Debug)] @@ -36,13 +39,20 @@ pub struct Command { help = "Emits binary data", )] pub binary: bool, + #[clap( + long = "recipient-cert", + value_name = "FINGERPRINT|KEYID", + multiple_occurrences = true, + help = "Encrypts to the named certificates", + )] + pub recipients_cert: Vec, #[clap( long = "recipient-file", value_name = "CERT_RING_FILE", multiple_occurrences = true, help = "Encrypts to all certificates in CERT_RING_FILE", )] - pub recipients_cert_file: Vec, + pub recipients_file: Vec, #[clap( long = "signer-file", value_name = "KEY_FILE", diff --git a/src/sq_cli/export.rs b/src/sq_cli/export.rs new file mode 100644 index 00000000..a2841932 --- /dev/null +++ b/src/sq_cli/export.rs @@ -0,0 +1,118 @@ +use clap::Parser; + +use sequoia_openpgp as openpgp; +use openpgp::KeyHandle; + +#[derive(Parser, Debug)] +#[clap( + name = "export", + about = "Exports certificates from the local certificate store", + long_about = +"Exports certificates from the local certificate store + +If multiple predicates are specified a certificate is returned if +at least one of them matches. + +This does not check the authenticity of the certificates in anyway. +Before using the certificates, be sure to validate and authenticate +them. + +When matching on subkeys or User IDs, the component must have a valid +self signature according to the policy. This is not the case when +matching the certificate's key handle using `--cert` or when exporting +all certificates. + +Fails if search criteria are specified and none of them matches any +certificates. Note: this means if the certificate store is empty and +no search criteria are specified, then this will return success.", + after_help = +"EXAMPLES: + +# Exports all certificates. +$ sq export > all.pgp + +# Exports certificates with a matching User ID packet. The binding +# signatures are checked, but the User IDs are not authenticated. +# Note: this check is case sensitive. +$ sq export --userid 'Alice ' + +# Exports certificates with a User ID containing the email address. +# The binding signatures are checked, but the User IDs are not +# authenticated. Note: this check is case insensitive. +$ sq export --email 'alice@example.org' + +# Exports certificates where the certificate (i.e., the primary key) +# has the specified Key ID. +$ sq export --cert 1234567812345678 + +# Exports certificates where the primary key or a subkey matches the +# specified Key ID. +$ sq export --key 1234567812345678 + +# Exports certificates that contain a User ID with *either* (not +# both!) email address. Note: this check is case insensitive. +$ sq export --email alice@example.org --email bob@example.org +", +)] +pub struct Command { + #[clap( + short = 'B', + long, + help = "Emits binary data", + )] + pub binary: bool, + + #[clap( + long = "cert", + value_name = "FINGERPRINT|KEYID", + multiple_occurrences = true, + help = "Returns certificates that \ + have the specified fingerprint or key ID", + )] + pub cert: Vec, + + #[clap( + long = "key", + value_name = "FINGERPRINT|KEYID", + multiple_occurrences = true, + help = "Returns certificates where the primary key or \ + a subkey has the specified fingerprint or key ID", + )] + pub key: Vec, + + #[clap( + long = "userid", + value_name = "USERID", + multiple_occurrences = true, + help = "Returns certificates that have a User ID that \ + matches exactly, including case", + )] + pub userid: Vec, + + #[clap( + long = "grep", + value_name = "PATTERN", + multiple_occurrences = true, + help = "Returns certificates that have a User ID that \ + contains the string, case insensitively", + )] + pub grep: Vec, + + #[clap( + long = "email", + value_name = "EMAIL", + multiple_occurrences = true, + help = "Returns certificates that have a User ID with \ + the specified email address, case insensitively", + )] + pub email: Vec, + + #[clap( + long = "domain", + value_name = "DOMAIN", + multiple_occurrences = true, + help = "Returns certificates that have a User ID with \ + an email address from the specified domain", + )] + pub domain: Vec, +} diff --git a/src/sq_cli/import.rs b/src/sq_cli/import.rs new file mode 100644 index 00000000..75135263 --- /dev/null +++ b/src/sq_cli/import.rs @@ -0,0 +1,20 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[clap( + name = "import", + about = "Imports certificates into the local certificate store", + long_about = +"Imports certificates into the local certificate store +", + after_help = +"EXAMPLES: + +# Imports a certificate. +$ sq import < juliet.pgp +", +)] +pub struct Command { + #[clap(value_name = "FILE", help = "Reads from FILE or stdin if omitted")] + pub input: Vec, +} diff --git a/src/sq_cli/mod.rs b/src/sq_cli/mod.rs index 5221a2ec..4f7d4b59 100644 --- a/src/sq_cli/mod.rs +++ b/src/sq_cli/mod.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + /// Command-line parser for sq. use clap::{Command, CommandFactory, Parser, Subcommand}; @@ -10,6 +12,8 @@ pub mod dane; mod dearmor; mod decrypt; pub mod encrypt; +pub mod export; +pub mod import; pub mod inspect; pub mod key; pub mod keyring; @@ -43,9 +47,10 @@ pub fn build() -> Command<'static> { about = "A command-line frontend for Sequoia, an implementation of OpenPGP", long_about = "A command-line frontend for Sequoia, an implementation of OpenPGP -Functionality is grouped and available using subcommands. Currently, -this interface is completely stateless. Therefore, you need to supply -all configuration and certificates explicitly on each invocation. +Functionality is grouped and available using subcommands. This +interface is not completely stateless. In particular, the user's +default certificate store is used. This can be disabled using +\"--no-cert-store\". OpenPGP data can be provided in binary or ASCII armored form. This will be handled automatically. Emitted OpenPGP data is ASCII armored @@ -67,6 +72,25 @@ pub struct SqCommand { help = "Overwrites existing files" )] pub force: bool, + #[clap( + long, + help = "Disables the use of a certificate store", + long_help = "\ +Disables the use of a certificate store. Normally sq uses the user's \ +standard cert-d, which is located in $HOME/.local/share/pgp.cert.d." + )] + pub no_cert_store: bool, + #[clap( + long, + value_name = "PATH", + conflicts_with_all = &[ "no-cert-store" ], + help = "Specifies the location of the certificate store", + long_help = "\ +Specifies the location of the certificate store. By default, sq uses \ +the OpenPGP certificate directory at `$HOME/.local/share/pgp.cert.d`, \ +and creates it if it does not exist." + )] + pub cert_store: Option, #[clap( long = "output-format", value_name = "FORMAT", @@ -124,6 +148,8 @@ pub enum SqSubcommands { Key(key::Command), Keyring(keyring::Command), + Import(import::Command), + Export(export::Command), Certify(certify::Command), #[cfg(feature = "autocrypt")] diff --git a/src/sq_cli/verify.rs b/src/sq_cli/verify.rs index 48d36698..6756d2f2 100644 --- a/src/sq_cli/verify.rs +++ b/src/sq_cli/verify.rs @@ -1,5 +1,8 @@ use clap::Parser; +use sequoia_openpgp as openpgp; +use openpgp::KeyHandle; + use crate::sq_cli::types::IoArgs; #[derive(Parser, Debug)] @@ -62,11 +65,23 @@ pub struct Command { value_name = "CERT_FILE", help = "Verifies signatures using the certificate in CERT_FILE", )] - // TODO: Should at least one sender_cert_file be required? Verification does not make sense + // TODO: Should at least one sender_file be required? Verification does not make sense // without one, does it? // TODO Use PathBuf instead of String. Path representation is platform dependent, so Rust's // utf-8 Strings are not quite appropriate. // TODO: And adapt load_certs in sq.rs - pub sender_cert_file: Vec, + pub sender_file: Vec, + #[clap( + long = "signer-cert", + value_name = "FINGERPRINT|KEYID", + help = "Verifies signatures using the specified certificate", + long_help = "\ +Verifies signatures using the specified certificate. This reads the +certificate from the certificate store, and considers it to be +authenticated. When this option is not provided, the certificate is +still read from the certificate store, if it exists, but it is not +considered authenticated." + )] + pub sender_certs: Vec, } diff --git a/tests/sq-certify.rs b/tests/sq-certify.rs index b1ed3f3a..44f04e3b 100644 --- a/tests/sq-certify.rs +++ b/tests/sq-certify.rs @@ -12,6 +12,7 @@ use openpgp::cert::prelude::*; use openpgp::KeyHandle; use openpgp::packet::signature::subpacket::NotationData; use openpgp::packet::signature::subpacket::NotationDataFlags; +use openpgp::packet::UserID; use openpgp::parse::Parse; use openpgp::policy::StandardPolicy; use openpgp::serialize::Serialize; @@ -40,6 +41,7 @@ fn sq_certify() -> Result<()> { // A simple certification. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("certify") .arg(alice_pgp.to_str().unwrap()) .arg(bob_pgp.to_str().unwrap()) @@ -74,6 +76,7 @@ fn sq_certify() -> Result<()> { // No expiry. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("certify") .arg(alice_pgp.to_str().unwrap()) .arg(bob_pgp.to_str().unwrap()) @@ -108,6 +111,7 @@ fn sq_certify() -> Result<()> { // Have alice certify bob@example.org for 0xB0B. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("certify") .arg(alice_pgp.to_str().unwrap()) .arg(bob_pgp.to_str().unwrap()) @@ -150,6 +154,7 @@ fn sq_certify() -> Result<()> { // It should fail if the User ID doesn't exist. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("certify") .arg(alice_pgp.to_str().unwrap()) .arg(bob_pgp.to_str().unwrap()) @@ -160,6 +165,7 @@ fn sq_certify() -> Result<()> { // With a notation. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("certify") .args(["--notation", "foo", "bar"]) .args(["--notation", "!foo", "xyzzy"]) @@ -281,7 +287,8 @@ fn sq_certify_creation_time() -> Result<()> // Build up the command line. let mut cmd = Command::cargo_bin("sq")?; - cmd.args(["certify", + cmd.args(["--no-cert-store", + "certify", &alice_pgp.to_string_lossy(), &bob_pgp.to_string_lossy(), bob, "--time", iso8601 ]); @@ -363,7 +370,8 @@ fn sq_certify_with_expired_key() -> Result<()> // Make sure using an expired key fails by default. let mut cmd = Command::cargo_bin("sq")?; - cmd.args(["certify", + cmd.args(["--no-cert-store", + "certify", &alice_pgp.to_string_lossy(), &bob_pgp.to_string_lossy(), bob ]); cmd.assert().failure(); @@ -371,7 +379,8 @@ fn sq_certify_with_expired_key() -> Result<()> // Try again. let mut cmd = Command::cargo_bin("sq")?; - cmd.args(["certify", + cmd.args(["--no-cert-store", + "certify", "--allow-not-alive-certifier", &alice_pgp.to_string_lossy(), &bob_pgp.to_string_lossy(), bob ]); @@ -452,7 +461,8 @@ fn sq_certify_with_revoked_key() -> Result<()> // Make sure using an expired key fails by default. let mut cmd = Command::cargo_bin("sq")?; - cmd.args(["certify", + cmd.args(["--no-cert-store", + "certify", &alice_pgp.to_string_lossy(), &bob_pgp.to_string_lossy(), bob ]); cmd.assert().failure(); @@ -460,7 +470,8 @@ fn sq_certify_with_revoked_key() -> Result<()> // Try again. let mut cmd = Command::cargo_bin("sq")?; - cmd.args(["certify", + cmd.args(["--no-cert-store", + "certify", "--allow-revoked-certifier", &alice_pgp.to_string_lossy(), &bob_pgp.to_string_lossy(), bob ]); @@ -497,3 +508,86 @@ fn sq_certify_with_revoked_key() -> Result<()> Ok(()) } + +// Certify a certificate in the cert store. +#[test] +fn sq_certify_using_cert_store() -> Result<()> +{ + let dir = TempDir::new()?; + + let certd = dir.path().join("cert.d").display().to_string(); + std::fs::create_dir(&certd).expect("mkdir works"); + + let alice_pgp = dir.path().join("alice.pgp").display().to_string(); + let bob_pgp = dir.path().join("bob.pgp").display().to_string(); + + // Generate keys. + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "key", "generate", + "--expires", "never", + "--userid", "", + "--export", &alice_pgp]); + cmd.assert().success(); + + let alice = Cert::from_file(&alice_pgp)?; + + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "key", "generate", + "--expires", "never", + "--userid", "", + "--export", &bob_pgp]); + cmd.assert().success(); + + let bob = Cert::from_file(&bob_pgp)?; + + // Import bob's (but not alice's!). + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "import", &bob_pgp]); + cmd.assert().success(); + + + // Have alice certify bob. + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "certify", &alice_pgp, + &bob.fingerprint().to_string(), + ""]); + + let output = cmd.output().expect("success"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success()); + + // Make sure the certificate on stdout is bob and that alice + // signed it. + let parser = CertParser::from_bytes(stdout.as_bytes()) + .expect("valid"); + let found = parser.collect::>>() + .expect("valid"); + + assert_eq!(found.len(), 1, + "stdout:\n{}\nstderr:\n{}", + stdout, stderr); + let found = found.into_iter().next().expect("have one"); + + assert_eq!(found.fingerprint(), bob.fingerprint()); + assert_eq!(found.userids().count(), 1); + + let ua = found.userids().next().expect("have one"); + let certifications: Vec<_> = ua.certifications().collect(); + assert_eq!(certifications.len(), 1); + let certification = certifications.into_iter().next().unwrap(); + + assert_eq!(certification.get_issuers().into_iter().next(), + Some(KeyHandle::from(alice.fingerprint()))); + certification.clone().verify_userid_binding( + alice.primary_key().key(), + bob.primary_key().key(), + &UserID::from("")) + .expect("valid certification"); + + Ok(()) +} diff --git a/tests/sq-decrypt.rs b/tests/sq-decrypt.rs index 7cb5ba1c..4894946a 100644 --- a/tests/sq-decrypt.rs +++ b/tests/sq-decrypt.rs @@ -19,6 +19,7 @@ mod integration { fn session_key() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("decrypt") .args(["--session-key", "1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) .arg(artifact("messages/rsa.msg.pgp")) @@ -32,6 +33,7 @@ mod integration { fn session_key_with_prefix() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("decrypt") .args(["--session-key", "9:1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) .arg(artifact("messages/rsa.msg.pgp")) @@ -45,6 +47,7 @@ mod integration { fn session_key_multiple() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("decrypt") .args(["--session-key", "2FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) .args(["--session-key", "9:1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) @@ -60,6 +63,7 @@ mod integration { fn session_key_wrong_key() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("decrypt") .args(["--session-key", "BB9CCB8EDE22DC222C83BD1C63AEB97335DDC7B696DB171BD16EAA5784CC0478"]) .arg(artifact("messages/rsa.msg.pgp")) diff --git a/tests/sq-encrypt.rs b/tests/sq-encrypt.rs new file mode 100644 index 00000000..127422a6 --- /dev/null +++ b/tests/sq-encrypt.rs @@ -0,0 +1,90 @@ +use assert_cmd::Command; +use tempfile::TempDir; + +use sequoia_openpgp as openpgp; +use openpgp::KeyHandle; +use openpgp::Result; +use openpgp::cert::prelude::*; +use openpgp::parse::Parse; + +mod integration { + use super::*; + + #[test] + fn sq_encrypt_using_cert_store() -> Result<()> + { + let dir = TempDir::new()?; + + let certd = dir.path().join("cert.d").display().to_string(); + std::fs::create_dir(&certd).expect("mkdir works"); + let key_pgp = dir.path().join("key.pgp").display().to_string(); + + // Generate a key. + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "key", "generate", + "--expires", "never", + "--userid", "", + "--export", &key_pgp]); + cmd.assert().success(); + + let cert = Cert::from_file(&key_pgp)?; + + // Try to encrypt a message. This should fail, because we + // haven't imported the key. + for kh in cert.keys().map(|ka| KeyHandle::from(ka.fingerprint())) + .chain(cert.keys().map(|ka| KeyHandle::from(ka.keyid()))) + { + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "encrypt", + "--recipient-cert", + &kh.to_string()]) + .write_stdin("a secret message") + .assert().failure(); + } + + // Import the key. + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "import", &key_pgp]); + cmd.assert().success(); + + const MESSAGE: &str = "\na secret message\n\nor two\n"; + + // Now we should be able to encrypt a message to it, and + // decrypt it. + for kh in cert.keys().map(|ka| KeyHandle::from(ka.fingerprint())) + .chain(cert.keys().map(|ka| KeyHandle::from(ka.keyid()))) + { + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "encrypt", + "--recipient-cert", + &kh.to_string()]) + .write_stdin(MESSAGE); + + let output = cmd.output().expect("success"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success(), + "encryption succeeds for {}\nstdout:\n{}\nstderr:\n{}", + kh, stdout, stderr); + + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["decrypt", + "--recipient-file", + &key_pgp]) + .write_stdin(stdout.as_bytes()); + + let output = cmd.output().expect("success"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success(), + "decryption succeeds for {}\nstdout:\n{}\nstderr:\n{}", + kh, stdout, stderr); + } + + Ok(()) + } +} diff --git a/tests/sq-export.rs b/tests/sq-export.rs new file mode 100644 index 00000000..2460ca68 --- /dev/null +++ b/tests/sq-export.rs @@ -0,0 +1,238 @@ +use std::borrow::Cow; +use std::ops::Deref; + +use assert_cmd::Command; +use tempfile::TempDir; + +use sequoia_openpgp as openpgp; +use openpgp::KeyHandle; +use openpgp::Result; +use openpgp::cert::prelude::*; +use openpgp::parse::Parse; + +mod integration { + use super::*; + + #[test] + fn sq_export() -> Result<()> + { + let dir = TempDir::new()?; + + let certd = dir.path().join("cert.d").display().to_string(); + std::fs::create_dir(&certd).expect("mkdir works"); + + struct Data { + userids: &'static [&'static str], + filename: String, + cert: Option, + } + + impl Data { + fn cert(&self) -> &Cert { + self.cert.as_ref().expect("have the cert") + } + } + + let data: &mut [Data] = &mut [ + Data { + userids: &[ "" ][..], + filename: dir.path().join("alice.pgp").display().to_string(), + cert: None, + }, + Data { + userids: &[ "" ][..], + filename: dir.path().join("bob.pgp").display().to_string(), + cert: None, + }, + Data { + userids: &[ + "", + "", + ][..], + filename: dir.path().join("carol.pgp").display().to_string(), + cert: None, + }, + ][..]; + + // Generate and import the keys. + for data in data.iter_mut() { + eprintln!("Generating key for {}", + data.userids.join(", ")); + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "key", "generate", + "--expires", "never", + "--export", &data.filename]); + for userid in data.userids.iter() { + cmd.args(["--userid", userid]); + } + cmd.assert().success(); + + let cert = Cert::from_file(&data.filename)?; + eprintln!("Importing {}", cert.fingerprint()); + + data.cert = Some(cert); + + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "import", + &data.filename]); + cmd.assert().success(); + } + + assert_eq!(data.len(), 3); + let alice = &data[0]; + let bob = &data[1]; + let carol = &data[2]; + + // Checks that the data contains exactly the listed + // certificates. + let check = |data: &[&Data], stdout: Cow, stderr: Cow| { + let parser = CertParser::from_bytes(stdout.as_bytes()) + .expect("valid"); + let found = parser.collect::>>() + .expect("valid"); + + assert_eq!(found.len(), data.len(), + "found: {}\nexpected: {}\n\ + stdout:\n{}\nstderr:\n{}", + found.iter().map(|c| c.fingerprint().to_string()) + .collect::>() + .join(", "), + data.iter().map(|d| d.cert().fingerprint().to_string()) + .collect::>() + .join(", "), + stdout, stderr); + for cert in found.iter() { + let fpr = cert.fingerprint(); + if let Some(_data) = data.iter().find(|data| { + data.cert().fingerprint() == fpr + }) { + () + } else { + panic!("Didn't find {} (have: {})\n\ + stdout:\n{}\nstderr:\n{}", + fpr, + data.iter() + .map(|d| d.cert().fingerprint().to_string()) + .collect::>() + .join(", "), + stdout, stderr); + }; + } + }; + + // args: --cert|--key|--userid|... pattern + let call = |args: &[&str], success: bool, data: &[&Data]| { + let mut cmd = Command::cargo_bin("sq").unwrap(); + cmd.args(["--cert-store", &certd, + "export"]); + cmd.args(args); + + let args = args.iter() + .map(|s| format!("{:?}", s)) + .collect::>() + .join(" "); + eprintln!("sq export {}...", args); + + let output = cmd.output().expect("success"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + if success { + assert!(output.status.success(), + "sq export {} should succeed\n\ + stdout:\n{}\nstderr:\n{}", + args, stdout, stderr); + check(data, stdout, stderr); + } else { + assert!(! output.status.success(), + "sq export {} should fail\n\ + stdout:\n{}\nstderr:\n{}", + args, stdout, stderr); + check(&[], stdout, stderr); + } + }; + + for data in data.iter() { + let cert = data.cert.as_ref().unwrap(); + + // Export them by the cert's fingerprint and keyid. + for ka in cert.keys() { + for kh in [ KeyHandle::from(ka.fingerprint()), + KeyHandle::from(ka.keyid()) ] + { + call(&["--cert", &kh.to_string()], ka.primary(), &[data]); + } + } + + // Export them by fingerprint and keyid. + for kh in cert.keys().map(|ka| KeyHandle::from(ka.fingerprint())) + .chain(cert.keys().map(|ka| KeyHandle::from(ka.keyid()))) + { + call(&["--key", &kh.to_string()], true, &[data]); + } + + for ua in cert.userids() { + // Export by user id. + let userid = String::from_utf8_lossy( + ua.userid().value()).into_owned(); + let email = ua.userid().email().unwrap().unwrap(); + + call(&["--userid", &userid], true, &[data]); + call(&["--userid", &email], false, &[]); + // Should be case sensitive. + call(&["--userid", &userid.deref().to_uppercase()], false, &[]); + // Substring should fail. + call(&["--userid", &userid[1..]], false, &[]); + + call(&["--email", &userid], false, &[]); + call(&["--email", &email], true, &[data]); + // Email is case insensitive. + call(&["--email", &email.deref().to_uppercase()], true, &[data]); + // Substring should fail. + call(&["--email", &email[1..]], false, &[]); + + call(&["--grep", &userid], true, &[data]); + call(&["--grep", &email], true, &[data]); + // Should be case insensitive. + call(&["--grep", &userid.deref().to_uppercase()], true, &[data]); + // Substring should succeed. + call(&["--grep", &userid[1..]], true, &[data]); + + } + } + + // By domain. + call(&["--domain", "example.org"], true, &[alice, bob]); + call(&["--domain", "EXAMPLE.ORG"], true, &[alice, bob]); + call(&["--domain", "sub.example.org"], true, &[carol]); + call(&["--domain", "SUB.EXAMPLE.ORG"], true, &[carol]); + call(&["--domain", "other.org"], true, &[carol]); + + call(&["--domain", "hello.com"], false, &[]); + call(&["--domain", "me@hello.com"], false, &[]); + call(&["--domain", "alice@example.org"], false, &[]); + call(&["--domain", "xample.org"], false, &[]); + call(&["--domain", "example.or"], false, &[]); + call(&["--domain", "@example.org"], false, &[]); + + // Match a cert in many ways. It should only be exported + // once. + call(&["--cert", &carol.cert().fingerprint().to_string(), + "--key", + &carol.cert().keys().nth(1).unwrap().fingerprint().to_string(), + "--userid", carol.userids[0], + "--email", "carol@example.org", + "--domain", "other.org" + ], true, &[carol]); + + // Match multiple certs in different ways. + call(&["--cert", &alice.cert().fingerprint().to_string(), + "--key", &bob.cert().fingerprint().to_string(), + "--email", "carol@sub.example.org", + ], true, &[alice, bob, carol]); + + Ok(()) + } +} diff --git a/tests/sq-import.rs b/tests/sq-import.rs new file mode 100644 index 00000000..30b27152 --- /dev/null +++ b/tests/sq-import.rs @@ -0,0 +1,120 @@ +use tempfile::TempDir; +use assert_cmd::Command; + +use sequoia_openpgp as openpgp; +use openpgp::Result; +use openpgp::cert::prelude::*; +use openpgp::parse::Parse; + +#[test] +fn sq_import() -> Result<()> +{ + let dir = TempDir::new()?; + + let certd = dir.path().join("cert.d").display().to_string(); + std::fs::create_dir(&certd).expect("mkdir works"); + + let alice_pgp = dir.path().join("alice.pgp").display().to_string(); + let alice_pgp = &alice_pgp[..]; + let bob_pgp = dir.path().join("bob.pgp").display().to_string(); + let bob_pgp = &bob_pgp[..]; + let carol_pgp = dir.path().join("carol.pgp").display().to_string(); + let carol_pgp = &carol_pgp[..]; + + // Generate keys. + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "key", "generate", + "--expires", "never", + "--userid", "", + "--export", &alice_pgp]); + cmd.assert().success(); + + let alice_bytes = std::fs::read(&alice_pgp)?; + + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "key", "generate", + "--expires", "never", + "--userid", "", + "--export", bob_pgp]); + cmd.assert().success(); + + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "key", "generate", + "--expires", "never", + "--userid", "", + "--export", carol_pgp]); + cmd.assert().success(); + + let files = &[ alice_pgp, bob_pgp, carol_pgp ]; + + let check = |files: &[&str], stdin: Option<&[u8]>, expected: usize| + { + // Use a fresh certd. + let dir = TempDir::new().unwrap(); + let certd = dir.path().join("cert.d").display().to_string(); + + // Import. + let mut cmd = Command::cargo_bin("sq").unwrap(); + cmd.args(["--cert-store", &certd, "import"]); + cmd.args(files); + if let Some(stdin) = stdin { + cmd.write_stdin(stdin); + } + eprintln!("sq import {}{}", + files.join(" "), + if stdin.is_some() { ">>() + .expect("valid"); + + assert_eq!(expected, found.len(), + "expected: {}\nfound: {} ({})\n\ + stdout:\n{}\nstderr:\n{}", + expected, found.len(), + found.iter().map(|c| c.fingerprint().to_string()) + .collect::>() + .join(", "), + stdout, stderr); + }; + + // Import from N files. + for i in 1..=files.len() { + check(&files[0..i], None, i); + } + + // Import from stdin. + check(&[], Some(&alice_bytes[..]), 1); + + // Specify "-". + check(&["-"], Some(&alice_bytes[..]), 1); + + // Provide stdin and a file. Only the file should be read. + check(&[bob_pgp], Some(&alice_bytes[..]), 1); + + // Provide stdin explicitly and a file. Both should be read. + check(&[bob_pgp, "-"], Some(&alice_bytes[..]), 2); + + Ok(()) +} diff --git a/tests/sq-key-adopt.rs b/tests/sq-key-adopt.rs index b3097f14..aa154d0f 100644 --- a/tests/sq-key-adopt.rs +++ b/tests/sq-key-adopt.rs @@ -141,7 +141,7 @@ mod integration { #[test] fn adopt_encryption() -> Result<()> { // Adopt an encryption subkey. - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(bob()) .arg("--keyring").arg(alice()) .arg("--key").arg(alice_encryption().0.to_hex()) @@ -157,7 +157,7 @@ mod integration { #[test] fn adopt_signing() -> Result<()> { // Adopt a signing subkey (subkey has secret key material). - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(bob()) .arg("--keyring").arg(alice()) .arg("--key").arg(alice_signing().0.to_hex()) @@ -173,7 +173,7 @@ mod integration { #[test] fn adopt_certification() -> Result<()> { // Adopt a certification subkey (subkey has secret key material). - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(carol()) .arg("--keyring").arg(alice()) .arg("--key").arg(alice_primary().0.to_hex()) @@ -189,7 +189,7 @@ mod integration { #[test] fn adopt_encryption_and_signing() -> Result<()> { // Adopt an encryption subkey and a signing subkey. - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(bob()) .arg("--keyring").arg(alice()) .arg("--key").arg(alice_signing().0.to_hex()) @@ -209,7 +209,7 @@ mod integration { #[test] fn adopt_twice() -> Result<()> { // Adopt the same an encryption subkey twice. - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(bob()) .arg("--keyring").arg(alice()) .arg("--key").arg(alice_encryption().0.to_hex()) @@ -226,7 +226,7 @@ mod integration { #[test] fn adopt_key_appears_twice() -> Result<()> { // Adopt the an encryption subkey that appears twice. - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(bob()) .arg("--keyring").arg(alice()) .arg("--keyring").arg(alice()) @@ -243,7 +243,7 @@ mod integration { #[test] fn adopt_own_encryption() -> Result<()> { // Adopt its own encryption subkey. This should be a noop. - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(alice()) .arg("--keyring").arg(alice()) .arg("--key").arg(alice_encryption().0.to_hex()) @@ -259,7 +259,7 @@ mod integration { #[test] fn adopt_own_primary() -> Result<()> { // Adopt own primary key. - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(bob()) .arg("--keyring").arg(bob()) .arg("--key").arg(bob_primary().0.to_hex()) @@ -275,7 +275,7 @@ mod integration { #[test] fn adopt_missing() -> Result<()> { // Adopt a key that is not present. - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(bob()) .arg("--keyring").arg(bob()) .arg("--key").arg("1234 5678 90AB CDEF 1234 5678 90AB CDEF") @@ -288,7 +288,7 @@ mod integration { #[test] fn adopt_from_multiple() -> Result<()> { // Adopt from multiple certificates simultaneously. - Command::cargo_bin("sq").unwrap().arg("key").arg("adopt") + Command::cargo_bin("sq").unwrap().arg("--no-cert-store").arg("key").arg("adopt") .arg(bob()) .arg("--keyring").arg(alice()) .arg("--key").arg(alice_signing().0.to_hex()) diff --git a/tests/sq-key-generate.rs b/tests/sq-key-generate.rs index 89f78648..7d066d1c 100644 --- a/tests/sq-key-generate.rs +++ b/tests/sq-key-generate.rs @@ -26,7 +26,8 @@ mod integration { // Build up the command line. let mut cmd = Command::cargo_bin("sq")?; - cmd.args(["key", "generate", + cmd.args(["--no-cert-store", + "key", "generate", "--creation-time", iso8601, "--expires", "never", "--export", &*key_pgp.to_string_lossy()]); diff --git a/tests/sq-packet-decrypt.rs b/tests/sq-packet-decrypt.rs index 5a466ac7..d7b3a8b9 100644 --- a/tests/sq-packet-decrypt.rs +++ b/tests/sq-packet-decrypt.rs @@ -19,6 +19,7 @@ mod sq_packet_decrypt { fn session_key() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("decrypt") .args(["--session-key", "1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) @@ -33,6 +34,7 @@ mod sq_packet_decrypt { fn session_key_with_prefix() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("decrypt") .args(["--session-key", "9:1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) @@ -47,6 +49,7 @@ mod sq_packet_decrypt { fn session_key_multiple() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("decrypt") .args(["--session-key", "2FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) @@ -63,6 +66,7 @@ mod sq_packet_decrypt { fn session_key_wrong_key() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("decrypt") .args(["--session-key", "BB9CCB8EDE22DC222C83BD1C63AEB97335DDC7B696DB171BD16EAA5784CC0478"]) diff --git a/tests/sq-packet-dump.rs b/tests/sq-packet-dump.rs index d431d995..4bfd9fcb 100644 --- a/tests/sq-packet-dump.rs +++ b/tests/sq-packet-dump.rs @@ -14,6 +14,7 @@ mod sq_packet_dump { fn session_key_without_prefix() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("dump") .args(["--session-key", "1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) @@ -28,6 +29,7 @@ mod sq_packet_dump { fn session_key_with_prefix() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("dump") .args(["--session-key", "9:1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) @@ -42,6 +44,7 @@ mod sq_packet_dump { fn session_key_with_bad_prefix() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("dump") .args(["--session-key", "1:1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4"]) @@ -58,6 +61,7 @@ mod sq_packet_dump { // too short Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("dump") .args(["--session-key", "1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437"]) @@ -69,6 +73,7 @@ mod sq_packet_dump { // too long Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("dump") .args(["--session-key", "1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4AB"]) @@ -84,6 +89,7 @@ mod sq_packet_dump { // too short Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("dump") .args(["--session-key", "1:1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437"]) @@ -95,6 +101,7 @@ mod sq_packet_dump { // too long Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("dump") .args(["--session-key", "1:1FE820EC21FB5D7E33D83367106D1D3747DCD48E6320C1AEC57EE7D18FC437D4AB"]) @@ -109,6 +116,7 @@ mod sq_packet_dump { fn session_key_wrong_key_with_prefix() -> Result<()> { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("packet") .arg("dump") .args(["--session-key", "9:BB9CCB8EDE22DC222C83BD1C63AEB97335DDC7B696DB171BD16EAA5784CC0478"]) diff --git a/tests/sq-revoke.rs b/tests/sq-revoke.rs index 55d255a0..c608bfd1 100644 --- a/tests/sq-revoke.rs +++ b/tests/sq-revoke.rs @@ -110,6 +110,7 @@ mod integration { // Build up the command line. let mut cmd = Command::cargo_bin("sq")?; + cmd.arg("--no-cert-store"); cmd.arg("revoke"); if let Some(userid) = userid { cmd.args([ @@ -230,7 +231,7 @@ mod integration { // Pretty print 'sq revoke''s output for debugging purposes. if TRACE { let mut cmd = Command::cargo_bin("sq")?; - cmd.args([ "inspect" ]); + cmd.args([ "--no-cert-store", "inspect" ]); cmd.write_stdin(stdout.as_bytes()); let assertion = cmd.assert().try_success()?; eprintln!("Result:\n{}", diff --git a/tests/sq-sign.rs b/tests/sq-sign.rs index a6f08291..d89cdd40 100644 --- a/tests/sq-sign.rs +++ b/tests/sq-sign.rs @@ -34,6 +34,7 @@ fn sq_sign() { // Sign message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .args(["--signer-file", &artifact("keys/dennis-simon-anton-private.pgp")]) .args(["--output", &sig.to_string_lossy()]) @@ -68,6 +69,7 @@ fn sq_sign() { // Verify signed message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/dennis-simon-anton.pgp")]) .arg(&*sig.to_string_lossy()) @@ -83,6 +85,7 @@ fn sq_sign_with_notations() { // Sign message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .args(["--signer-file", &artifact("keys/dennis-simon-anton-private.pgp")]) .args(["--output", &sig.to_string_lossy()]) @@ -145,6 +148,7 @@ fn sq_sign_with_notations() { // Verify signed message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .args(["--known-notation", "foo"]) .arg("verify") .args(["--signer-file", &artifact("keys/dennis-simon-anton.pgp")]) @@ -161,6 +165,7 @@ fn sq_sign_append() { // Sign message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .args(["--signer-file", &artifact("keys/dennis-simon-anton-private.pgp")]) .args(["--output", &sig0.to_string_lossy()]) @@ -195,6 +200,7 @@ fn sq_sign_append() { // Verify signed message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/dennis-simon-anton.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -205,6 +211,7 @@ fn sq_sign_append() { let sig1 = tmp_dir.path().join("sig1"); Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--append") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp")]) @@ -254,6 +261,7 @@ fn sq_sign_append() { // Verify both signatures of the signed message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/dennis-simon-anton.pgp")]) .arg(&*sig1.to_string_lossy()) @@ -261,6 +269,7 @@ fn sq_sign_append() { .success(); Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")]) .arg(&*sig1.to_string_lossy()) @@ -322,6 +331,7 @@ fn sq_sign_append_on_compress_then_sign() { // Verify signed message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/dennis-simon-anton.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -332,6 +342,7 @@ fn sq_sign_append_on_compress_then_sign() { let sig1 = tmp_dir.path().join("sig1"); Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--append") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp")]) @@ -384,6 +395,7 @@ fn sq_sign_append_on_compress_then_sign() { // Verify both signatures of the signed message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/dennis-simon-anton.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -392,6 +404,7 @@ fn sq_sign_append_on_compress_then_sign() { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -407,6 +420,7 @@ fn sq_sign_detached() { // Sign detached. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--detached") .args(["--signer-file", &artifact("keys/dennis-simon-anton-private.pgp")]) @@ -431,6 +445,7 @@ fn sq_sign_detached() { // Verify detached. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/dennis-simon-anton.pgp")]) .args(["--detached", &sig.to_string_lossy()]) @@ -447,6 +462,7 @@ fn sq_sign_detached_append() { // Sign detached. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--detached") .args(["--signer-file", &artifact("keys/dennis-simon-anton-private.pgp")]) @@ -471,6 +487,7 @@ fn sq_sign_detached_append() { // Verify detached. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/dennis-simon-anton.pgp")]) .args(["--detached", &sig.to_string_lossy()]) @@ -481,6 +498,7 @@ fn sq_sign_detached_append() { // Check that we don't blindly overwrite signatures. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--detached") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp")]) @@ -492,6 +510,7 @@ fn sq_sign_detached_append() { // Now add a second signature with --append. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--detached") .arg("--append") @@ -522,6 +541,7 @@ fn sq_sign_detached_append() { // Verify both detached signatures. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/dennis-simon-anton.pgp")]) .args(["--detached", &sig.to_string_lossy()]) @@ -531,6 +551,7 @@ fn sq_sign_detached_append() { Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")]) .args(["--detached", &sig.to_string_lossy()]) @@ -542,6 +563,7 @@ fn sq_sign_detached_append() { // goes wrong. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--detached") .arg("--append") @@ -578,6 +600,7 @@ fn sq_sign_append_a_notarization() { // Now add a third signature with --append to a notarized message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--append") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp")]) @@ -638,6 +661,7 @@ fn sq_sign_append_a_notarization() { // Verify both notarizations and the signature. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/neal.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -645,6 +669,7 @@ fn sq_sign_append_a_notarization() { .success(); Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/emmelie-dorothea-dina-samantha-awina-ed25519.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -652,6 +677,7 @@ fn sq_sign_append_a_notarization() { .success(); Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -667,6 +693,7 @@ fn sq_sign_notarize() { // Now add a third signature with --append to a notarized message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--notarize") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp")]) @@ -715,6 +742,7 @@ fn sq_sign_notarize() { // Verify both notarizations and the signature. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/neal.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -722,6 +750,7 @@ fn sq_sign_notarize() { .success(); Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -737,6 +766,7 @@ fn sq_sign_notarize_a_notarization() { // Now add a third signature with --append to a notarized message. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("sign") .arg("--notarize") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256-private.pgp")]) @@ -797,6 +827,7 @@ fn sq_sign_notarize_a_notarization() { // Verify both notarizations and the signature. Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/neal.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -804,6 +835,7 @@ fn sq_sign_notarize_a_notarization() { .success(); Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/emmelie-dorothea-dina-samantha-awina-ed25519.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -811,6 +843,7 @@ fn sq_sign_notarize_a_notarization() { .success(); Command::cargo_bin("sq") .unwrap() + .arg("--no-cert-store") .arg("verify") .args(["--signer-file", &artifact("keys/erika-corinna-daniela-simone-antonia-nistp256.pgp")]) .arg(&*sig0.to_string_lossy()) @@ -842,6 +875,7 @@ fn sq_multiple_signers() -> Result<()> { // Sign message. let assertion = Command::cargo_bin("sq")? .args([ + "--no-cert-store", "sign", "--signer-file", alice_pgp.to_str().unwrap(), "--signer-file", &bob_pgp.to_str().unwrap(), @@ -887,3 +921,139 @@ fn sq_multiple_signers() -> Result<()> { Ok(()) } + +#[test] +fn sq_sign_using_cert_store() -> Result<()> { + let dir = TempDir::new()?; + + let certd = dir.path().join("cert.d").display().to_string(); + std::fs::create_dir(&certd).expect("mkdir works"); + + let alice_pgp = dir.path().join("alice.pgp").display().to_string(); + let msg_pgp = dir.path().join("msg.pgp").display().to_string(); + + // Generate a key. + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "key", "generate", + "--expires", "never", + "--userid", "", + "--export", &alice_pgp]); + cmd.assert().success(); + + let alice = Cert::from_file(&alice_pgp)?; + + // Import it. + let mut cmd = Command::cargo_bin("sq")?; + cmd.args(["--cert-store", &certd, + "import", &alice_pgp]); + cmd.assert().success(); + + + // Sign a message. + Command::cargo_bin("sq") + .unwrap() + .arg("--no-cert-store") + .arg("sign") + .args(["--signer-file", &alice_pgp]) + .args(["--output", &msg_pgp]) + .arg(&artifact("messages/a-cypherpunks-manifesto.txt")) + .assert() + .success(); + + // Check that the content is sane. + let packets: Vec = + PacketPile::from_file(&msg_pgp).unwrap().into_children().collect(); + assert_eq!(packets.len(), 3); + if let Packet::OnePassSig(ref ops) = packets[0] { + assert!(ops.last()); + assert_eq!(ops.typ(), SignatureType::Binary); + } else { + panic!("expected one pass signature"); + } + if let Packet::Literal(_) = packets[1] { + // Do nothing. + } else { + panic!("expected literal"); + } + if let Packet::Signature(ref sig) = packets[2] { + assert_eq!(sig.typ(), SignatureType::Binary); + + let alice_signer = alice.with_policy(P, None)? + .keys().for_signing().next().expect("have one"); + assert_eq!(sig.get_issuers().into_iter().next(), + Some(KeyHandle::from(alice_signer.fingerprint()))); + } else { + panic!("expected signature"); + } + + let content = fs::read(&msg_pgp).unwrap(); + assert!(&content[..].starts_with(b"-----BEGIN PGP MESSAGE-----\n\n")); + + // Verify the signed message. First, we specify the certificate + // explicitly. + Command::cargo_bin("sq") + .unwrap() + .arg("--no-cert-store") + .arg("verify") + .args(["--signer-file", &alice_pgp]) + .arg(&msg_pgp) + .assert() + .success(); + + // Verify the signed message. Now, we don't specify the + // certificate or use a certificate store. + let mut cmd = Command::cargo_bin("sq").unwrap(); + cmd.arg("--no-cert-store") + .arg("verify") + .arg(&msg_pgp); + let output = cmd.output().expect("success"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(! output.status.success(), + "stdout:\n{}\nstderr: {}", stdout, stderr); + + assert!(stderr.contains("No key to check checksum from "), + "stdout:\n{}\nstderr: {}", stdout, stderr); + assert!(stderr.contains("Error: Verification failed"), + "stdout:\n{}\nstderr: {}", stdout, stderr); + + // Now we use the certificate store. + let mut cmd = Command::cargo_bin("sq").unwrap(); + cmd.arg("--cert-store").arg(&certd) + .arg("verify") + .arg(&msg_pgp); + let output = cmd.output().expect("success"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(! output.status.success(), + "stdout:\n{}\nstderr: {}", stdout, stderr); + + // The default trust model says that certificates from the + // certificate store are not authenticated. + assert!(stderr.contains("Good checksum from "), + "stdout:\n{}\nstderr: {}", stdout, stderr); + assert!(stderr.contains("Error: Verification failed"), + "stdout:\n{}\nstderr: {}", stdout, stderr); + + // Now we use the certificate store *and* specify the certificate. + let mut cmd = Command::cargo_bin("sq").unwrap(); + cmd.arg("--cert-store").arg(&certd) + .arg("verify") + .arg("--signer-cert").arg(&alice.fingerprint().to_string()) + .arg(&msg_pgp); + let output = cmd.output().expect("success"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success(), + "stdout:\n{}\nstderr: {}", stdout, stderr); + + // The default trust model says that certificates from the + // certificate store are not authenticated. + assert!(stderr.contains("Good signature from "), + "stdout:\n{}\nstderr: {}", stdout, stderr); + assert!(stderr.contains("1 good signature."), + "stdout:\n{}\nstderr: {}", stdout, stderr); + + Ok(()) +}