Compare commits
No commits in common. "1468f3cbc0a89a0cb073f46a985911c0327885d1" and "96a558da161215d965373d61380ab89f6736e032" have entirely different histories.
1468f3cbc0
...
96a558da16
11 changed files with 54 additions and 910 deletions
643
Cargo.lock
generated
643
Cargo.lock
generated
|
|
@ -105,17 +105,6 @@ version = "1.0.104"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
|
|
@ -146,12 +135,6 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.5.1"
|
||||
|
|
@ -338,17 +321,6 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "displaydoc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
|
|
@ -374,18 +346,6 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
|
|
@ -410,15 +370,6 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fumi"
|
||||
version = "0.1.0"
|
||||
|
|
@ -427,7 +378,6 @@ dependencies = [
|
|||
"clap",
|
||||
"data-encoding",
|
||||
"fumi-core",
|
||||
"hickory-resolver",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
|
|
@ -450,45 +400,6 @@ dependencies = [
|
|||
"x25519-dalek",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
|
|
@ -544,51 +455,6 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hickory-proto"
|
||||
version = "0.24.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
"data-encoding",
|
||||
"enum-as-inner",
|
||||
"futures-channel",
|
||||
"futures-io",
|
||||
"futures-util",
|
||||
"idna",
|
||||
"ipnet",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hickory-resolver"
|
||||
version = "0.24.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"hickory-proto",
|
||||
"ipconfig",
|
||||
"lru-cache",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"resolv-conf",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.12.4"
|
||||
|
|
@ -607,110 +473,6 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
"utf8_iter",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
|
|
@ -720,25 +482,6 @@ dependencies = [
|
|||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipconfig"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
|
||||
dependencies = [
|
||||
"socket2",
|
||||
"widestring",
|
||||
"windows-registry",
|
||||
"windows-result",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
|
|
@ -768,53 +511,12 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-cache"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
|
|
@ -833,41 +535,6 @@ version = "0.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
|
|
@ -907,24 +574,6 @@ dependencies = [
|
|||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
|
|
@ -943,27 +592,6 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
|
|
@ -973,21 +601,6 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resolv-conf"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
|
|
@ -1011,12 +624,6 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
|
|
@ -1092,12 +699,6 @@ dependencies = [
|
|||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.2"
|
||||
|
|
@ -1120,16 +721,6 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
|
|
@ -1140,12 +731,6 @@ dependencies = [
|
|||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
|
@ -1180,98 +765,6 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
|
|
@ -1294,24 +787,6 @@ dependencies = [
|
|||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
|
|
@ -1336,47 +811,12 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
|
|
@ -1386,12 +826,6 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
||||
|
||||
[[package]]
|
||||
name = "x25519-dalek"
|
||||
version = "2.0.1"
|
||||
|
|
@ -1404,29 +838,6 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
||||
dependencies = [
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.59"
|
||||
|
|
@ -1447,27 +858,6 @@ dependencies = [
|
|||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.0"
|
||||
|
|
@ -1488,39 +878,6 @@ dependencies = [
|
|||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
|
|
|
|||
|
|
@ -17,10 +17,6 @@ smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a short form over
|
|||
smol+rns://alice@8f2c1d0a7b6e5f4c3d2a1b0e9f8c7d6a/mfrggzdfzt... self-certifying over Reticulum
|
||||
```
|
||||
|
||||
When a TCP address carries no port, the CLI discovers one by DNS SRV before dialing: `_smolmail._tcp.<host>` states the target and port serving that domain's mail (lowest priority wins, weight ignored), the discovered target is only a dial hint, and the domain keeps every identity role. No record, a failing lookup, or an explicit port means the default 1961 — the feature is purely additive, and old deployments never notice it existed. Discovery runs when an address is given; a stored account is dialed at the port it was registered with.
|
||||
|
||||
Server pins are scoped by port: `trust example.org` pins the default port, and `trust example.org:1962` pins any other — one host may serve several domains, each with its own key. The fallback is asymmetric by design: the default port inherits the bare-host pin existing stores already hold, and a non-default port never falls back to it — the first dial at a new port is trust on first use, like any first contact, rather than a mismatch against its sibling's key.
|
||||
|
||||
## Usage
|
||||
|
||||
One master per store, selected by the global `--key` and `--db` flags; a second identity is a second pair of files.
|
||||
|
|
@ -35,7 +31,7 @@ fumi restore <address> recover local state from the master a
|
|||
fumi rotate advance the rotation index, sign and push the certificate
|
||||
|
||||
# servers and contacts
|
||||
fumi trust <host[:port]> <key-b32> [--force] pin a server's Noise static key, scoped by port
|
||||
fumi trust <host> <key-b32> [--force] pin a server's Noise static key
|
||||
fumi register <address> [--invite TOKEN] bind this identity to a username
|
||||
fumi resolve <address> look up a contact's key, walk the chain, pin it
|
||||
fumi import <smol-uri> add a contact from a self-certifying address
|
||||
|
|
@ -100,7 +96,7 @@ Every mail column holds a sealed envelope, never plaintext. Timestamps are Unix
|
|||
| Table | Columns | Meaning and invariants |
|
||||
|---|---|---|
|
||||
| `state` | one row, `id = 1`, always present | `username`, `host`, `port`, `scheme` (`tcp` or `rns`) are the account; `rotations` is the sec 2 rotation index the account is at; `after_time` and `after_id` are the sec 6.1 fetch cursor; `sync_ok` (0/1) says whether the local accept-token set may replace the server's — a client restored from the master alone must not erase it (sec 4) |
|
||||
| `servers` | `host`, `static`, `pinned_at` | The sec 4 pins: a server's Noise static key, keyed by host on the default port and by `host:port` on any other — the same label the trust errors name |
|
||||
| `servers` | `host`, `static`, `pinned_at` | The sec 4 pins: a server's Noise static key, keyed by host without port |
|
||||
| `contacts` | `address`, `identity`, `verified`, `seen_at` | Every key known for an address; `verified` is 1 only when it came from a self-certifying URI |
|
||||
| `accepted` | `address`, `identity`, `active`, `added_at` | Main-tier admission (sec 5.8). `identity` is frozen at acceptance because the token the correspondent holds is derived from it; `active = 0` is a block and keeps the row |
|
||||
| `tokens` | `address`, `token`, `seen_at` | Accept tokens correspondents issued us, filed under their address, which outlives the keys behind it (sec 5.8) |
|
||||
|
|
|
|||
|
|
@ -20,4 +20,3 @@ clap = { version = "4", features = ["derive"] }
|
|||
anyhow = "1"
|
||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
data-encoding = "2"
|
||||
hickory-resolver = "0.24"
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ use fumi::client::{self, Pushed, SendDraft, TrustChange};
|
|||
use fumi::crypto::{b32, fingerprint, unb32, KEY_LEN};
|
||||
|
||||
use fumi::store::Store;
|
||||
|
||||
mod srv;
|
||||
use fumi::transport::ID_LEN;
|
||||
|
||||
#[derive(Parser)]
|
||||
|
|
@ -172,9 +170,8 @@ fn main() {
|
|||
/// reports the distinction; only the CLI knows the command.
|
||||
fn hint(error: Option<&fumi::error::Error>) {
|
||||
match error {
|
||||
Some(fumi::error::Error::NotPinned { host, port }) => eprintln!(
|
||||
"obtain the key from the operator through a trusted channel, then:\n fumi trust {} <key>",
|
||||
fumi::address::trust_label(host, *port)
|
||||
Some(fumi::error::Error::NotPinned { host }) => eprintln!(
|
||||
"obtain the key from the operator through a trusted channel, then:\n fumi trust {host} <key>"
|
||||
),
|
||||
Some(fumi::error::Error::KeyChanged { address, offered, .. }) => {
|
||||
if let Ok(addr) = Address::parse(address) {
|
||||
|
|
@ -327,39 +324,9 @@ fn whoami(key: &PathBuf, store: &Store) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses an address and applies SRV discovery when it is TCP and carried no
|
||||
/// explicit port (agreed with bunshin 2026-09-30): the discovered port scopes
|
||||
/// the pin and the SRV target is only a dial hint, so the domain keeps every
|
||||
/// identity role. A miss keeps the default port — one DNS lookup on the
|
||||
/// failure path and nothing else. Discovery happens when an address is
|
||||
/// given; the stored account is dialed at the port it was registered with,
|
||||
/// so an operator moving ports does not move existing accounts.
|
||||
fn parse_dial_address(raw: &str) -> Result<Address> {
|
||||
let mut addr = Address::parse(raw)?;
|
||||
if addr.scheme == fumi::address::Scheme::Tcp && !port_is_explicit(raw) {
|
||||
if let Some(endpoint) = srv::discover(&addr.host) {
|
||||
addr.port = endpoint.port;
|
||||
addr = addr.with_dial(&endpoint.target);
|
||||
}
|
||||
}
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Whether the address text states a port after the host: the same
|
||||
/// right-hand split `trust` uses, over the host part of either address
|
||||
/// form, so an invalid suffix is not a port and an explicit port is never
|
||||
/// overridden by discovery.
|
||||
fn port_is_explicit(raw: &str) -> bool {
|
||||
let host_part = raw.rsplit_once('@').map_or(raw, |(_, host)| host);
|
||||
let host_part = host_part.split('/').next().unwrap_or(host_part);
|
||||
host_part
|
||||
.rsplit_once(':')
|
||||
.is_some_and(|(_, port)| port.parse::<u16>().is_ok())
|
||||
}
|
||||
|
||||
fn restore(key: &PathBuf, store: &Store, address: &str, timeout: u64) -> Result<()> {
|
||||
let master = load_master(key)?;
|
||||
let addr = parse_dial_address(address)?;
|
||||
let addr = Address::parse(address)?;
|
||||
let index = client::restore(store, &master, &addr, timeout)?;
|
||||
let identity = fumi::account::Identity::from_seed(identity_seed(&master, index));
|
||||
println!("restored {} at rotation {index}", addr.short());
|
||||
|
|
@ -391,30 +358,21 @@ fn trust(store: &Store, host: &str, key_b32: &str, force: bool) -> Result<()> {
|
|||
v.len()
|
||||
))
|
||||
})?;
|
||||
// A pin is scoped per server and port (address::trust_label): the port
|
||||
// splits from the right so a bare host means the default port, and an
|
||||
// explicit default port normalizes onto the same bare row existing
|
||||
// stores already hold.
|
||||
let (host, port) = match host.rsplit_once(':') {
|
||||
Some((h, p)) => match p.parse::<u16>() {
|
||||
Ok(port) => (h, port),
|
||||
Err(_) => (host, fumi::address::DEFAULT_PORT),
|
||||
},
|
||||
None => (host, fumi::address::DEFAULT_PORT),
|
||||
};
|
||||
match store.server_pin(host, port)? {
|
||||
// A pin is per server, and the reference client strips any :port the
|
||||
// same way its address grammar does: at the first colon.
|
||||
let host = host.split(':').next().unwrap_or(host);
|
||||
match store.server_pin(host)? {
|
||||
Some(old) if old != key && !force => {
|
||||
return Err(anyhow!(format!(
|
||||
"{} is already pinned to {}; use --force to replace",
|
||||
fumi::address::trust_label(host, port),
|
||||
"{host} is already pinned to {}; use --force to replace",
|
||||
b32(&old)
|
||||
))
|
||||
.into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
store.pin_server(host, port, &key)?;
|
||||
println!("pinned {} {}", fumi::address::trust_label(host, port), b32(&key));
|
||||
store.pin_server(host, &key)?;
|
||||
println!("pinned {host} {}", b32(&key));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -426,7 +384,7 @@ fn register(
|
|||
timeout: u64,
|
||||
) -> Result<()> {
|
||||
let account = load_account(key, store)?;
|
||||
let addr = parse_dial_address(address)?;
|
||||
let addr = Address::parse(address)?;
|
||||
client::register(store, &addr, &account, invite, timeout)?;
|
||||
println!("registered {}", addr.short());
|
||||
println!("share: {}", addr.uri(&account.me().pk()));
|
||||
|
|
@ -434,7 +392,7 @@ fn register(
|
|||
}
|
||||
|
||||
fn resolve(store: &Store, address: &str, timeout: u64) -> Result<()> {
|
||||
let addr = parse_dial_address(address)?;
|
||||
let addr = Address::parse(address)?;
|
||||
if addr.identity.is_some() {
|
||||
return Err(
|
||||
anyhow!("that address already carries a key; use `import` instead").into(),
|
||||
|
|
@ -591,7 +549,7 @@ fn send(cli: &Cli, store: &Store) -> Result<()> {
|
|||
unreachable!("send dispatches only from the Send command")
|
||||
};
|
||||
let account = load_account(&cli.key, store)?;
|
||||
let addr = parse_dial_address(address)?;
|
||||
let addr = Address::parse(address)?;
|
||||
let text = match (body, file) {
|
||||
(Some(text), _) => text.clone(),
|
||||
(None, Some(path)) if path == "-" => read_stdin()?,
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
//! SRV discovery for portless TCP addresses (agreed with bunshin
|
||||
//! 2026-09-30): `_smolmail._tcp.<host>` states which target and port serve a
|
||||
//! domain's mail. Discovery is routing only — a lying record lands the client
|
||||
//! on a server with the wrong static key, where the handshake dies exactly as
|
||||
//! it does today; the pin stays out of band. No record or a failing lookup
|
||||
//! means the default port, so the feature is purely additive and old
|
||||
//! deployments never notice it existed.
|
||||
|
||||
/// One endpoint as an SRV record states it.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Endpoint {
|
||||
/// The host to dial — a routing fact only, never an identity.
|
||||
pub target: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
/// One SRV record, in the shape `pick` reasons about.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Record {
|
||||
pub priority: u16,
|
||||
pub target: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
/// The label the convention reserves (ratified with bunshin): it matches the
|
||||
/// RNS destination `smolmail.server` and the reference client's name.
|
||||
pub const SERVICE: &str = "_smolmail._tcp";
|
||||
|
||||
/// Picks the endpoint: lowest priority wins and weight is ignored —
|
||||
/// fetching mail needs no client-side load balancing.
|
||||
pub fn pick(records: &[Record]) -> Option<&Record> {
|
||||
records.iter().min_by_key(|record| record.priority)
|
||||
}
|
||||
|
||||
/// Queries `_smolmail._tcp.<host>` through the system resolver. Discovery
|
||||
/// never fails an operation: any miss or error is simply no endpoint, and
|
||||
/// the caller keeps the default port.
|
||||
pub fn discover(host: &str) -> Option<Endpoint> {
|
||||
let name = format!("{SERVICE}.{host}.");
|
||||
let resolver = hickory_resolver::Resolver::from_system_conf().ok()?;
|
||||
let srv = resolver.srv_lookup(name).ok()?;
|
||||
let records: Vec<Record> = srv
|
||||
.iter()
|
||||
.map(|record| Record {
|
||||
priority: record.priority(),
|
||||
target: record.target().to_string(),
|
||||
port: record.port(),
|
||||
})
|
||||
.collect();
|
||||
pick(&records).map(|record| Endpoint {
|
||||
target: record.target.trim_end_matches('.').to_string(),
|
||||
port: record.port,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn lowest_priority_wins_and_weight_is_ignored() {
|
||||
let records = [
|
||||
Record { priority: 10, target: "a.example.net".into(), port: 1962 },
|
||||
Record { priority: 5, target: "b.example.net".into(), port: 1963 },
|
||||
Record { priority: 10, target: "c.example.net".into(), port: 1964 },
|
||||
];
|
||||
let picked = pick(&records).expect("records exist");
|
||||
assert_eq!((picked.target.as_str(), picked.port), ("b.example.net", 1963));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_records_is_no_endpoint() {
|
||||
assert!(pick(&[]).is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -5,22 +5,6 @@ use crate::crypto::{b32, unb32, KEY_LEN};
|
|||
use crate::error::Error;
|
||||
|
||||
pub const DEFAULT_PORT: u16 = 1961;
|
||||
|
||||
/// The trust label of a host: the bare host on the default port, `host:port`
|
||||
/// on any other. One rule for the store's pin keys and the trust errors, so
|
||||
/// a pin and its error always name the same trust object. The fallback is
|
||||
/// asymmetric by design: the default port inherits the legacy bare-host pin,
|
||||
/// and a non-default port never falls back to it — a second domain on one
|
||||
/// host earns its own trust on first use instead of a mismatch against its
|
||||
/// sibling's key.
|
||||
pub fn trust_label(host: &str, port: u16) -> String {
|
||||
if port == DEFAULT_PORT {
|
||||
host.to_string()
|
||||
} else {
|
||||
format!("{host}:{port}")
|
||||
}
|
||||
}
|
||||
|
||||
/// A Reticulum destination hash, as printed by every RNS tool: 32 lowercase
|
||||
/// hexadecimal characters (RNS.md sec 13.2).
|
||||
pub const RNS_HASH_HEX: usize = 32;
|
||||
|
|
|
|||
|
|
@ -79,11 +79,10 @@ pub fn connect(
|
|||
) -> Result<Session, Error> {
|
||||
match addr.scheme {
|
||||
Scheme::Tcp => {
|
||||
let pinned = store.server_pin(&addr.host, addr.port)?;
|
||||
let pinned = store.server_pin(&addr.host)?;
|
||||
if pinned.is_none() && require_pin {
|
||||
return Err(Error::NotPinned {
|
||||
host: addr.host.clone(),
|
||||
port: addr.port,
|
||||
});
|
||||
}
|
||||
// The dial hint lets a host that resolves DNS itself (an
|
||||
|
|
@ -95,19 +94,6 @@ pub fn connect(
|
|||
pinned,
|
||||
timeout,
|
||||
)?;
|
||||
// Pin enforcement lives here, one layer above the transport: the
|
||||
// identity host names the trust object even when a dial hint
|
||||
// routed the packets elsewhere (sec 4).
|
||||
if let Some(pinned) = pinned {
|
||||
if !ct_eq(&pinned, transport.server_static()) {
|
||||
return Err(Error::PinMismatch {
|
||||
host: addr.host.clone(),
|
||||
port: addr.port,
|
||||
pinned,
|
||||
presented: *transport.server_static(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let unpinned_static = if pinned.is_none() {
|
||||
Some(*transport.server_static())
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -26,14 +26,12 @@ pub enum Error {
|
|||
UnknownStatus(u8, String),
|
||||
/// The host has no pinned server key, so the session cannot be
|
||||
/// authenticated (sec 4). A GUI routes this to pinning; an auth failure
|
||||
/// is terminal. The port scopes the pin (address::trust_label).
|
||||
NotPinned { host: String, port: u16 },
|
||||
/// is terminal.
|
||||
NotPinned { host: String },
|
||||
/// A pinned server presented a different key (sec 4): a hard abort, the
|
||||
/// pin is the entire trust model. The port scopes the pin
|
||||
/// (address::trust_label).
|
||||
/// pin is the entire trust model.
|
||||
PinMismatch {
|
||||
host: String,
|
||||
port: u16,
|
||||
pinned: [u8; KEY_LEN],
|
||||
presented: [u8; KEY_LEN],
|
||||
},
|
||||
|
|
@ -91,20 +89,14 @@ impl fmt::Display for Error {
|
|||
Self::UnknownStatus(status, what) => {
|
||||
write!(f, "{what} failed: unknown status {status}")
|
||||
}
|
||||
Self::NotPinned { host, port } => write!(
|
||||
f,
|
||||
"no pinned key for {}",
|
||||
crate::address::trust_label(host, *port)
|
||||
),
|
||||
Self::NotPinned { host } => write!(f, "no pinned key for {host}"),
|
||||
Self::PinMismatch {
|
||||
host,
|
||||
port,
|
||||
pinned,
|
||||
presented,
|
||||
} => write!(
|
||||
f,
|
||||
"{} presented a different key than the one pinned\n pinned: {}\n presented: {}",
|
||||
crate::address::trust_label(host, *port),
|
||||
"{host} presented a different key than the one pinned\n pinned: {}\n presented: {}",
|
||||
b32(pinned),
|
||||
b32(presented)
|
||||
),
|
||||
|
|
|
|||
|
|
@ -249,21 +249,11 @@ pub fn import(store: &Store, master: &[u8; KEY_LEN], text: &str) -> Result<Impor
|
|||
|
||||
for (host, key) in &payload.servers {
|
||||
let key: Option<[u8; KEY_LEN]> = unb32(key).ok().and_then(|k| k.try_into().ok());
|
||||
// A backup pin key is a bare host (the default port) or host:port,
|
||||
// scoping exactly as the live store does; the port splits from the
|
||||
// right, so a host without a valid port suffix stays whole.
|
||||
let (host, port) = match host.rsplit_once(':') {
|
||||
Some((h, p)) => match p.parse::<u16>() {
|
||||
Ok(port) => (h.to_string(), port),
|
||||
Err(_) => (host.clone(), crate::address::DEFAULT_PORT),
|
||||
},
|
||||
None => (host.clone(), crate::address::DEFAULT_PORT),
|
||||
};
|
||||
match key {
|
||||
None => summary.malformed += 1,
|
||||
Some(key) => match store.server_pin(&host, port)? {
|
||||
Some(key) => match store.server_pin(host)? {
|
||||
None => {
|
||||
store.pin_server(&host, port, &key)?;
|
||||
store.pin_server(host, &key)?;
|
||||
summary.pins_added += 1;
|
||||
}
|
||||
Some(existing) if existing != key => summary.pins_conflicted += 1,
|
||||
|
|
@ -347,7 +337,7 @@ mod tests {
|
|||
fn seeded_store() -> (Store, [u8; KEY_LEN]) {
|
||||
let store = Store::open_in_memory().unwrap();
|
||||
let master = [7u8; KEY_LEN];
|
||||
store.pin_server("example.org", crate::address::DEFAULT_PORT, &[1u8; KEY_LEN]).unwrap();
|
||||
store.pin_server("example.org", &[1u8; KEY_LEN]).unwrap();
|
||||
store
|
||||
.save_contact("alice@example.org", &[2u8; KEY_LEN], true)
|
||||
.unwrap();
|
||||
|
|
@ -376,7 +366,7 @@ mod tests {
|
|||
assert_eq!(summary.contacts_added, 1);
|
||||
assert_eq!(summary.malformed, 0);
|
||||
|
||||
assert_eq!(fresh.server_pin("example.org", crate::address::DEFAULT_PORT).unwrap(), Some([1u8; KEY_LEN]));
|
||||
assert_eq!(fresh.server_pin("example.org").unwrap(), Some([1u8; KEY_LEN]));
|
||||
let (key, verified) = fresh.contact("alice@example.org").unwrap().unwrap();
|
||||
assert_eq!(key, [2u8; KEY_LEN]);
|
||||
assert!(verified);
|
||||
|
|
@ -405,13 +395,13 @@ mod tests {
|
|||
let file = export(&store, &master).unwrap();
|
||||
|
||||
let mine = Store::open_in_memory().unwrap();
|
||||
mine.pin_server("example.org", crate::address::DEFAULT_PORT, &[6u8; KEY_LEN]).unwrap();
|
||||
mine.pin_server("example.org", &[6u8; KEY_LEN]).unwrap();
|
||||
mine.save_contact("alice@example.org", &[7u8; KEY_LEN], false)
|
||||
.unwrap();
|
||||
let summary = import(&mine, &master, &file).unwrap();
|
||||
assert_eq!(summary.pins_conflicted, 1);
|
||||
assert_eq!(summary.contacts_conflicted, 1);
|
||||
assert_eq!(mine.server_pin("example.org", crate::address::DEFAULT_PORT).unwrap(), Some([6u8; KEY_LEN]));
|
||||
assert_eq!(mine.server_pin("example.org").unwrap(), Some([6u8; KEY_LEN]));
|
||||
let (key, _) = mine.contact("alice@example.org").unwrap().unwrap();
|
||||
assert_eq!(key, [7u8; KEY_LEN]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -276,12 +276,9 @@ impl Store {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn server_pin(&self, host: &str, port: u16) -> Result<Option<[u8; KEY_LEN]>, Error> {
|
||||
pub fn server_pin(&self, host: &str) -> Result<Option<[u8; KEY_LEN]>, Error> {
|
||||
Ok(self
|
||||
.one::<Option<Vec<u8>>>(
|
||||
"SELECT static FROM servers WHERE host = ?1",
|
||||
&[&crate::address::trust_label(host, port)],
|
||||
)?
|
||||
.one::<Option<Vec<u8>>>("SELECT static FROM servers WHERE host = ?1", &[&host])?
|
||||
.flatten()
|
||||
.and_then(|k| k.try_into().ok()))
|
||||
}
|
||||
|
|
@ -289,11 +286,9 @@ impl Store {
|
|||
/// Removes a pin: the settings screen's unpin, the inverse of
|
||||
/// `pin_server`. The next session against that host is trust on first
|
||||
/// use again (sec 4, sec 8).
|
||||
pub fn unpin_server(&self, host: &str, port: u16) -> Result<(), Error> {
|
||||
self.db().execute(
|
||||
"DELETE FROM servers WHERE host = ?1",
|
||||
params![&crate::address::trust_label(host, port)],
|
||||
)?;
|
||||
pub fn unpin_server(&self, host: &str) -> Result<(), Error> {
|
||||
self.db()
|
||||
.execute("DELETE FROM servers WHERE host = ?1", params![host])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -312,20 +307,12 @@ impl Store {
|
|||
.collect())
|
||||
}
|
||||
|
||||
/// Pins a server key, scoped by port (address::trust_label): the default
|
||||
/// port keeps the bare-host row existing stores already hold, and any
|
||||
/// other port earns its own row.
|
||||
pub fn pin_server(
|
||||
&self,
|
||||
host: &str,
|
||||
port: u16,
|
||||
key: &[u8; KEY_LEN],
|
||||
) -> Result<(), Error> {
|
||||
pub fn pin_server(&self, host: &str, key: &[u8; KEY_LEN]) -> Result<(), Error> {
|
||||
self.db()
|
||||
.execute(
|
||||
"INSERT INTO servers (host, static, pinned_at) VALUES (?1, ?2, ?3) \
|
||||
ON CONFLICT (host) DO UPDATE SET static = ?2, pinned_at = ?3",
|
||||
params![&crate::address::trust_label(host, port), key, now()],
|
||||
params![host, key, now()],
|
||||
)
|
||||
.map(|_| ())?;
|
||||
Ok(())
|
||||
|
|
@ -672,7 +659,6 @@ fn row_stored(row: &rusqlite::Row<'_>) -> rusqlite::Result<Stored> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use crate::account::{identity_seed, Account};
|
||||
use crate::address::DEFAULT_PORT;
|
||||
|
||||
fn master() -> [u8; 32] {
|
||||
core::array::from_fn(|i| i as u8)
|
||||
|
|
@ -812,11 +798,11 @@ mod tests {
|
|||
#[test]
|
||||
fn pins_and_contacts_round_trip() {
|
||||
let store = temp_store("pins");
|
||||
assert!(store.server_pin("example.org", DEFAULT_PORT).unwrap().is_none());
|
||||
store.pin_server("example.org", DEFAULT_PORT, &[5u8; 32]).unwrap();
|
||||
assert_eq!(store.server_pin("example.org", DEFAULT_PORT).unwrap(), Some([5u8; 32]));
|
||||
store.pin_server("example.org", DEFAULT_PORT, &[6u8; 32]).unwrap();
|
||||
assert_eq!(store.server_pin("example.org", DEFAULT_PORT).unwrap(), Some([6u8; 32]));
|
||||
assert!(store.server_pin("example.org").unwrap().is_none());
|
||||
store.pin_server("example.org", &[5u8; 32]).unwrap();
|
||||
assert_eq!(store.server_pin("example.org").unwrap(), Some([5u8; 32]));
|
||||
store.pin_server("example.org", &[6u8; 32]).unwrap();
|
||||
assert_eq!(store.server_pin("example.org").unwrap(), Some([6u8; 32]));
|
||||
|
||||
let key = Account::new(master(), 1).unwrap().me().pk();
|
||||
assert!(store.contact("bob@example.org").unwrap().is_none());
|
||||
|
|
@ -927,48 +913,14 @@ mod tests {
|
|||
assert_eq!(store.accepted_state("a@example.org").unwrap(), Some(false));
|
||||
}
|
||||
|
||||
/// Pins scope by port with an asymmetric fallback: the default port
|
||||
/// inherits the bare-host row, and any other port never falls back to
|
||||
/// it — a second server on one host earns its own trust on first use,
|
||||
/// not a mismatch against its sibling's key.
|
||||
#[test]
|
||||
fn pins_scope_by_port_asymmetrically() {
|
||||
let store = temp_store("pin-scope");
|
||||
store
|
||||
.pin_server("example.org", DEFAULT_PORT, &[1u8; KEY_LEN])
|
||||
.unwrap();
|
||||
store
|
||||
.pin_server("example.org", 1962, &[2u8; KEY_LEN])
|
||||
.unwrap();
|
||||
// The default port reads the legacy bare-host row.
|
||||
assert_eq!(
|
||||
store.server_pin("example.org", DEFAULT_PORT).unwrap(),
|
||||
Some([1u8; KEY_LEN])
|
||||
);
|
||||
// A non-default port reads only its own scoped row.
|
||||
assert_eq!(
|
||||
store.server_pin("example.org", 1962).unwrap(),
|
||||
Some([2u8; KEY_LEN])
|
||||
);
|
||||
// The rule: no fallback to the bare row from another port.
|
||||
assert_eq!(store.server_pin("example.org", 1963).unwrap(), None);
|
||||
// Unpin is scoped too; the default-port row survives it.
|
||||
store.unpin_server("example.org", 1962).unwrap();
|
||||
assert_eq!(store.server_pin("example.org", 1962).unwrap(), None);
|
||||
assert_eq!(
|
||||
store.server_pin("example.org", DEFAULT_PORT).unwrap(),
|
||||
Some([1u8; KEY_LEN])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unpin_returns_to_trust_on_first_use() {
|
||||
let store = temp_store("unpin");
|
||||
store.pin_server("example.org", DEFAULT_PORT, &[1u8; KEY_LEN]).unwrap();
|
||||
store.unpin_server("example.org", DEFAULT_PORT).unwrap();
|
||||
assert_eq!(store.server_pin("example.org", DEFAULT_PORT).unwrap(), None);
|
||||
store.pin_server("example.org", &[1u8; KEY_LEN]).unwrap();
|
||||
store.unpin_server("example.org").unwrap();
|
||||
assert_eq!(store.server_pin("example.org").unwrap(), None);
|
||||
// Unpinning what was never pinned is not an error.
|
||||
store.unpin_server("never.example.org", DEFAULT_PORT).unwrap();
|
||||
store.unpin_server("never.example.org").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use std::time::Duration;
|
|||
|
||||
use snow::{Builder, TransportState};
|
||||
|
||||
use crate::crypto::KEY_LEN;
|
||||
use crate::crypto::{ct_eq, KEY_LEN};
|
||||
use crate::error::Error;
|
||||
use crate::transport::{
|
||||
Response, Transport, TransportBindValues, MAX_FRAME, NOISE_PARAMS, NOISE_PAYLOAD, PROLOGUE,
|
||||
|
|
@ -40,11 +40,16 @@ impl TcpTransport {
|
|||
let mut refused = None;
|
||||
for attempt in 1..=HANDSHAKE_ATTEMPTS {
|
||||
match handshake(host, port, timeout) {
|
||||
Ok((stream, noise, _server_static, bind)) => {
|
||||
// Pin enforcement lives one layer up, in client::connect:
|
||||
// it knows the identity host, which a dial hint may have
|
||||
// routed away from, so the mismatch error must name the
|
||||
// trust object and not wherever the packets went.
|
||||
Ok((stream, noise, server_static, bind)) => {
|
||||
if let Some(pinned) = pinned {
|
||||
if !ct_eq(&pinned, &server_static) {
|
||||
return Err(Error::PinMismatch {
|
||||
host: host.to_string(),
|
||||
pinned,
|
||||
presented: server_static,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Ok(TcpTransport {
|
||||
stream,
|
||||
noise,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue