Merge branch 'malte/signing' into 'main'

Signing outgoing mails

See merge request husk-project/husk-milter!6
This commit is contained in:
Malte Meiboom
2026-06-24 09:29:16 +00:00
17 changed files with 1047 additions and 24 deletions
+1 -1
View File
@@ -3,6 +3,6 @@ stages:
test-job:
stage: test
image: localhost:5000/huskbuilder:latest
image: localhost:5000/mta:latest
script:
- cargo test
Generated
+407 -8
View File
@@ -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",
+3
View File
@@ -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"
+2
View File
@@ -42,6 +42,8 @@ RUN apt-get update -yqq && \
libssl-dev \
llvm \
nettle-dev \
capnproto \
libsqlite3-dev \
pkg-config
##
+1 -1
View File
@@ -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
}
+71
View File
@@ -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
+81 -4
View File
@@ -18,7 +18,7 @@ pub struct CliArgs {
#[clap(
long,
global = true,
help = "Certification store (SEQUOIA_HOME).",
help = "Certificate store (SEQUOIA_HOME).",
)]
pub home: Option<String>,
@@ -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<String>,
@@ -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 <FILE>.",
value_name = "FILE",
conflicts_with = "cert"
)]
pub cert_file: Option<String>,
#[clap(
long = "cert",
help = "Certificate to declare as signing key.",
conflicts_with = "cert_file",
value_name = "FINGERPRINT",
)]
pub cert: Option<String>,
}
#[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,
}
+6
View File
@@ -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(())
+1
View File
@@ -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() {
+58
View File
@@ -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(())
}
+35
View File
@@ -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(())
}
+69
View File
@@ -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<ValidCert> = 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(())
}
+255 -7
View File
@@ -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<Cert> {
@@ -413,10 +423,21 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec<Recipien
let mut sink = Vec::new();
let message = Message::new(&mut sink);
let message = Armorer::new(message).build()?;
let message = Encryptor::for_recipients(message, keys).build()?;
let mut message = LiteralWriter::new(message).build()?;
message.write_all(&body[..])?;
message.finalize()?;
let mut message = Encryptor::for_recipients(message, keys).build()?;
if let Some(key) = &context.signing_key {
log::debug!("use signing key {}", key.fingerprint());
message = Signer::new(message, key.clone())?.build()?;
}
message = LiteralWriter::new(message).build()?;
// XXX: error handling!!!
std::thread::scope(|s| {
s.spawn(|| -> 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<Recipien
pub enum Role {
Local,
Introducer,
SigningKey,
}
/// Authenticate a certificate.
@@ -437,7 +459,17 @@ pub enum Role {
/// `domains`. Passing `None` or an empty list in this case is an error.
/// If `activate` is false, create a certifiaction with minimal trust_roots
/// amount and no depth. In this case `domains` is ignored.
pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option<Vec<String>>)
pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option<Vec<String>>) -> 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<Vec<String>>, 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<Option<Key>> {
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<SystemTime> {
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<Key> {
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(())
}
}
+29 -1
View File
@@ -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<Vec<String>>,
pub subject_replacement: Option<String>,
logging: Option<toml::Value>,
}
@@ -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<Mutex<HuskConfig>>;
@@ -79,3 +87,23 @@ impl From<HuskConfigContainer> 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);
}
}
+5
View File
@@ -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);
+6 -2
View File
@@ -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\"
+17
View File
@@ -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<Key>,
pub introducers: Vec<Introducer>,
pub keyservers: Vec<String>,
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(),
})
}