diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 367b65a..df9d10e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,6 @@ stages: test-job: stage: test - image: localhost:5000/huskbuilder:latest + image: localhost:5000/mta:latest script: - cargo test diff --git a/Cargo.lock b/Cargo.lock index 05ab558..a3c5442 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,17 @@ dependencies = [ "term", ] +[[package]] +name = "async-generic" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3728566eefa873833159754f5732fb0951d3649e6e5b891cc70d56dd41673" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -165,15 +176,30 @@ dependencies = [ "syn", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + [[package]] name = "bit-set" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" dependencies = [ - "bit-vec", + "bit-vec 0.9.1", ] +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit-vec" version = "0.9.1" @@ -255,6 +281,46 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "capnp" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfd2485d4b36ac9c5aa6572d7d35daa63a5b34f517627d6c34d068e616e4a73" +dependencies = [ + "embedded-io", +] + +[[package]] +name = "capnp-futures" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b69dfddccc57844f9a90f9d72b44b97c326914851ea94fb7da40ef9cad6e8d" +dependencies = [ + "capnp", + "futures-channel", + "futures-util", +] + +[[package]] +name = "capnp-rpc" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c74c337e87f75f3174ffb3513738ab0acc631c04f64cc33b867c60f84771da" +dependencies = [ + "capnp", + "capnp-futures", + "futures", +] + +[[package]] +name = "capnpc" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fca02be865c8c5a78bfc24b9819006ab6b59bef238467203928e26459557af93" +dependencies = [ + "capnp", +] + [[package]] name = "cc" version = "1.2.62" @@ -469,6 +535,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "data-encoding" version = "2.11.0" @@ -567,6 +649,21 @@ dependencies = [ "syn", ] +[[package]] +name = "dtor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -579,6 +676,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" + [[package]] name = "ena" version = "0.14.4" @@ -615,6 +718,29 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -724,6 +850,31 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -731,6 +882,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -739,6 +891,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -774,9 +937,13 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -1044,11 +1211,13 @@ dependencies = [ "log4rs", "sequoia-cert-store", "sequoia-directories", + "sequoia-keystore", "sequoia-net", "sequoia-openpgp", "sequoia-wot", "serde", "serde_derive", + "tempfile", "thiserror 2.0.18", "tokio", "toml", @@ -1338,6 +1507,30 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.99" @@ -1359,6 +1552,27 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "lalrpop" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4ebbd48ce411c1d10fb35185f5a51a7bfa3d8b24b4e330d30c9e3a34129501" +dependencies = [ + "ascii-canvas", + "bit-set 0.8.0", + "ena", + "itertools 0.14.0", + "lalrpop-util 0.22.2", + "petgraph 0.7.1", + "regex", + "regex-syntax", + "sha3", + "string_cache 0.8.9", + "term", + "unicode-xid", + "walkdir", +] + [[package]] name = "lalrpop" version = "0.23.1" @@ -1366,20 +1580,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98a80a963123205c7157323c99611bc4abb65dcbd62ef46dc4bac74a3941bc75" dependencies = [ "ascii-canvas", - "bit-set", + "bit-set 0.9.1", "ena", "itertools 0.14.0", - "lalrpop-util", - "petgraph", + "lalrpop-util 0.23.1", + "petgraph 0.8.3", "regex", "regex-syntax", "sha3", - "string_cache", + "string_cache 0.9.0", "term", "unicode-xid", "walkdir", ] +[[package]] +name = "lalrpop-util" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5baa5e9ff84f1aefd264e6869907646538a52147a755d494517a8007fb48733" +dependencies = [ + "regex-automata", + "rustversion", +] + [[package]] name = "lalrpop-util" version = "0.23.1" @@ -1777,12 +2001,28 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "petgraph" version = "0.8.3" @@ -1794,6 +2034,15 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "phf_shared" version = "0.13.1" @@ -1815,6 +2064,21 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -2274,6 +2538,122 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "sequoia-gpg-agent" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c36b37720b61230507a953d9879eb0dd8c228d86948d37cba53ced942e862a1" +dependencies = [ + "anyhow", + "chrono", + "futures", + "lalrpop 0.22.2", + "lalrpop-util 0.22.2", + "libc", + "sequoia-ipc", + "sequoia-openpgp", + "stfu8", + "tempfile", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "sequoia-ipc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0770bf144d96733921b4647cd4b243cbebf032127676a441e3ebe702c0768c54" +dependencies = [ + "anyhow", + "capnp-rpc", + "capnpc", + "ctor", + "dirs", + "fs2", + "lalrpop 0.22.2", + "lalrpop-util 0.22.2", + "libc", + "memsec", + "sequoia-openpgp", + "socket2", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "winapi", +] + +[[package]] +name = "sequoia-keystore" +version = "0.7.2" +source = "git+https://gitlab.com/sequoia-pgp/sequoia-keystore?branch=malte%2Fsoftkey-refresh#cc77f2ea46ab1c3ca1ace0c16848a4137e67bdf7" +dependencies = [ + "anyhow", + "async-generic", + "capnp", + "capnpc", + "dirs", + "env_logger", + "log", + "paste", + "sequoia-directories", + "sequoia-ipc", + "sequoia-keystore-backend", + "sequoia-keystore-gpg-agent", + "sequoia-keystore-softkeys", + "sequoia-openpgp", + "thiserror 1.0.69", + "tokio", + "tokio-util", +] + +[[package]] +name = "sequoia-keystore-backend" +version = "0.7.0" +source = "git+https://gitlab.com/sequoia-pgp/sequoia-keystore?branch=malte%2Fsoftkey-refresh#cc77f2ea46ab1c3ca1ace0c16848a4137e67bdf7" +dependencies = [ + "anyhow", + "async-trait", + "env_logger", + "futures", + "log", + "sequoia-openpgp", + "tempfile", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "sequoia-keystore-gpg-agent" +version = "0.5.0" +source = "git+https://gitlab.com/sequoia-pgp/sequoia-keystore?branch=malte%2Fsoftkey-refresh#cc77f2ea46ab1c3ca1ace0c16848a4137e67bdf7" +dependencies = [ + "anyhow", + "async-trait", + "futures", + "log", + "openpgp-cert-d", + "sequoia-gpg-agent", + "sequoia-ipc", + "sequoia-keystore-backend", + "sequoia-openpgp", + "tokio", +] + +[[package]] +name = "sequoia-keystore-softkeys" +version = "0.7.1" +source = "git+https://gitlab.com/sequoia-pgp/sequoia-keystore?branch=malte%2Fsoftkey-refresh#cc77f2ea46ab1c3ca1ace0c16848a4137e67bdf7" +dependencies = [ + "anyhow", + "async-trait", + "dirs", + "futures", + "log", + "sequoia-keystore-backend", + "sequoia-openpgp", +] + [[package]] name = "sequoia-net" version = "0.30.1" @@ -2314,8 +2694,8 @@ dependencies = [ "flate2", "getrandom 0.2.17", "idna", - "lalrpop", - "lalrpop-util", + "lalrpop 0.23.1", + "lalrpop-util 0.23.1", "libc", "memsec", "nettle", @@ -2527,6 +2907,24 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stfu8" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", +] + [[package]] name = "string_cache" version = "0.9.0" @@ -2535,7 +2933,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared", + "phf_shared 0.13.1", "precomputed-hash", ] @@ -2755,6 +3153,7 @@ checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", + "futures-io", "futures-sink", "pin-project-lite", "tokio", diff --git a/Cargo.toml b/Cargo.toml index a9add1e..64465d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,14 @@ log = "0.4.30" log4rs = "1.3.0" sequoia-cert-store = "0.7.3" sequoia-directories = "0.1.0" +#sequoia-keystore = "0.7.2" +sequoia-keystore = { git = "https://gitlab.com/sequoia-pgp/sequoia-keystore", branch = "malte/softkey-refresh" } sequoia-net = "0.30.1" sequoia-openpgp = "2.3.0" sequoia-wot = "0.15.0" serde = "1.0.228" serde_derive = "1.0.228" +tempfile = "3.27.0" thiserror = "2.0.17" tokio = { version = "1.52.3", features = [ "tokio-macros", "rt-multi-thread", "signal" ] } toml = "0.9.8" diff --git a/docker/MTAContainerFile b/docker/MTAContainerFile index df41e00..cb4056e 100644 --- a/docker/MTAContainerFile +++ b/docker/MTAContainerFile @@ -42,6 +42,8 @@ RUN apt-get update -yqq && \ libssl-dev \ llvm \ nettle-dev \ + capnproto \ + libsqlite3-dev \ pkg-config ## diff --git a/e2e-tests/common.sh b/e2e-tests/common.sh index 113b0aa..e97d84c 100644 --- a/e2e-tests/common.sh +++ b/e2e-tests/common.sh @@ -18,7 +18,7 @@ send_test_mail() { # email_address chown $USER:mail /var/mail/$USER chmod 600 /var/mail/$USER - echo "test mail" | sendmail $1 + echo -e "Subject: testmail\n\ntest mail" | sendmail $1 inotifywait -qq -t 3 -e close_write /var/mail/$USER } diff --git a/e2e-tests/tests/60_signing.sh b/e2e-tests/tests/60_signing.sh new file mode 100644 index 0000000..fe0202e --- /dev/null +++ b/e2e-tests/tests/60_signing.sh @@ -0,0 +1,71 @@ +#!/usr/bin/bash + +# source some functions +. common.sh + +test_header "Signing" + +# sq key generate --shared-key --without-password --email 'signing@example.com' --output /tmp/signing.pgp --rev-cert /tmp/signing.rev + +SIGN_FPR=$(sq key generate --own-key --without-password --name "Signing Key" 2>&1 | grep "Fingerprint:" | cut -d ':' -f 2) + +cd $HUSK_DIR + +$HUSK_BIN --config config/config.toml signkey set --cert $SIGN_FPR +SIGN_FPR=$($HUSK_BIN --config config/config.toml signkey show | cut -d ':' -f 2 | tr -d ' ') +echo "found signing key $SIGN_FPR" + +create_unix_user juliette +JULIETTE_FPR=$(sq key generate --own-key --without-password --name "Juliette" --email "juliette@example.com" 2>&1 | grep "Fingerprint:" | cut -d ':' -f 2) +sq pki vouch add --certifier-userid "CA" --cert $JULIETTE_FPR --all > /dev/null 2>&1 + +# send a mail, husk should encrypt it +echo "send mail to juliette@example.com" +send_test_mail juliette@example.com + +if [ -f /var/mail/juliette ] ; then + if grep -q "BEGIN PGP MESSAGE" /var/mail/juliette ; then + + FOUND_FPR=$(sq packet dump /var/mail/juliette | grep "Issuer Fingerprint:" | cut -d ':' -f 2 | tr -d ' ') + + if [ "$SIGN_FPR" != "$FOUND_FPR" ] ; then + echo "Found wrong fingerprint '$FOUND_FPR' - expected '$SIGN_FPR'" + exit 1 + fi + else + echo "no encryption detected" + exit 1 + fi +else + echo "no mail detected" + exit 1 +fi + +echo "set new signing key" + +SIGN_FPR=$($HUSK_BIN --config config/config.toml signkey create | cut -d ':' -f 2 | tr -d ' ') +echo "new signing key $SIGN_FPR" + +# remove old mail +rm /var/mail/juliette + +echo "send mail to juliette@example.com" +send_test_mail juliette@example.com + +if [ -f /var/mail/juliette ] ; then + if grep -q "BEGIN PGP MESSAGE" /var/mail/juliette ; then + + FOUND_FPR=$(sq packet dump /var/mail/juliette | grep "Issuer Fingerprint:" | cut -d ':' -f 2 | tr -d ' ') + + if [ "$SIGN_FPR" != "$FOUND_FPR" ] ; then + echo "Found wrong fingerprint '$FOUND_FPR' - expected '$SIGN_FPR'" + exit 1 + fi + else + echo "no encryption detected" + exit 1 + fi +else + echo "no mail detected" + exit 1 +fi diff --git a/src/cli/cli_args.rs b/src/cli/cli_args.rs index a20d589..b38e40a 100644 --- a/src/cli/cli_args.rs +++ b/src/cli/cli_args.rs @@ -18,7 +18,7 @@ pub struct CliArgs { #[clap( long, global = true, - help = "Certification store (SEQUOIA_HOME).", + help = "Certificate store (SEQUOIA_HOME).", )] pub home: Option, @@ -33,6 +33,7 @@ pub enum HuskSubcommands { Daemon(DaemonCommand), Introducer(IntroducerCommand), Locals(LocalsCommand), + Signkey(SignkeyCommand), } // Daemon subcommands @@ -76,6 +77,9 @@ pub struct IntroducerCommand { pub enum IntroducerSubcommand { Add(IntroducerAddCommand), Remove(IntroducerRemoveCommand), + #[clap( + about = "List introducers." + )] List } @@ -88,7 +92,8 @@ pub struct IntroducerAddCommand { #[clap( long = "cert", help = "Certificate to declare as introducer.", - conflicts_with = "cert_file" + conflicts_with = "cert_file", + value_name = "FINGERPRINT", )] pub cert: Option, @@ -118,7 +123,8 @@ pub struct IntroducerAddCommand { pub struct IntroducerRemoveCommand { #[clap( long = "cert", - help = "Certificate to be removed as introducer." + help = "Certificate to be removed as introducer.", + value_name = "FINGERPRINT", )] pub cert: String, @@ -149,6 +155,9 @@ pub struct LocalsCommand { pub enum LocalsSubcommand { Add(LocalsAddCommand), Remove(LocalsRemoveCommand), + #[clap( + about = "List locals." + )] List } @@ -174,8 +183,76 @@ pub struct LocalsAddCommand { pub struct LocalsRemoveCommand { #[clap( long = "cert", - help = "Certificate to be removed." + help = "Certificate to be removed.", + value_name = "FINGERPRINT", )] pub cert: String, } +// Signkey subcommands + +#[derive(Parser, Debug)] +#[clap( + name = "signkey", + about = "Manage the signing key.", + subcommand_required = true, + arg_required_else_help = true, + disable_colored_help = true, + disable_version_flag = true, +)] +pub struct SignkeyCommand { + #[clap(subcommand)] + pub subcommand: SignkeySubcommand, +} +#[derive(Debug, Subcommand)] +pub enum SignkeySubcommand { + Set(SignkeySetCommand), + Create(SignkeyCreateCommand), + #[clap( + about = "Show the fingerprint of the signing key." + )] + Show, + #[clap( + about = "Print the certificate of the signing key." + )] + Dump, +} + +#[derive(Parser, Debug)] +#[clap( + about = "Set a new signing key.", +)] +pub struct SignkeySetCommand { + #[clap( + long, + help = "Load a new signing key from .", + value_name = "FILE", + conflicts_with = "cert" + )] + pub cert_file: Option, + + #[clap( + long = "cert", + help = "Certificate to declare as signing key.", + conflicts_with = "cert_file", + value_name = "FINGERPRINT", + )] + pub cert: Option, +} + + +#[derive(Parser, Debug)] +#[clap( + about = "Create a new signing key.", +)] +pub struct SignkeyCreateCommand { + #[clap( + long, + help = "Set the userid of the new key.", + value_name = "USERID", + default_value = "Husk signing key", + )] + pub userid: String, +} + + diff --git a/src/commands.rs b/src/commands.rs index 333cafb..ebe6038 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -9,6 +9,7 @@ use crate::config::HuskConfigContainer; pub mod daemon; pub mod introducer; pub mod locals; +pub mod signkey; #[derive(thiserror::Error, Debug)] pub enum CommandError { @@ -20,6 +21,8 @@ pub enum CommandError { CertNotAlive(Fingerprint), #[error("Certificate is revoked: {0}")] CertRevoked(Fingerprint), + #[error("No signing key available")] + SigningKeyNotAvailable, } pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> { @@ -34,6 +37,9 @@ pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> { HuskSubcommands::Locals(subcmd) => { commands::locals::dispatch(subcmd, config).await?; }, + HuskSubcommands::Signkey(subcmd) => { + commands::signkey::dispatch(subcmd, config).await?; + } } Ok(()) diff --git a/src/commands/introducer/add.rs b/src/commands/introducer/add.rs index 113b91b..5f0da8d 100644 --- a/src/commands/introducer/add.rs +++ b/src/commands/introducer/add.rs @@ -45,6 +45,7 @@ pub async fn dispatch(cmd: IntroducerAddCommand, config: HuskConfigContainer) -> .filter_map(|c| c.with_policy(policy, None).ok() ) + .filter(|vc| crypto::can_certify(vc)) .collect(); if let Some(cert) = certs.first() { diff --git a/src/commands/signkey.rs b/src/commands/signkey.rs new file mode 100644 index 0000000..6a6e959 --- /dev/null +++ b/src/commands/signkey.rs @@ -0,0 +1,58 @@ +//! signkey subcommand + +use anyhow::Result; +use sequoia_cert_store::Store; +use sequoia_openpgp::serialize::SerializeInto; + +use crate::cli::cli_args::{SignkeyCommand, SignkeySubcommand}; +use crate::config::HuskConfigContainer; +use crate::types::husk_context::HuskContext; +use crate::commands::CommandError; + +pub mod set; +pub mod create; + +pub async fn dispatch(cmd: SignkeyCommand, config: HuskConfigContainer) + -> Result<()> { + match cmd.subcommand { + SignkeySubcommand::Set(set_cmd) => { + set::dispatch(set_cmd, config).await?; + }, + SignkeySubcommand::Create(create_cmd) => { + create::dispatch(create_cmd, config).await?; + } + SignkeySubcommand::Show => { + let context = HuskContext::new(&config.into())?; + if let Some(key) = context.signing_key { + println!("Fingerprint: {}", key.fingerprint()); + } else { + return Err(CommandError::SigningKeyNotAvailable.into()); + } + }, + SignkeySubcommand::Dump => { + let context = HuskContext::new(&config.into())?; + if let Some(key) = context.signing_key { + + if let Ok(cert) = &context.cert_store + .lookup_by_cert_fpr(&key.fingerprint()) { + if let Ok(cert) = cert.to_cert() { + let armored = cert.armored().to_vec()?; + println!("{}", String::from_utf8_lossy(&armored[..])); + } else { + return Err( + CommandError::CertNotUsable(cert.fingerprint()) + .into()); + } + } else { + return Err( + CommandError::CertNotFound(key.fingerprint()) + .into()); + } + } else { + return Err(CommandError::SigningKeyNotAvailable.into()); + } + } + } + + Ok(()) +} diff --git a/src/commands/signkey/create.rs b/src/commands/signkey/create.rs new file mode 100644 index 0000000..de873c5 --- /dev/null +++ b/src/commands/signkey/create.rs @@ -0,0 +1,35 @@ +//! signkey create command + +use std::path::PathBuf; + +use anyhow::Result; +use sequoia_openpgp::cert::CertBuilder; +use sequoia_directories::Home; + +use crate::cli::cli_args::SignkeyCreateCommand; +use crate::common::crypto; +use crate::config::{HuskConfig, HuskConfigContainer}; +use crate::types::husk_context::HuskContext; + +pub async fn dispatch(cmd: SignkeyCreateCommand, config: HuskConfigContainer) + -> Result<()> { + + let config: HuskConfig = config.into(); + let context = HuskContext::new(&config)?; + let sequoia_home = Home::new(PathBuf::from(&config.sequoia_home))?; + + // XXX: cleanup the userid before using + let (cert, _) = CertBuilder::new() + .add_signing_subkey() + .add_userid(cmd.userid) + .generate()?; + + if crypto::import_key(sequoia_home, &cert) { + let vc = cert.with_policy(&context.policy, None)?; + crypto::set_signing_key(&context, &vc)?; + + println!("New signing key: {}", vc.fingerprint()); + } + + Ok(()) +} diff --git a/src/commands/signkey/set.rs b/src/commands/signkey/set.rs new file mode 100644 index 0000000..8c276a0 --- /dev/null +++ b/src/commands/signkey/set.rs @@ -0,0 +1,69 @@ +//! signkey set command + +use anyhow::Result; +use sequoia_cert_store::Store; +use sequoia_openpgp::cert::ValidCert; +use sequoia_openpgp::cert; +use sequoia_openpgp::parse::Parse; +use sequoia_openpgp::{Fingerprint, KeyHandle}; +use sequoia_openpgp::types::RevocationStatus; + +use crate::cli::cli_args::SignkeySetCommand; +use crate::commands::CommandError; +use crate::common::crypto; +use crate::config::HuskConfigContainer; +use crate::types::husk_context::HuskContext; + +pub async fn dispatch(cmd: SignkeySetCommand, config: HuskConfigContainer) + -> Result<()> { + let context = HuskContext::new(&config.into())?; + let policy = &context.policy; + let cert_store = &context.cert_store; + + if let Some(cert_file) = cmd.cert_file { + let c = cert::Cert::from_file(cert_file)?; + let vc = c.with_policy(policy, None)?; + + // guards + if vc.alive().is_err() { + return Err(CommandError::CertNotAlive(vc.fingerprint()).into()); + } + if matches!(vc.revocation_status(), RevocationStatus::Revoked(_)) { + return Err(CommandError::CertRevoked(vc.fingerprint()).into()); + } + if !crypto::can_sign(&vc) { + return Err(CommandError::CertNotUsable(vc.fingerprint()).into()); + } + + // set the signing key + crypto::set_signing_key( + &context, + &vc + )?; + + } else if let Some(cert) = cmd.cert { + let fpr = Fingerprint::from_hex(cert.as_str())?; + let certs = cert_store + .lookup_by_cert(&KeyHandle::from(&fpr)) + .map_err(|_| CommandError::CertNotFound(fpr.clone()))?; + + let certs: Vec = certs.iter() + .filter_map(|c| + c.with_policy(policy, None).ok() + ) + .filter(|vc| crypto::can_sign(vc)) + .collect(); + + if let Some(cert) = certs.first() { + crypto::set_signing_key( + &context, + cert + )?; + } else { + return Err(CommandError::CertNotUsable(fpr).into()); + } + } + + Ok(()) +} + diff --git a/src/common/crypto.rs b/src/common/crypto.rs index 575d5f7..acfe245 100644 --- a/src/common/crypto.rs +++ b/src/common/crypto.rs @@ -3,19 +3,25 @@ use std::io::Write; use std::sync::Arc; use std::time::Duration; +use std::time::SystemTime; use std::collections::HashMap; use std::collections::HashSet; use std::collections::hash_map::Entry::{Occupied, Vacant}; use anyhow; use sequoia_openpgp::cert::ValidCert; +use sequoia_openpgp::packet::signature::subpacket::NotationDataFlags; use tokio::task::JoinSet; use bytes::Bytes; use sequoia_net::{KeyServer, wkd, dane}; +use sequoia_directories::Home; +use sequoia_keystore::Keystore; +use sequoia_keystore::Context; +use sequoia_keystore::Key; use sequoia_openpgp::packet::prelude::SignatureBuilder; use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse}; use sequoia_openpgp::policy::StandardPolicy; -use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, LiteralWriter}; +use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, Signer, LiteralWriter}; use sequoia_openpgp::{Fingerprint, Cert, Packet}; use sequoia_openpgp::cert::raw::RawCertParser; use sequoia_openpgp::types::SignatureType; @@ -32,6 +38,10 @@ use crate::types::recipient::Recipient; use crate::mail; use crate::common::escape; +/// Signing keys used by Husk must have a certification with a +/// notation of the name `HUSK_SIG_NOTATION`. +const HUSK_SIG_NOTATION: &str = "signing_key@husk-project.org"; + /// Returns the local trust root from the cert_store. If the trust root /// cannot be found, it returns an error. pub fn get_local_trust_root(cert_store: &CertStore) -> anyhow::Result { @@ -413,10 +423,21 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec anyhow::Result<()> { + message.write_all(body)?; + message.finalize()?; + Ok(()) + }); + }); Ok(Bytes::from(sink)) } else { @@ -429,6 +450,7 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec>) +pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option>) -> anyhow::Result<()> { + internal_authenticate(context, cert, role, activate, domains, None) +} + +/// Make `cert` the new signing key. +pub fn set_signing_key(context: &HuskContext, cert: &ValidCert) -> anyhow::Result<()> { + internal_authenticate(context, cert, Role::SigningKey, true, None, Some((HUSK_SIG_NOTATION.to_string(), "placeholder".to_string()))) +} + +/// Internal authentication method. +fn internal_authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option>, notation: Option<(String, String)>) -> anyhow::Result<()> { let policy = &context.policy; @@ -470,7 +502,7 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activat if activate { // set the trust level and depth for an active local/introducer. - if role == Role::Local { + if role == Role::Local || role == Role::SigningKey { builder = builder.set_trust_signature(0, 120)?; } else { // Introducer @@ -502,6 +534,14 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activat builder = builder.set_trust_signature(0, 1)?; } + if let Some((name, value)) = notation { + builder = builder.set_notation( + name, + value, + NotationDataFlags::empty().set_human_readable(), + false)?; + } + // For a certification, user ids are needed. Build a list of // valid (non self revoked) user ids. let user_ids: Vec<_> = cert.userids() @@ -559,9 +599,140 @@ pub fn can_encrypt(cert: &ValidCert) -> bool { .count() > 0 } +/// Returns true if `cert` can be used for signing. +pub fn can_sign(cert: &ValidCert) -> bool { + cert.keys() + .for_signing() + .alive() + .supported() + .revoked(false) + .count() > 0 +} + + +/// Get Husks signing key +pub fn get_signing_key(sequoia_home: Home, cert_store: &CertStore<'_>, policy: &StandardPolicy<'_>, local_trust_root: &Cert) -> anyhow::Result> { + + let local_trust_root = local_trust_root.with_policy(policy, None)?; + let local_trust_fpr = local_trust_root.fingerprint(); + + let mut newest = None; + let mut result = None; + + let keys = get_all_keys(sequoia_home); + for key in keys { + if let Ok(cert) = cert_store.lookup_by_cert_fpr(&key.fingerprint()) { + if let Ok(vc) = cert.with_policy(policy, None) { + if let Some(time) = contains_signing_key(&vc, &local_trust_fpr) { + if let Some(othertime) = newest { + if time > othertime { + newest = Some(time); + result = Some(key.clone()); + } + } else { // first detected key + newest = Some(time); + result = Some(key.clone()); + } + } + } + } + } + Ok(result) +} + +/// If `vc` is a certificate marked as a Husk signing key, return the +/// creation time of the signature - else return `None`. +pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Option { + log::debug!("check {} for signing key", vc.fingerprint()); + for uid in vc.userids() { + for sig in uid.certifications() { + if sig.issuer_fingerprints().any(|i| i == local_root_fpr) + && sig.notation(HUSK_SIG_NOTATION).count() > 0 { + + // ignore signatures without creation time + if let Some(time) = sig.signature_creation_time() { + log::debug!("found"); + return Some(time); + } + } + } + } + None +} + +/// Get all keys from all backends +// XXX: Maybe the softkeys backend is sufficient +pub fn get_all_keys(sequoia_home: Home) -> Vec { + + let mut result = Vec::new(); + std::thread::scope(|s| { + s.spawn(|| { + if let Ok(keystore_base) = Context::configure() + .home(sequoia_home.data_dir(sequoia_directories::Component::Keystore)) + .build() { + if let Ok(mut keystore) = Keystore::connect(&keystore_base) { + if let Ok(mut backends) = keystore.backends() { + for backend in &mut backends { + log::debug!("scanning backend {:?}", backend.id()); + if let Ok(devices) = backend.devices() { + for mut device in devices { + if let Ok(keys) = device.keys() { + keys.iter().for_each(|k| { + result.push(k.clone()); + }); + } + } + } + } + } + } + } + }); + }); + result +} + +pub fn import_key(sequoia_home: Home, cert: &Cert) -> bool { + let mut imported = false; + std::thread::scope(|s| { + s.spawn(|| { + if let Ok(keystore_base) = Context::configure() + .home(sequoia_home.data_dir(sequoia_directories::Component::Keystore)) + .build() { + if let Ok(mut keystore) = Keystore::connect(&keystore_base) { + if let Ok(mut backends) = keystore.backends() { + for backend in &mut backends { + if let Ok(id) = backend.id() { + if id == "softkeys" + && backend.import(cert).is_ok() { + imported = true; + } + } + } + } + } + } + }); + }); + + imported +} #[cfg(test)] pub mod tests { + use sequoia_cert_store::store::CertD; + use sequoia_cert_store::{CertStore, LazyCert, StoreUpdate}; + use sequoia_keystore::Context; + use sequoia_keystore::Keystore; + use sequoia_directories::Home; + use sequoia_directories::Component; + use sequoia_openpgp::{cert::CertBuilder, policy::StandardPolicy}; + use std::sync::Arc; + + use crate::common::crypto::get_all_keys; + use crate::types::{husk_context::HuskContext, mail_context::MailContext}; + + use super::get_signing_key; #[test] pub fn test_is_encrypted() { @@ -644,4 +815,81 @@ jk6lh01Ej9QTGQ== } + #[test] + fn test_signing_key() -> anyhow::Result<()> { + + let temp_dir = tempfile::tempdir()?; + let temp_path = temp_dir.path().to_path_buf(); + let home = Home::new(temp_path)?; + + // build the keystore + let keystore_home = home.data_dir(Component::Keystore); + let c = Context::configure() + .home(keystore_home) + .build()?; + let mut ks = Keystore::connect(&c)?; + + // get the softkeys backend + let mut softkeys = None; + for mut backend in ks.backends()?.into_iter() { + if backend.id().expect("backend id") == "softkeys" { + softkeys = Some(backend); + } + } + assert!(softkeys.is_some()); + let mut softkeys = softkeys.unwrap(); + + // build the certstore + let cert_store_base = home.data_dir(sequoia_directories::Component::CertD); + std::fs::create_dir_all(&cert_store_base)?; + let _ = CertD::open(&cert_store_base)?; + let store = CertStore::open(cert_store_base)?; + + // build the trust root + let (root, _) = CertBuilder::general_purpose(Some("local root")) + .add_signing_subkey() + .generate()?; + + // create a skeleton HuskContext for later use. + let context = HuskContext { + cert_store: store, + policy: StandardPolicy::new(), + local_trust_root: root, + signing_key: None, + introducers: Vec::new(), + keyservers: Vec::new(), + subject_replacement: "...".into(), + mail: MailContext::new() + }; + + // store the local trust root + context.cert_store.update(Arc::new(LazyCert::from(context.local_trust_root.clone())))?; + + // the siging key + let (signing_key, _) = CertBuilder::general_purpose(Some("signing key")) + .add_signing_subkey() + .generate()?; + let valid_signing_cert = signing_key.with_policy(&context.policy, None)?; + let signing_fpr = signing_key.fingerprint(); + softkeys.import(&signing_key)?; + + let keys= get_all_keys(home.clone()); + for key in keys { eprintln!("{}", key.fingerprint()); } + + super::set_signing_key(&context, &valid_signing_cert)?; + + // get the Husk signing key + let key = get_signing_key(home, + &context.cert_store, + &context.policy, + &context.local_trust_root)?; + + assert!(key.is_some()); + let fpr = key.unwrap().fingerprint(); + + assert_eq!(signing_fpr, fpr); + + Ok(()) + } + } diff --git a/src/config.rs b/src/config.rs index 75c7947..1c9afac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,13 +9,14 @@ use toml; use log4rs; use crate::types::errors::HuskError; -use crate::types::defaults::CONSOLE_LOGGING; +use crate::types::defaults::{CONSOLE_LOGGING, SUBJECT_REPLACEMENT}; #[derive(Deserialize, Debug, Clone)] pub struct HuskConfig { pub connection: String, pub sequoia_home: String, pub keyservers: Option>, + pub subject_replacement: Option, logging: Option, } @@ -68,6 +69,13 @@ impl HuskConfig { } } + pub fn get_subject_replacement(&self) -> String { + if let Some(replacement) = &self.subject_replacement { + replacement.clone() + } else { + SUBJECT_REPLACEMENT.to_string() + } + } } pub type HuskConfigContainer = Arc>; @@ -79,3 +87,23 @@ impl From for HuskConfig { } } +#[cfg(test)] +mod tests { + use crate::types::defaults; + + use super::HuskConfig; + + + #[test] + fn subject_line() { + let config = HuskConfig { + connection: "".into(), + sequoia_home: "".into(), + keyservers: None, + subject_replacement: None, + logging: None + }; + + assert_eq!(config.get_subject_replacement(), defaults::SUBJECT_REPLACEMENT); + } +} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index e071ff3..9cab8db 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -241,6 +241,11 @@ impl Daemon { } } } + // Change the subject + let _ = cx.actions.change_header( + "Subject", + 1, + Some(context.subject_replacement.clone())).await; }, Err(e) => { log::error!("{}", e); diff --git a/src/types/defaults.rs b/src/types/defaults.rs index ab955ea..e9b4d8f 100644 --- a/src/types/defaults.rs +++ b/src/types/defaults.rs @@ -1,9 +1,13 @@ //! Defaults -pub const CONFIG_FILE_LOCATION: &'static str = "/etc/husk/config.toml"; +pub const CONFIG_FILE_LOCATION: &str = "/etc/husk/config.toml"; + +/// If encrypting a mail, the subject is moved into the encrypted body +/// and replaced by `SUBJECT_REPLACEMENT` (unless configured otherwise). +pub const SUBJECT_REPLACEMENT: &str = "..."; /// Default logging configuration. -pub const CONSOLE_LOGGING: &'static str = " +pub const CONSOLE_LOGGING: &str = " [appenders.console] kind = \"console\" diff --git a/src/types/husk_context.rs b/src/types/husk_context.rs index 3ca6dce..9efac1d 100644 --- a/src/types/husk_context.rs +++ b/src/types/husk_context.rs @@ -15,6 +15,7 @@ use sequoia_openpgp::policy::StandardPolicy; use sequoia_openpgp::Cert; use sequoia_cert_store::CertStore; use sequoia_directories::Home; +use sequoia_keystore::Key; use crate::config::HuskConfig; use crate::common::crypto; @@ -26,8 +27,10 @@ pub struct HuskContext<'hc> { pub policy: StandardPolicy<'hc>, pub cert_store: CertStore<'hc>, pub local_trust_root: Cert, + pub signing_key: Option, pub introducers: Vec, pub keyservers: Vec, + pub subject_replacement: String, pub mail: MailContext<'hc>, } @@ -45,12 +48,26 @@ impl<'hc> HuskContext<'hc> { let local_trust_root = crypto::get_local_trust_root(&cert_store)?; let introducers = crypto::get_introducers(&cert_store, &policy, local_trust_root.fingerprint()); + let signing_key = match crypto::get_signing_key(sequoia_home, &cert_store, &policy, &local_trust_root) { + Ok(Some(cert)) => Some(cert), + Ok(None) => { + log::warn!("No signing key found"); + None + }, + Err(e) => { + log::error!("Error while fetching signing key: {}", e); + None + } + }; + Ok(HuskContext { policy, cert_store, local_trust_root, + signing_key, introducers, keyservers: config.get_keyservers(), + subject_replacement: config.get_subject_replacement(), mail: MailContext::new(), }) }