SSE streaming with long-poll fallback; encrypted unicast profile; registry enc keys
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
## Repo shape
|
## Repo shape
|
||||||
- `rfc.txt` (FRX — Federated Retrieval Exchange, Draft 0.5) is the normative spec; `src/` is the Phase 1 `frxd` implementation (single crate, two binaries).
|
- `rfc.txt` (FRX — Federated Retrieval Exchange, Draft 0.5) is the normative spec; `src/` is the Phase 1 `frxd` implementation (single crate, two binaries).
|
||||||
- `frxd` is the member node (init/add/index/serve/relay/query/status); `frx` is the thin client (search/query/status). Relay and node roles are separate subcommands.
|
- `frxd` is the member node (init/add/index/serve/relay/query/status); `frx` is the thin client (search/query/status). Relay and node roles are separate subcommands.
|
||||||
- Commands: `cargo build`, `cargo test` (85 tests: unit in `src/`; e2e `tests/phase1.rs`; conformance `tests/conformance.rs`; aggregates + member directory `tests/aggregates.rs`; registry `tests/registry.rs`; federation/isolation/admission `tests/federation.rs`; concurrency/restart `tests/concurrency.rs`; real subprocess CLI `tests/cli.rs`; 1000-doc `tests/scale.rs`; purge-log absence `tests/purges.rs`; shared fixtures `tests/common/mod.rs`). No CI/lint config.
|
- Commands: `cargo build`, `cargo test` (90 tests: unit in `src/`; e2e `tests/phase1.rs`; conformance `tests/conformance.rs`; aggregates + member directory `tests/aggregates.rs`; registry `tests/registry.rs`; federation/isolation/admission `tests/federation.rs`; SSE `tests/streaming.rs`; encrypted unicast `tests/encryption.rs`; concurrency/restart `tests/concurrency.rs`; real subprocess CLI `tests/cli.rs`; 1000-doc `tests/scale.rs`; purge-log absence `tests/purges.rs`; shared fixtures `tests/common/mod.rs`). No CI/lint config.
|
||||||
- E2E pattern: relay + nodes in-process on ephemeral ports with tempdir corpora; use `tests/common/mod.rs` helpers (`spawn_relay*`, `query_envelope`, `poll_messages`, `register`) for new coverage. Raw relay polls return envelopes (payload under `body`), not response bodies.
|
- E2E pattern: relay + nodes in-process on ephemeral ports with tempdir corpora; use `tests/common/mod.rs` helpers (`spawn_relay*`, `query_envelope`, `poll_messages`, `register`) for new coverage. Raw relay polls return envelopes (payload under `body`), not response bodies.
|
||||||
|
|
||||||
## Editing the spec
|
## Editing the spec
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
- Relay addresses transport mailboxes by `key` (unicast `to` = recipient pubkey; queues keyed by pubkey); the identifier is protocol identity only. Registry binding checks `map[key].id == from`.
|
- Relay addresses transport mailboxes by `key` (unicast `to` = recipient pubkey; queues keyed by pubkey); the identifier is protocol identity only. Registry binding checks `map[key].id == from`.
|
||||||
- Built: envelope/query/response, Tantivy index, aggregates, member directory. Not built: dashboard UI, directory watching, TLS, lineage/delegation. Responses travel relay-mediated unicast; transport is HTTP long-poll, not SSE.
|
- Built: envelope/query/response, Tantivy index, aggregates, member directory. Not built: dashboard UI, directory watching, TLS, lineage/delegation. Responses travel relay-mediated unicast; transport is HTTP long-poll, not SSE.
|
||||||
- Economics is out of protocol scope (I4: aggregates advise, contracts govern): no receipt, citation, pricing, or settlement fields or message types exist or may be added.
|
- Economics is out of protocol scope (I4: aggregates advise, contracts govern): no receipt, citation, pricing, or settlement fields or message types exist or may be added.
|
||||||
- Relay verifies signatures and ±300s timestamp skew, carries only `query` broadcasts, holds no history (queues drained on poll), and requires challenge–response proof of key possession for mailbox polls. Per-member queues are isolated: a lagging member gets 429 + Retry-After with a `missed` count; publishers and other members are never stalled. Relays MAY flood to configured peers (`/v1/federation`, `--peer`+`--url`, hop-bounded, seen-set dedup without suppressing identical direct publishes) and MAY gate senders against a registry (`--registry`+`--ma-key`).
|
- Relay verifies signatures and ±300s timestamp skew, carries only `query` broadcasts, holds no history (queues drained on poll), and requires challenge–response proof of key possession for mailbox polls. Nodes prefer SSE streams (`/v1/stream`, authenticated like polls) and fall back to long-poll on 404/405. Per-member queues are isolated: a lagging member gets 429 + Retry-After with a `missed` count; publishers and other members are never stalled. Relays MAY flood to configured peers (`/v1/federation`, `--peer`+`--url`, hop-bounded, seen-set dedup without suppressing identical direct publishes) and MAY gate senders against a registry (`--registry`+`--ma-key`).
|
||||||
- Responder searches only collections marked shared (I9), stays silent when nothing matches, and emits results with honest `truncated`/`more_available` and no scores (I6). BM25 order is a local implementation detail, not protocol surface.
|
- Responder searches only collections marked shared (I9), stays silent when nothing matches, and emits results with honest `truncated`/`more_available` and no scores (I6). BM25 order is a local implementation detail, not protocol surface.
|
||||||
- Index layout: Tantivy at `<data_dir>/index`, collections manifest at `<data_dir>/collections.toml`; `exposure` (metadata|full) gates whether `content` is returned.
|
- Index layout: Tantivy at `<data_dir>/index`, collections manifest at `<data_dir>/collections.toml`; `exposure` (metadata|full) gates whether `content` is returned.
|
||||||
- Egress checks live in the responder path (`src/node.rs` `respond`), not the relay — keep private collections unreachable there.
|
- Egress checks live in the responder path (`src/node.rs` `respond`), not the relay — keep private collections unreachable there.
|
||||||
@@ -34,7 +34,8 @@
|
|||||||
- Aggregate semantics are our implementation choices from a terse spec: requests are `aggregate` envelopes carrying only `period`; replies carry `sent` (broadcasts that month) / `passed` (responses consumed from that member); granularity floor is enforced as YYYY or YYYY-MM only (finer rejected), yearly rolls up months. Revisit with §10 sufficiency review.
|
- Aggregate semantics are our implementation choices from a terse spec: requests are `aggregate` envelopes carrying only `period`; replies carry `sent` (broadcasts that month) / `passed` (responses consumed from that member); granularity floor is enforced as YYYY or YYYY-MM only (finer rejected), yearly rolls up months. Revisit with §10 sufficiency review.
|
||||||
|
|
||||||
## Known gaps (Phase 2/3, intentional — don't fake them)
|
## Known gaps (Phase 2/3, intentional — don't fake them)
|
||||||
- No dashboard UI, no directory watching (new files need `reindex`), no TLS, no user-supplied URL ingestion, no node-side (bilateral) rate limiting, no SSE streaming (long-poll only), no unicast confidentiality (HPKE).
|
- No dashboard UI, no directory watching (new files need `reindex`), no TLS, no user-supplied URL ingestion, no node-side (bilateral) rate limiting.
|
||||||
|
- Unicast confidentiality is an implementation profile, not yet normative (RFC §10): response/aggregate bodies are encrypted to the recipient's registry-listed X25519 `enc_key` (`x25519-hkdf-sha256-chacha20poly1305`, `src/crypto.rs`); members without an `enc_key` get plaintext; relays verify signatures over ciphertext but cannot read payloads.
|
||||||
- Receipts/settlement are out of protocol scope, not unimplemented (I4; Appendix B row 2). Lineage and delegation remain §10 open issues — unspecified without a supply stream, so not buildable as written; don't invent them silently.
|
- Receipts/settlement are out of protocol scope, not unimplemented (I4; Appendix B row 2). Lineage and delegation remain §10 open issues — unspecified without a supply stream, so not buildable as written; don't invent them silently.
|
||||||
- Node query dedup is by `qid` only; replay inside the ±300s skew window remains possible (no nonce cache at nodes), relays have no directory/admission, and there is no end-to-end encryption — relays see everything in clear.
|
- Node query dedup is by `qid` only; replay inside the ±300s skew window remains possible (no nonce cache at nodes), relays have no directory/admission, and there is no end-to-end encryption — relays see everything in clear.
|
||||||
|
|
||||||
|
|||||||
Generated
+283
-256
@@ -2,6 +2,16 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common 0.1.7",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.5"
|
version = "1.1.5"
|
||||||
@@ -91,6 +101,28 @@ dependencies = [
|
|||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
||||||
|
dependencies = [
|
||||||
|
"async-stream-impl",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-stream-impl"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.92"
|
version = "0.1.92"
|
||||||
@@ -114,29 +146,6 @@ version = "1.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aws-lc-rs"
|
|
||||||
version = "1.18.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
|
|
||||||
dependencies = [
|
|
||||||
"aws-lc-sys",
|
|
||||||
"zeroize",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aws-lc-sys"
|
|
||||||
version = "0.45.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"cmake",
|
|
||||||
"dunce",
|
|
||||||
"fs_extra",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -195,12 +204,6 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "base64"
|
|
||||||
version = "0.23.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.13.2"
|
version = "2.13.2"
|
||||||
@@ -216,6 +219,15 @@ dependencies = [
|
|||||||
"crunchy",
|
"crunchy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -296,6 +308,17 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -303,10 +326,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.3.1",
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20poly1305"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"chacha20 0.9.1",
|
||||||
|
"cipher",
|
||||||
|
"poly1305",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.45"
|
version = "0.4.45"
|
||||||
@@ -318,6 +354,17 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common 0.1.7",
|
||||||
|
"inout",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -358,47 +405,27 @@ version = "1.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cmake"
|
|
||||||
version = "0.1.58"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "combine"
|
|
||||||
version = "4.6.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.7"
|
version = "0.8.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -457,6 +484,17 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -467,6 +505,21 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "curve25519-dalek"
|
||||||
|
version = "4.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"curve25519-dalek-derive",
|
||||||
|
"fiat-crypto 0.2.9",
|
||||||
|
"rustc_version",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "curve25519-dalek"
|
name = "curve25519-dalek"
|
||||||
version = "5.0.0"
|
version = "5.0.0"
|
||||||
@@ -474,10 +527,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23"
|
checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.3.1",
|
||||||
"curve25519-dalek-derive",
|
"curve25519-dalek-derive",
|
||||||
"digest",
|
"digest 0.11.3",
|
||||||
"fiat-crypto",
|
"fiat-crypto 0.3.0",
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"subtle",
|
"subtle",
|
||||||
@@ -544,14 +597,25 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer 0.10.4",
|
||||||
|
"crypto-common 0.1.7",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer 0.12.1",
|
||||||
"crypto-common",
|
"crypto-common 0.2.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -571,12 +635,6 @@ version = "2.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"
|
checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dunce"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ed25519"
|
name = "ed25519"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
@@ -592,10 +650,10 @@ version = "3.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"
|
checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek 5.0.0",
|
||||||
"ed25519",
|
"ed25519",
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
"sha2",
|
"sha2 0.11.0",
|
||||||
"signature",
|
"signature",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -646,6 +704,12 @@ version = "2.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fiat-crypto"
|
||||||
|
version = "0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fiat-crypto"
|
name = "fiat-crypto"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -684,19 +748,26 @@ name = "frxd"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"async-stream",
|
||||||
"axum",
|
"axum",
|
||||||
|
"bytes",
|
||||||
|
"chacha20poly1305",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
|
"hkdf",
|
||||||
"rand 0.8.8",
|
"rand 0.8.8",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2 0.10.9",
|
||||||
"tantivy",
|
"tantivy",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
"x25519-dalek",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -709,12 +780,6 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fs_extra"
|
|
||||||
version = "1.3.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@@ -730,6 +795,12 @@ version = "0.3.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-io"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@@ -760,12 +831,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
"memchr",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -841,6 +925,24 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hkdf"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||||
|
dependencies = [
|
||||||
|
"digest 0.10.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "htmlescape"
|
name = "htmlescape"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -936,6 +1038,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -944,7 +1047,7 @@ version = "0.1.20"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -1105,6 +1208,15 @@ dependencies = [
|
|||||||
"hashbrown 0.17.1",
|
"hashbrown 0.17.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inventory"
|
name = "inventory"
|
||||||
version = "0.3.24"
|
version = "0.3.24"
|
||||||
@@ -1141,55 +1253,6 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni"
|
|
||||||
version = "0.22.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"combine",
|
|
||||||
"jni-macros",
|
|
||||||
"jni-sys",
|
|
||||||
"log",
|
|
||||||
"simd_cesu8",
|
|
||||||
"thiserror",
|
|
||||||
"walkdir",
|
|
||||||
"windows-link",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni-macros"
|
|
||||||
version = "0.22.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"rustc_version",
|
|
||||||
"simd_cesu8",
|
|
||||||
"syn 2.0.119",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni-sys"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
|
||||||
dependencies = [
|
|
||||||
"jni-sys-macros",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jni-sys-macros"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.119",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
version = "0.1.35"
|
version = "0.1.35"
|
||||||
@@ -1374,10 +1437,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107"
|
checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "opaque-debug"
|
||||||
version = "0.2.1"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-float"
|
name = "ordered-float"
|
||||||
@@ -1438,6 +1501,17 @@ version = "0.3.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "poly1305"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
|
||||||
|
dependencies = [
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -1507,7 +1581,6 @@ version = "0.11.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
|
checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"lru-slab",
|
"lru-slab",
|
||||||
@@ -1570,7 +1643,7 @@ version = "0.10.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chacha20",
|
"chacha20 0.10.2",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
@@ -1669,13 +1742,14 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.5"
|
version = "0.12.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.23.1",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
@@ -1690,19 +1764,22 @@ dependencies = [
|
|||||||
"quinn",
|
"quinn",
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-platform-verifier",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1763,26 +1840,14 @@ version = "0.23.45"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"subtle",
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-native-certs"
|
|
||||||
version = "0.8.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
|
||||||
dependencies = [
|
|
||||||
"openssl-probe",
|
|
||||||
"rustls-pki-types",
|
|
||||||
"schannel",
|
|
||||||
"security-framework",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -1793,40 +1858,12 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-platform-verifier"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"jni",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"rustls",
|
|
||||||
"rustls-native-certs",
|
|
||||||
"rustls-platform-verifier-android",
|
|
||||||
"rustls-webpki",
|
|
||||||
"security-framework",
|
|
||||||
"security-framework-sys",
|
|
||||||
"webpki-root-certs",
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-platform-verifier-android"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.15"
|
version = "0.103.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
|
||||||
"ring",
|
"ring",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
@@ -1844,53 +1881,12 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "same-file"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "schannel"
|
|
||||||
version = "0.1.29"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "security-framework"
|
|
||||||
version = "3.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
"core-foundation",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
"security-framework-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "security-framework-sys"
|
|
||||||
version = "2.17.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
|
|
||||||
dependencies = [
|
|
||||||
"core-foundation-sys",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
@@ -1972,6 +1968,17 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.2.17",
|
||||||
|
"digest 0.10.7",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -1979,8 +1986,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.3.1",
|
||||||
"digest",
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2008,22 +2015,6 @@ dependencies = [
|
|||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "simd_cesu8"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
|
||||||
dependencies = [
|
|
||||||
"rustc_version",
|
|
||||||
"simdutf8",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "simdutf8"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sketches-ddsketch"
|
name = "sketches-ddsketch"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -2123,7 +2114,7 @@ checksum = "861facfabd71044968f364837f9a083b56464ba5a59079f88706ee5c451ca069"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"base64 0.22.1",
|
"base64",
|
||||||
"bitpacking",
|
"bitpacking",
|
||||||
"bon",
|
"bon",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -2547,6 +2538,16 @@ version = "1.0.24"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-hash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common 0.1.7",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2596,14 +2597,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "version_check"
|
||||||
version = "2.5.0"
|
version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
dependencies = [
|
|
||||||
"same-file",
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "want"
|
name = "want"
|
||||||
@@ -2675,6 +2672,19 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.105"
|
version = "0.3.105"
|
||||||
@@ -2696,10 +2706,10 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-root-certs"
|
name = "webpki-roots"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
|
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
@@ -2720,15 +2730,6 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-util"
|
|
||||||
version = "0.1.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -2897,6 +2898,18 @@ version = "0.6.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x25519-dalek"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek 4.1.3",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
"serde",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -2966,6 +2979,20 @@ name = "zeroize"
|
|||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize_derive"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
|
|||||||
+8
-1
@@ -5,18 +5,25 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.104"
|
anyhow = "1.0.104"
|
||||||
|
async-stream = "0.3.6"
|
||||||
axum = "0.8.9"
|
axum = "0.8.9"
|
||||||
|
chacha20poly1305 = "0.10"
|
||||||
chrono = { version = "0.4.45", default-features = false, features = ["clock", "std"] }
|
chrono = { version = "0.4.45", default-features = false, features = ["clock", "std"] }
|
||||||
clap = { version = "4.6.7", features = ["derive"] }
|
clap = { version = "4.6.7", features = ["derive"] }
|
||||||
ed25519-dalek = { version = "3.0.0", features = ["rand_core"] }
|
ed25519-dalek = { version = "3.0.0", features = ["rand_core"] }
|
||||||
|
futures-util = "0.3.34"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
hkdf = "0.12"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
reqwest = { version = "0.13.5", default-features = false, features = ["json", "rustls", "http2"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "http2", "stream"] }
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.151"
|
serde_json = "1.0.151"
|
||||||
|
sha2 = "0.10"
|
||||||
tantivy = "0.26.2"
|
tantivy = "0.26.2"
|
||||||
tokio = { version = "1.53.1", features = ["full"] }
|
tokio = { version = "1.53.1", features = ["full"] }
|
||||||
toml = "1.1.6"
|
toml = "1.1.6"
|
||||||
|
x25519-dalek = { version = "2", features = ["static_secrets"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
bytes = "1.12.1"
|
||||||
tempfile = "3.27.0"
|
tempfile = "3.27.0"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ FRX is a membership federation for retrieval. Content owners answer broadcast qu
|
|||||||
|
|
||||||
3. System Model
|
3. System Model
|
||||||
|
|
||||||
Members exchange signed messages via dumb relays (multiply-operated transport, zero protocol authority). Queries are broadcast live to all members; relays hold no history and replay nothing. Relays MAY flood queries among themselves — copy-only, hop-bounded, duplicate-suppressed — so a member publishes once and coverage scales with relays, not senders. Delivery is per-member and isolated: a lagging member is backpressured visibly and never stalls publishers or peers. Responses are unicast from responder to querier. Transport DEFAULT: HTTPS + long-poll/SSE. Bilateral rate limits with visible backpressure are normative; application-layer rejection is silent.
|
Members exchange signed messages via dumb relays (multiply-operated transport, zero protocol authority). Queries are broadcast live to all members; relays hold no history and replay nothing. Relays MAY flood queries among themselves — copy-only, hop-bounded, duplicate-suppressed — so a member publishes once and coverage scales with relays, not senders. Delivery is per-member and isolated: a lagging member is backpressured visibly and never stalls publishers or peers. Responses are unicast from responder to querier. Transport DEFAULT: HTTPS + streaming (SSE) with long-poll fallback; members SHOULD hold one authenticated stream per relay. Bilateral rate limits with visible backpressure are normative; application-layer rejection is silent.
|
||||||
|
|
||||||
STREAM
|
STREAM
|
||||||
TYPE
|
TYPE
|
||||||
@@ -69,7 +69,7 @@ Protocol-silent by design (I2): ranking, ordering, presentation, relevance gatin
|
|||||||
|
|
||||||
6. Membership
|
6. Membership
|
||||||
|
|
||||||
The MA governs identity, contract, expulsion — who, never quality. Identifiers are MA-hosted FQDNs (`<label>.frx.<ma-domain>`); no member-controlled DNS is required. Member-hosted identifiers — keys published in the member's own domain and allowlisted by the MA — are planned, not yet normative. The MA maintains a signed, versioned registry snapshot listing identifiers, class, authorized keys with validity windows, and the federation's relay endpoints. Members and queriers may discover relays from it; relays MAY verify sender admission against it, rejecting unlisted keys visibly. Nodes pin the MA key; the snapshot is the sole authority for the key→identifier binding. Rotation publishes a successor key before retiring its predecessor; revocation removes a key or shortens its validity. Registry outage is fail-static: the last validated snapshot stays in force, and open bootstrap requires an explicit development flag. Admission cost is the Sybil defense. Expulsion grounds: fabrication, admission fraud, sustained abuse — never low quality. Escalation: local throttle → advisory aggregates → MA warning → delisting → expulsion. Aggregates are inadmissible as sanction evidence (I4). Conduct not observable on the wire — link handling, retention, gating — is governed by contract; the protocol neither observes nor adjudicates it. Membership classes: source members (own content) and enrichment members (derived corpora, e.g. GDELT/CC-NEWS bots — metadata-only exposure, transformation logic open and auditable).
|
The MA governs identity, contract, expulsion — who, never quality. Identifiers are MA-hosted FQDNs (`<label>.frx.<ma-domain>`); no member-controlled DNS is required. Member-hosted identifiers — keys published in the member's own domain and allowlisted by the MA — are planned, not yet normative. The MA maintains a signed, versioned registry snapshot listing identifiers, class, authorized keys with validity windows, an optional X25519 encryption key per member, and the federation's relay endpoints. Members and queriers may discover relays from it; relays MAY verify sender admission against it, rejecting unlisted keys visibly. Nodes pin the MA key; the snapshot is the sole authority for the key→identifier binding. Rotation publishes a successor key before retiring its predecessor; revocation removes a key or shortens its validity. Registry outage is fail-static: the last validated snapshot stays in force, and open bootstrap requires an explicit development flag. Admission cost is the Sybil defense. Expulsion grounds: fabrication, admission fraud, sustained abuse — never low quality. Escalation: local throttle → advisory aggregates → MA warning → delisting → expulsion. Aggregates are inadmissible as sanction evidence (I4). Conduct not observable on the wire — link handling, retention, gating — is governed by contract; the protocol neither observes nor adjudicates it. Membership classes: source members (own content) and enrichment members (derived corpora, e.g. GDELT/CC-NEWS bots — metadata-only exposure, transformation logic open and auditable).
|
||||||
|
|
||||||
7. Reference Implementation — frxd
|
7. Reference Implementation — frxd
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ Default-relay governance — registry-listed relays settle discovery, but who op
|
|||||||
Delegation grant mechanism.
|
Delegation grant mechanism.
|
||||||
Member-hosted identifiers — keys published in the member's own DNS instead of the MA registry; MA-hosted is normative until specified.
|
Member-hosted identifiers — keys published in the member's own DNS instead of the MA registry; MA-hosted is normative until specified.
|
||||||
MA anchor rollover — successor commitment and overlap for the registry signing key.
|
MA anchor rollover — successor commitment and overlap for the registry signing key.
|
||||||
Unicast confidentiality — response and aggregate payloads are visible to relays; no end-to-end scheme is specified.
|
Unicast confidentiality — the reference implementation encrypts response and aggregate bodies to the recipient's registry-listed X25519 key (X25519 / HKDF-SHA256 / ChaCha20-Poly1305); the profile is not yet normative.
|
||||||
Document lineage (revision/supersedes) and delegation without a supply stream — previously carried by publish; now unspecified.
|
Document lineage (revision/supersedes) and delegation without a supply stream — previously carried by publish; now unspecified.
|
||||||
Claim/entity minting conventions — recommended, non-normative (I8).
|
Claim/entity minting conventions — recommended, non-normative (I8).
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,15 @@ pub fn key_show(config_path: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn key_show_enc(config_path: &Path) -> Result<()> {
|
||||||
|
let config = Config::load(config_path)?;
|
||||||
|
let secret = config
|
||||||
|
.load_enc_key()?
|
||||||
|
.ok_or_else(|| anyhow!("no encryption key at {}", config.enc_key_path().display()))?;
|
||||||
|
println!("{}", crate::crypto::enc_public_from_secret(&secret)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn key_rotate(config_path: &Path) -> Result<()> {
|
pub fn key_rotate(config_path: &Path) -> Result<()> {
|
||||||
let config = Config::load(config_path)?;
|
let config = Config::load(config_path)?;
|
||||||
let old = config.load_key()?;
|
let old = config.load_key()?;
|
||||||
@@ -155,8 +164,11 @@ pub fn key_rotate(config_path: &Path) -> Result<()> {
|
|||||||
std::fs::copy(config.key_path(), &backup)?;
|
std::fs::copy(config.key_path(), &backup)?;
|
||||||
let new_key = Keypair::generate();
|
let new_key = Keypair::generate();
|
||||||
config.save_key(&new_key)?;
|
config.save_key(&new_key)?;
|
||||||
|
let (enc_secret, enc_public) = crate::crypto::generate_enc_keypair();
|
||||||
|
config.save_enc_key(&enc_secret)?;
|
||||||
println!("old pubkey {}", old.public_hex());
|
println!("old pubkey {}", old.public_hex());
|
||||||
println!("new pubkey {}", new_key.public_hex());
|
println!("new pubkey {}", new_key.public_hex());
|
||||||
|
println!("new enc pubkey {enc_public}");
|
||||||
println!("old key saved to {}", backup.display());
|
println!("old key saved to {}", backup.display());
|
||||||
println!(
|
println!(
|
||||||
"peers accept the new key via: frxd member add <name> {} --previous {}",
|
"peers accept the new key via: frxd member add <name> {} --previous {}",
|
||||||
@@ -293,8 +305,10 @@ pub fn registry_add(
|
|||||||
class: &str,
|
class: &str,
|
||||||
not_before: Option<u64>,
|
not_before: Option<u64>,
|
||||||
not_after: Option<u64>,
|
not_after: Option<u64>,
|
||||||
|
enc_key: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let pubkey = normalize_key(pubkey)?;
|
let pubkey = normalize_key(pubkey)?;
|
||||||
|
let enc_key = enc_key.map(|key| normalize_key(&key)).transpose()?;
|
||||||
let class = if class == CLASS_ENRICHMENT {
|
let class = if class == CLASS_ENRICHMENT {
|
||||||
CLASS_ENRICHMENT
|
CLASS_ENRICHMENT
|
||||||
} else {
|
} else {
|
||||||
@@ -312,6 +326,7 @@ pub fn registry_add(
|
|||||||
not_before: not_before.unwrap_or_else(now_ts),
|
not_before: not_before.unwrap_or_else(now_ts),
|
||||||
not_after,
|
not_after,
|
||||||
}],
|
}],
|
||||||
|
enc_key: enc_key.clone(),
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
@@ -319,6 +334,19 @@ pub fn registry_add(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn registry_set_enc_key(dir: &Path, id: &str, enc_key: &str) -> Result<()> {
|
||||||
|
let enc_key = normalize_key(enc_key)?;
|
||||||
|
mutate_registry(dir, |doc| {
|
||||||
|
let Some(member) = doc.members.iter_mut().find(|member| member.id == id) else {
|
||||||
|
return Err(anyhow!("no member named {id}"));
|
||||||
|
};
|
||||||
|
member.enc_key = Some(enc_key.clone());
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
println!("set enc key for {id}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn registry_add_key(
|
pub fn registry_add_key(
|
||||||
dir: &Path,
|
dir: &Path,
|
||||||
id: &str,
|
id: &str,
|
||||||
@@ -387,6 +415,9 @@ pub fn registry_list(dir: &Path) -> Result<()> {
|
|||||||
};
|
};
|
||||||
println!(" {} ({window})", entry.key);
|
println!(" {} ({window})", entry.key);
|
||||||
}
|
}
|
||||||
|
if let Some(enc_key) = &member.enc_key {
|
||||||
|
println!(" enc {enc_key}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-2
@@ -123,13 +123,13 @@ fn default_data_dir() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn new(name: &str, listen: &str, relay: &str, data_dir: &str) -> Self {
|
pub fn new(name: &str, listen: &str, relays: Vec<String>, data_dir: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
node: NodeSection {
|
node: NodeSection {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
id: None,
|
id: None,
|
||||||
listen: listen.to_string(),
|
listen: listen.to_string(),
|
||||||
relays: vec![relay.to_string()],
|
relays,
|
||||||
registry: None,
|
registry: None,
|
||||||
ma_key: None,
|
ma_key: None,
|
||||||
dev_bootstrap: false,
|
dev_bootstrap: false,
|
||||||
@@ -182,6 +182,27 @@ impl Config {
|
|||||||
self.data_dir().join("registry-cache.json")
|
self.data_dir().join("registry-cache.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn enc_key_path(&self) -> PathBuf {
|
||||||
|
self.data_dir().join("enc-key.hex")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_enc_key(&self) -> Result<Option<String>> {
|
||||||
|
let path = self.enc_key_path();
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let raw = fs::read_to_string(&path).context("reading enc key")?;
|
||||||
|
Ok(Some(raw.trim().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_enc_key(&self, secret_hex: &str) -> Result<()> {
|
||||||
|
fs::create_dir_all(self.data_dir())?;
|
||||||
|
let path = self.enc_key_path();
|
||||||
|
fs::write(&path, secret_hex)?;
|
||||||
|
set_private_permissions(&path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_key(&self) -> Result<Keypair> {
|
pub fn load_key(&self) -> Result<Keypair> {
|
||||||
let raw = fs::read_to_string(self.key_path())
|
let raw = fs::read_to_string(self.key_path())
|
||||||
.with_context(|| format!("reading key {}", self.key_path().display()))?;
|
.with_context(|| format!("reading key {}", self.key_path().display()))?;
|
||||||
|
|||||||
+129
@@ -1,9 +1,17 @@
|
|||||||
use crate::PROTOCOL;
|
use crate::PROTOCOL;
|
||||||
use crate::message::Envelope;
|
use crate::message::Envelope;
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use chacha20poly1305::aead::{Aead, KeyInit};
|
||||||
|
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
||||||
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
|
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
|
||||||
|
use hkdf::Hkdf;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use sha2::Sha256;
|
||||||
|
use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey, StaticSecret};
|
||||||
|
|
||||||
|
pub const ENC_ALG: &str = "x25519-hkdf-sha256-chacha20poly1305";
|
||||||
|
|
||||||
pub struct Keypair {
|
pub struct Keypair {
|
||||||
signing: SigningKey,
|
signing: SigningKey,
|
||||||
@@ -136,6 +144,102 @@ pub fn verify_envelope(envelope: &Envelope) -> Result<()> {
|
|||||||
verify_signature(&envelope.key, &signing_bytes(envelope), &envelope.sig)
|
verify_signature(&envelope.key, &signing_bytes(envelope), &envelope.sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_enc_keypair() -> (String, String) {
|
||||||
|
let secret = StaticSecret::random_from_rng(OsRng);
|
||||||
|
let public = X25519PublicKey::from(&secret);
|
||||||
|
(
|
||||||
|
hex::encode(secret.to_bytes()),
|
||||||
|
hex::encode(public.to_bytes()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enc_public_from_secret(secret_hex: &str) -> Result<String> {
|
||||||
|
let secret = enc_secret_from_hex(secret_hex)?;
|
||||||
|
Ok(hex::encode(X25519PublicKey::from(&secret).to_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_secret_from_hex(secret_hex: &str) -> Result<StaticSecret> {
|
||||||
|
let bytes = hex::decode(secret_hex.trim()).context("enc key is not hex")?;
|
||||||
|
let bytes: [u8; 32] = bytes
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| anyhow!("enc key must be 32 bytes"))?;
|
||||||
|
Ok(StaticSecret::from(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enc_public_from_hex(public_hex: &str) -> Result<X25519PublicKey> {
|
||||||
|
let bytes = hex::decode(public_hex.trim()).context("enc public key is not hex")?;
|
||||||
|
let bytes: [u8; 32] = bytes
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| anyhow!("enc public key must be 32 bytes"))?;
|
||||||
|
Ok(X25519PublicKey::from(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unicast_context(from: &str, recipient_enc_public: &str) -> String {
|
||||||
|
format!("{} unicast {} {}", PROTOCOL, from, recipient_enc_public)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encrypt_unicast(recipient_enc_hex: &str, context: &str, plaintext: &[u8]) -> Result<Value> {
|
||||||
|
let recipient = enc_public_from_hex(recipient_enc_hex)?;
|
||||||
|
let ephemeral = EphemeralSecret::random_from_rng(OsRng);
|
||||||
|
let epk = X25519PublicKey::from(&ephemeral);
|
||||||
|
let shared = ephemeral.diffie_hellman(&recipient);
|
||||||
|
let mut key = [0u8; 32];
|
||||||
|
Hkdf::<Sha256>::new(None, shared.as_bytes())
|
||||||
|
.expand(context.as_bytes(), &mut key)
|
||||||
|
.map_err(|_| anyhow!("hkdf expand failed"))?;
|
||||||
|
let cipher = ChaCha20Poly1305::new(Key::from_slice(&key));
|
||||||
|
let mut nonce_bytes = [0u8; 12];
|
||||||
|
rand::thread_rng().fill_bytes(&mut nonce_bytes);
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(Nonce::from_slice(&nonce_bytes), plaintext)
|
||||||
|
.map_err(|_| anyhow!("encryption failed"))?;
|
||||||
|
let mut payload = nonce_bytes.to_vec();
|
||||||
|
payload.extend_from_slice(&ciphertext);
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"enc": {
|
||||||
|
"alg": ENC_ALG,
|
||||||
|
"epk": hex::encode(epk.to_bytes()),
|
||||||
|
"ct": hex::encode(payload),
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrypt_unicast(secret_hex: &str, context: &str, enc: &Value) -> Result<Vec<u8>> {
|
||||||
|
let secret = enc_secret_from_hex(secret_hex)?;
|
||||||
|
let alg = enc
|
||||||
|
.get("alg")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| anyhow!("missing algorithm"))?;
|
||||||
|
if alg != ENC_ALG {
|
||||||
|
return Err(anyhow!("unsupported encryption algorithm"));
|
||||||
|
}
|
||||||
|
let epk = enc
|
||||||
|
.get("epk")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| anyhow!("missing ephemeral key"))?;
|
||||||
|
let epk = enc_public_from_hex(epk)?;
|
||||||
|
let ct = enc
|
||||||
|
.get("ct")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| anyhow!("missing ciphertext"))?;
|
||||||
|
let payload = hex::decode(ct).context("ciphertext is not hex")?;
|
||||||
|
if payload.len() < 12 {
|
||||||
|
return Err(anyhow!("ciphertext too short"));
|
||||||
|
}
|
||||||
|
let (nonce_bytes, ciphertext) = payload.split_at(12);
|
||||||
|
let shared = secret.diffie_hellman(&epk);
|
||||||
|
let mut key = [0u8; 32];
|
||||||
|
Hkdf::<Sha256>::new(None, shared.as_bytes())
|
||||||
|
.expand(context.as_bytes(), &mut key)
|
||||||
|
.map_err(|_| anyhow!("hkdf expand failed"))?;
|
||||||
|
let cipher = ChaCha20Poly1305::new(Key::from_slice(&key));
|
||||||
|
cipher
|
||||||
|
.decrypt(Nonce::from_slice(nonce_bytes), ciphertext)
|
||||||
|
.map_err(|_| anyhow!("decryption failed"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn now_ts() -> u64 {
|
pub fn now_ts() -> u64 {
|
||||||
std::time::SystemTime::now()
|
std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
@@ -168,6 +272,31 @@ mod tests {
|
|||||||
verify_envelope(&env).unwrap();
|
verify_envelope(&env).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unicast_encryption_roundtrips_and_rejects_tampering() {
|
||||||
|
let (secret, public) = generate_enc_keypair();
|
||||||
|
let context = unicast_context("alice.example", &public);
|
||||||
|
let plaintext = b"{\"qid\":\"q1\",\"results\":[]}";
|
||||||
|
let envelope_body = encrypt_unicast(&public, &context, plaintext).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
envelope_body.pointer("/enc/alg").and_then(Value::as_str),
|
||||||
|
Some(ENC_ALG)
|
||||||
|
);
|
||||||
|
let enc = envelope_body.get("enc").unwrap();
|
||||||
|
let decrypted = decrypt_unicast(&secret, &context, enc).unwrap();
|
||||||
|
assert_eq!(decrypted, plaintext);
|
||||||
|
|
||||||
|
let (other_secret, _) = generate_enc_keypair();
|
||||||
|
assert!(decrypt_unicast(&other_secret, &context, enc).is_err());
|
||||||
|
|
||||||
|
let mut tampered = enc.clone();
|
||||||
|
let ct = tampered.get("ct").and_then(Value::as_str).unwrap();
|
||||||
|
let mut bytes = hex::decode(ct).unwrap();
|
||||||
|
bytes[13] ^= 0xff;
|
||||||
|
tampered["ct"] = json!(hex::encode(bytes));
|
||||||
|
assert!(decrypt_unicast(&secret, &context, &tampered).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tampered_body_fails_verification() {
|
fn tampered_body_fails_verification() {
|
||||||
let key = Keypair::generate();
|
let key = Keypair::generate();
|
||||||
|
|||||||
+20
-3
@@ -28,7 +28,7 @@ enum Command {
|
|||||||
#[arg(long, default_value = "127.0.0.1:7701")]
|
#[arg(long, default_value = "127.0.0.1:7701")]
|
||||||
listen: String,
|
listen: String,
|
||||||
#[arg(long, default_value = "http://127.0.0.1:7700")]
|
#[arg(long, default_value = "http://127.0.0.1:7700")]
|
||||||
relay: String,
|
relay: Vec<String>,
|
||||||
#[arg(long, default_value = "./frx-data")]
|
#[arg(long, default_value = "./frx-data")]
|
||||||
data_dir: String,
|
data_dir: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -125,6 +125,7 @@ enum MemberCommand {
|
|||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum KeyCommand {
|
enum KeyCommand {
|
||||||
Show,
|
Show,
|
||||||
|
ShowEnc,
|
||||||
Rotate,
|
Rotate,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +141,12 @@ enum RegistryCommand {
|
|||||||
not_before: Option<u64>,
|
not_before: Option<u64>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
not_after: Option<u64>,
|
not_after: Option<u64>,
|
||||||
|
#[arg(long)]
|
||||||
|
enc_key: Option<String>,
|
||||||
|
},
|
||||||
|
SetEncKey {
|
||||||
|
id: String,
|
||||||
|
enc_key: String,
|
||||||
},
|
},
|
||||||
AddKey {
|
AddKey {
|
||||||
id: String,
|
id: String,
|
||||||
@@ -197,7 +204,7 @@ async fn main() -> Result<()> {
|
|||||||
if registry.is_some() && ma_key.is_none() {
|
if registry.is_some() && ma_key.is_none() {
|
||||||
bail!("--ma-key is required with --registry");
|
bail!("--ma-key is required with --registry");
|
||||||
}
|
}
|
||||||
let mut config = Config::new(&name, &listen, &relay, &data_dir);
|
let mut config = Config::new(&name, &listen, relay, &data_dir);
|
||||||
config.node.id = id;
|
config.node.id = id;
|
||||||
config.node.registry = registry;
|
config.node.registry = registry;
|
||||||
config.node.ma_key = ma_key;
|
config.node.ma_key = ma_key;
|
||||||
@@ -209,10 +216,13 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
let key = Keypair::generate();
|
let key = Keypair::generate();
|
||||||
config.save_key(&key)?;
|
config.save_key(&key)?;
|
||||||
|
let (enc_secret, enc_public) = frxd::crypto::generate_enc_keypair();
|
||||||
|
config.save_enc_key(&enc_secret)?;
|
||||||
config.save(&cli.config)?;
|
config.save(&cli.config)?;
|
||||||
println!("wrote config {}", cli.config.display());
|
println!("wrote config {}", cli.config.display());
|
||||||
println!("wrote key {}", config.key_path().display());
|
println!("wrote key {}", config.key_path().display());
|
||||||
println!("pubkey {}", key.public_hex());
|
println!("pubkey {}", key.public_hex());
|
||||||
|
println!("enc pubkey {enc_public}");
|
||||||
println!("data dir {}", config.data_dir().display());
|
println!("data dir {}", config.data_dir().display());
|
||||||
}
|
}
|
||||||
Command::Add {
|
Command::Add {
|
||||||
@@ -279,6 +289,7 @@ async fn main() -> Result<()> {
|
|||||||
},
|
},
|
||||||
Command::Key { command } => match command {
|
Command::Key { command } => match command {
|
||||||
KeyCommand::Show => commands::key_show(&cli.config)?,
|
KeyCommand::Show => commands::key_show(&cli.config)?,
|
||||||
|
KeyCommand::ShowEnc => commands::key_show_enc(&cli.config)?,
|
||||||
KeyCommand::Rotate => commands::key_rotate(&cli.config)?,
|
KeyCommand::Rotate => commands::key_rotate(&cli.config)?,
|
||||||
},
|
},
|
||||||
Command::Registry { dir, command } => match command {
|
Command::Registry { dir, command } => match command {
|
||||||
@@ -289,7 +300,13 @@ async fn main() -> Result<()> {
|
|||||||
class,
|
class,
|
||||||
not_before,
|
not_before,
|
||||||
not_after,
|
not_after,
|
||||||
} => commands::registry_add(&dir, &id, &pubkey, &class, not_before, not_after)?,
|
enc_key,
|
||||||
|
} => {
|
||||||
|
commands::registry_add(&dir, &id, &pubkey, &class, not_before, not_after, enc_key)?
|
||||||
|
}
|
||||||
|
RegistryCommand::SetEncKey { id, enc_key } => {
|
||||||
|
commands::registry_set_enc_key(&dir, &id, &enc_key)?
|
||||||
|
}
|
||||||
RegistryCommand::AddKey {
|
RegistryCommand::AddKey {
|
||||||
id,
|
id,
|
||||||
pubkey,
|
pubkey,
|
||||||
|
|||||||
+143
-11
@@ -38,6 +38,8 @@ pub struct Node {
|
|||||||
members: RwLock<Vec<Member>>,
|
members: RwLock<Vec<Member>>,
|
||||||
members_mtime: Mutex<Option<SystemTime>>,
|
members_mtime: Mutex<Option<SystemTime>>,
|
||||||
registry: Option<Arc<Watcher>>,
|
registry: Option<Arc<Watcher>>,
|
||||||
|
enc_secret: Option<String>,
|
||||||
|
enc_public: Option<String>,
|
||||||
aggregates: Mutex<Aggregates>,
|
aggregates: Mutex<Aggregates>,
|
||||||
seen: Mutex<HashSet<String>>,
|
seen: Mutex<HashSet<String>>,
|
||||||
pending: Mutex<HashMap<String, Vec<(String, ResponseBody)>>>,
|
pending: Mutex<HashMap<String, Vec<(String, ResponseBody)>>>,
|
||||||
@@ -136,6 +138,11 @@ impl Node {
|
|||||||
if let Some(watcher) = ®istry {
|
if let Some(watcher) = ®istry {
|
||||||
watcher.load_initial();
|
watcher.load_initial();
|
||||||
}
|
}
|
||||||
|
let enc_secret = config.load_enc_key()?;
|
||||||
|
let enc_public = match &enc_secret {
|
||||||
|
Some(secret) => Some(crate::crypto::enc_public_from_secret(secret)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
config,
|
config,
|
||||||
key,
|
key,
|
||||||
@@ -143,6 +150,8 @@ impl Node {
|
|||||||
members: RwLock::new(members),
|
members: RwLock::new(members),
|
||||||
members_mtime: Mutex::new(members_mtime),
|
members_mtime: Mutex::new(members_mtime),
|
||||||
registry,
|
registry,
|
||||||
|
enc_secret,
|
||||||
|
enc_public,
|
||||||
aggregates: Mutex::new(Aggregates::default()),
|
aggregates: Mutex::new(Aggregates::default()),
|
||||||
seen: Mutex::new(HashSet::new()),
|
seen: Mutex::new(HashSet::new()),
|
||||||
pending: Mutex::new(HashMap::new()),
|
pending: Mutex::new(HashMap::new()),
|
||||||
@@ -386,7 +395,52 @@ impl Node {
|
|||||||
delivered
|
delivered
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dispatch(self: &Arc<Self>, envelope: Envelope, relay: &str) {
|
fn encrypt_for(&self, recipient_key: &str, body: &Value) -> Value {
|
||||||
|
let Some(watcher) = &self.registry else {
|
||||||
|
return body.clone();
|
||||||
|
};
|
||||||
|
let Some(recipient_enc) = watcher.enc_key(recipient_key) else {
|
||||||
|
return body.clone();
|
||||||
|
};
|
||||||
|
let context = crate::crypto::unicast_context(&self.identifier(), &recipient_enc);
|
||||||
|
match serde_json::to_vec(body) {
|
||||||
|
Ok(plaintext) => crate::crypto::encrypt_unicast(&recipient_enc, &context, &plaintext)
|
||||||
|
.unwrap_or_else(|_| body.clone()),
|
||||||
|
Err(_) => body.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_body(&self, envelope: &mut Envelope) -> Result<()> {
|
||||||
|
let Some(enc) = envelope.body.get("enc").cloned() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let secret = self
|
||||||
|
.enc_secret
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow!("encrypted message received but no enc key configured"))?;
|
||||||
|
let public = self
|
||||||
|
.enc_public
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| anyhow!("encrypted message received but no enc key configured"))?;
|
||||||
|
let context = crate::crypto::unicast_context(&envelope.from, public);
|
||||||
|
let plaintext = crate::crypto::decrypt_unicast(secret, &context, &enc)?;
|
||||||
|
envelope.body = serde_json::from_slice(&plaintext).context("decrypted body is not json")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_payload(
|
||||||
|
&self,
|
||||||
|
to_key: &str,
|
||||||
|
msg_type: &str,
|
||||||
|
body: Value,
|
||||||
|
relay: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let body = self.encrypt_for(to_key, &body);
|
||||||
|
let envelope = Envelope::new(&self.key, &self.identifier(), msg_type, body);
|
||||||
|
self.send_unicast(to_key, envelope, relay).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dispatch(self: &Arc<Self>, mut envelope: Envelope, relay: &str) {
|
||||||
if envelope.verify().is_err() {
|
if envelope.verify().is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -413,6 +467,10 @@ impl Node {
|
|||||||
if !listed {
|
if !listed {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if let Err(error) = self.decrypt_body(&mut envelope) {
|
||||||
|
eprintln!("dropped encrypted message: {error}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
match envelope.msg_type.as_str() {
|
match envelope.msg_type.as_str() {
|
||||||
TYPE_QUERY => {
|
TYPE_QUERY => {
|
||||||
if envelope.from == self.identifier() || !self.config.node.responder {
|
if envelope.from == self.identifier() || !self.config.node.responder {
|
||||||
@@ -498,13 +556,8 @@ impl Node {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let body = build_response(&query.qid, response_items(&hits), total, max);
|
let body = build_response(&query.qid, response_items(&hits), total, max);
|
||||||
let envelope = Envelope::new(
|
self.send_payload(querier, TYPE_RESPONSE, serde_json::to_value(&body)?, relay)
|
||||||
&self.key,
|
.await
|
||||||
&self.identifier(),
|
|
||||||
TYPE_RESPONSE,
|
|
||||||
serde_json::to_value(&body)?,
|
|
||||||
);
|
|
||||||
self.send_unicast(querier, envelope, relay).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request_aggregate(
|
pub async fn request_aggregate(
|
||||||
@@ -561,8 +614,10 @@ impl Node {
|
|||||||
let Ok(value) = serde_json::to_value(&body) else {
|
let Ok(value) = serde_json::to_value(&body) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let envelope = Envelope::new(&self.key, &self.identifier(), TYPE_AGGREGATE, value);
|
if let Err(error) = self
|
||||||
if let Err(error) = self.send_unicast(requester_key, envelope, relay).await {
|
.send_payload(requester_key, TYPE_AGGREGATE, value, relay)
|
||||||
|
.await
|
||||||
|
{
|
||||||
eprintln!("aggregate reply failed: {error}");
|
eprintln!("aggregate reply failed: {error}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,6 +673,82 @@ async fn poll_relay(node: Arc<Node>, relay: String) {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let signature = node.key.sign(&poll_signing_bytes(&member, &nonce));
|
let signature = node.key.sign(&poll_signing_bytes(&member, &nonce));
|
||||||
|
let url = format!(
|
||||||
|
"{}/v1/stream?member={}&nonce={}&sig={}",
|
||||||
|
base, member, nonce, signature
|
||||||
|
);
|
||||||
|
match node.client.get(&url).send().await {
|
||||||
|
Ok(response) if response.status().is_success() => {
|
||||||
|
if let Err(error) = consume_stream(&node, &base, response).await {
|
||||||
|
eprintln!("stream from {base} ended: {error}");
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||||
|
}
|
||||||
|
Ok(response)
|
||||||
|
if response.status() == reqwest::StatusCode::NOT_FOUND
|
||||||
|
|| response.status() == reqwest::StatusCode::METHOD_NOT_ALLOWED =>
|
||||||
|
{
|
||||||
|
long_poll_relay(&node, &base).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(response) => {
|
||||||
|
eprintln!("stream from {base} rejected: {}", response.status());
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn consume_stream(
|
||||||
|
node: &Arc<Node>,
|
||||||
|
base: &str,
|
||||||
|
response: reqwest::Response,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
let mut stream = response.bytes_stream();
|
||||||
|
let mut buffer = String::new();
|
||||||
|
let mut event = String::new();
|
||||||
|
let mut data = String::new();
|
||||||
|
while let Some(chunk) = stream.next().await {
|
||||||
|
let chunk = chunk?;
|
||||||
|
buffer.push_str(&String::from_utf8_lossy(&chunk));
|
||||||
|
while let Some(newline) = buffer.find('\n') {
|
||||||
|
let line = buffer[..newline].trim_end_matches('\r').to_string();
|
||||||
|
buffer.drain(..=newline);
|
||||||
|
if line.is_empty() {
|
||||||
|
if event == "envelope" && !data.is_empty() {
|
||||||
|
if let Ok(envelope) = serde_json::from_str::<Envelope>(&data) {
|
||||||
|
node.dispatch(envelope, base).await;
|
||||||
|
}
|
||||||
|
} else if event == "lag" {
|
||||||
|
eprintln!("relay {base} reports lag: {data}");
|
||||||
|
}
|
||||||
|
event.clear();
|
||||||
|
data.clear();
|
||||||
|
} else if let Some(rest) = line.strip_prefix("event:") {
|
||||||
|
event = rest.trim().to_string();
|
||||||
|
} else if let Some(rest) = line.strip_prefix("data:") {
|
||||||
|
if !data.is_empty() {
|
||||||
|
data.push('\n');
|
||||||
|
}
|
||||||
|
data.push_str(rest.strip_prefix(' ').unwrap_or(rest));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn long_poll_relay(node: &Arc<Node>, base: &str) {
|
||||||
|
let member = node.key.public_hex();
|
||||||
|
loop {
|
||||||
|
let Some(nonce) = fetch_challenge(node, base, &member).await else {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let signature = node.key.sign(&poll_signing_bytes(&member, &nonce));
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/v1/poll?member={}&nonce={}&sig={}&timeout_ms=20000",
|
"{}/v1/poll?member={}&nonce={}&sig={}&timeout_ms=20000",
|
||||||
base, member, nonce, signature
|
base, member, nonce, signature
|
||||||
@@ -632,7 +763,7 @@ async fn poll_relay(node: Arc<Node>, relay: String) {
|
|||||||
if let Ok(envelope) =
|
if let Ok(envelope) =
|
||||||
serde_json::from_value::<Envelope>(message.clone())
|
serde_json::from_value::<Envelope>(message.clone())
|
||||||
{
|
{
|
||||||
node.dispatch(envelope, &base).await;
|
node.dispatch(envelope, base).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,6 +827,7 @@ async fn local_status(State(node): State<Arc<Node>>) -> Response {
|
|||||||
Json(json!({
|
Json(json!({
|
||||||
"name": node.config.node.name,
|
"name": node.config.node.name,
|
||||||
"id": node.config.node.id,
|
"id": node.config.node.id,
|
||||||
|
"enc_key": node.enc_public,
|
||||||
"registry_version": registry_version,
|
"registry_version": registry_version,
|
||||||
"pubkey": node.key.public_hex(),
|
"pubkey": node.key.public_hex(),
|
||||||
"listen": node.config.node.listen,
|
"listen": node.config.node.listen,
|
||||||
|
|||||||
+17
-3
@@ -8,9 +8,7 @@ use anyhow::{Context, Result, anyhow};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::PROTOCOL;
|
use crate::PROTOCOL;
|
||||||
#[cfg(test)]
|
use crate::crypto::{Keypair, canonical_json, now_ts, verify_signature};
|
||||||
use crate::crypto::now_ts;
|
|
||||||
use crate::crypto::{Keypair, canonical_json, verify_signature};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct KeyEntry {
|
pub struct KeyEntry {
|
||||||
@@ -28,6 +26,8 @@ pub struct RegistryMember {
|
|||||||
pub class: String,
|
pub class: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub keys: Vec<KeyEntry>,
|
pub keys: Vec<KeyEntry>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub enc_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -182,6 +182,18 @@ impl Watcher {
|
|||||||
.and_then(|signed| authorized_keys(signed, now).remove(key))
|
.and_then(|signed| authorized_keys(signed, now).remove(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn enc_key(&self, member_key: &str) -> Option<String> {
|
||||||
|
let current = self.current.lock().expect("registry lock");
|
||||||
|
let signed = current.as_ref()?;
|
||||||
|
let (id, _) = authorized_keys(signed, now_ts()).remove(member_key)?;
|
||||||
|
signed
|
||||||
|
.doc
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.find(|member| member.id == id)
|
||||||
|
.and_then(|member| member.enc_key.clone())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn version(&self) -> Option<u64> {
|
pub fn version(&self) -> Option<u64> {
|
||||||
self.current
|
self.current
|
||||||
.lock()
|
.lock()
|
||||||
@@ -234,6 +246,7 @@ mod tests {
|
|||||||
not_before,
|
not_before,
|
||||||
not_after,
|
not_after,
|
||||||
}],
|
}],
|
||||||
|
enc_key: None,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
sign_registry(
|
sign_registry(
|
||||||
@@ -292,6 +305,7 @@ mod tests {
|
|||||||
not_before: 0,
|
not_before: 0,
|
||||||
not_after: Some(now.saturating_sub(1)),
|
not_after: Some(now.saturating_sub(1)),
|
||||||
}],
|
}],
|
||||||
|
enc_key: None,
|
||||||
});
|
});
|
||||||
registry.doc.members = expired;
|
registry.doc.members = expired;
|
||||||
let registry = sign_registry(registry.doc, &ma);
|
let registry = sign_registry(registry.doc, &ma);
|
||||||
|
|||||||
+107
-38
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::convert::Infallible;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -6,12 +7,14 @@ use std::time::{Duration, Instant};
|
|||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use axum::extract::{Query, State};
|
use axum::extract::{Query, State};
|
||||||
use axum::http::{StatusCode, header};
|
use axum::http::{StatusCode, header};
|
||||||
|
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
use tokio::sync::Notify;
|
||||||
|
|
||||||
use crate::crypto::{now_ts, poll_signing_bytes, random_nonce, verify_signature};
|
use crate::crypto::{now_ts, poll_signing_bytes, random_nonce, verify_signature};
|
||||||
use crate::message::{Envelope, TYPE_AGGREGATE, TYPE_QUERY, TYPE_RESPONSE, timestamp_is_fresh};
|
use crate::message::{Envelope, TYPE_AGGREGATE, TYPE_QUERY, TYPE_RESPONSE, timestamp_is_fresh};
|
||||||
@@ -68,6 +71,7 @@ pub struct Relay {
|
|||||||
options: RelayOptions,
|
options: RelayOptions,
|
||||||
registry: Option<Arc<Watcher>>,
|
registry: Option<Arc<Watcher>>,
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
|
notify: Notify,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Relay {
|
impl Relay {
|
||||||
@@ -92,6 +96,7 @@ impl Relay {
|
|||||||
client: reqwest::Client::builder()
|
client: reqwest::Client::builder()
|
||||||
.timeout(Duration::from_secs(5))
|
.timeout(Duration::from_secs(5))
|
||||||
.build()?,
|
.build()?,
|
||||||
|
notify: Notify::new(),
|
||||||
});
|
});
|
||||||
if let Some(watcher) = &relay.registry {
|
if let Some(watcher) = &relay.registry {
|
||||||
watcher.load_initial();
|
watcher.load_initial();
|
||||||
@@ -123,38 +128,45 @@ impl Relay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push_local(&self, target: Option<&str>, envelope: Envelope) -> Result<usize, StatusCode> {
|
fn push_local(&self, target: Option<&str>, envelope: Envelope) -> Result<usize, StatusCode> {
|
||||||
let mut inner = self.inner.lock().expect("relay lock");
|
let outcome = {
|
||||||
inner.seq += 1;
|
let mut inner = self.inner.lock().expect("relay lock");
|
||||||
let seq = inner.seq;
|
inner.seq += 1;
|
||||||
match target {
|
let seq = inner.seq;
|
||||||
Some(member) => {
|
match target {
|
||||||
let Some(queue) = inner.members.get_mut(member) else {
|
Some(member) => match inner.members.get_mut(member) {
|
||||||
return Err(StatusCode::NOT_FOUND);
|
None => Err(StatusCode::NOT_FOUND),
|
||||||
};
|
Some(queue) => {
|
||||||
if queue.items.len() >= self.options.capacity {
|
if queue.items.len() >= self.options.capacity {
|
||||||
queue.lagging = true;
|
queue.lagging = true;
|
||||||
queue.missed += 1;
|
queue.missed += 1;
|
||||||
return Err(StatusCode::TOO_MANY_REQUESTS);
|
Err(StatusCode::TOO_MANY_REQUESTS)
|
||||||
}
|
} else {
|
||||||
queue.items.push_back((seq, envelope));
|
queue.items.push_back((seq, envelope));
|
||||||
Ok(1)
|
Ok(1)
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
let publisher = envelope.key.clone();
|
|
||||||
inner.members.entry(publisher).or_default();
|
|
||||||
let mut delivered = 0;
|
|
||||||
for queue in inner.members.values_mut() {
|
|
||||||
if queue.items.len() >= self.options.capacity {
|
|
||||||
queue.lagging = true;
|
|
||||||
queue.missed += 1;
|
|
||||||
} else {
|
|
||||||
queue.items.push_back((seq, envelope.clone()));
|
|
||||||
delivered += 1;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
let publisher = envelope.key.clone();
|
||||||
|
inner.members.entry(publisher).or_default();
|
||||||
|
let mut delivered = 0;
|
||||||
|
for queue in inner.members.values_mut() {
|
||||||
|
if queue.items.len() >= self.options.capacity {
|
||||||
|
queue.lagging = true;
|
||||||
|
queue.missed += 1;
|
||||||
|
} else {
|
||||||
|
queue.items.push_back((seq, envelope.clone()));
|
||||||
|
delivered += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(delivered)
|
||||||
}
|
}
|
||||||
Ok(delivered)
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if outcome.is_ok() {
|
||||||
|
self.notify.notify_waiters();
|
||||||
}
|
}
|
||||||
|
outcome
|
||||||
}
|
}
|
||||||
|
|
||||||
fn forward(&self, envelope: Envelope, origin: Option<&str>, hops: usize) {
|
fn forward(&self, envelope: Envelope, origin: Option<&str>, hops: usize) {
|
||||||
@@ -199,6 +211,7 @@ pub fn router(relay: Arc<Relay>) -> Router {
|
|||||||
.route("/v1/federation", post(federation))
|
.route("/v1/federation", post(federation))
|
||||||
.route("/v1/unicast", post(unicast))
|
.route("/v1/unicast", post(unicast))
|
||||||
.route("/v1/poll", get(poll))
|
.route("/v1/poll", get(poll))
|
||||||
|
.route("/v1/stream", get(stream))
|
||||||
.with_state(relay)
|
.with_state(relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,9 +390,9 @@ struct PollParams {
|
|||||||
timeout_ms: Option<u64>,
|
timeout_ms: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn poll(State(relay): State<Arc<Relay>>, Query(params): Query<PollParams>) -> Response {
|
fn authenticate(relay: &Relay, params: &PollParams) -> Result<(), Response> {
|
||||||
if !valid_key(¶ms.member) {
|
if !valid_key(¶ms.member) {
|
||||||
return bad_request("valid member key required");
|
return Err(bad_request("valid member key required"));
|
||||||
}
|
}
|
||||||
if verify_signature(
|
if verify_signature(
|
||||||
¶ms.member,
|
¶ms.member,
|
||||||
@@ -388,16 +401,72 @@ async fn poll(State(relay): State<Arc<Relay>>, Query(params): Query<PollParams>)
|
|||||||
)
|
)
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return unauthorized("invalid poll signature");
|
return Err(unauthorized("invalid poll signature"));
|
||||||
}
|
}
|
||||||
{
|
let mut inner = relay.inner.lock().expect("relay lock");
|
||||||
let mut inner = relay.inner.lock().expect("relay lock");
|
match inner.challenges.remove(¶ms.nonce) {
|
||||||
match inner.challenges.remove(¶ms.nonce) {
|
Some(challenge)
|
||||||
Some(challenge)
|
if challenge.member == params.member && challenge.created.elapsed() < CHALLENGE_TTL =>
|
||||||
if challenge.member == params.member
|
{
|
||||||
&& challenge.created.elapsed() < CHALLENGE_TTL => {}
|
Ok(())
|
||||||
_ => return unauthorized("unknown, expired, or reused challenge"),
|
|
||||||
}
|
}
|
||||||
|
_ => Err(unauthorized("unknown, expired, or reused challenge")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stream(State(relay): State<Arc<Relay>>, Query(params): Query<PollParams>) -> Response {
|
||||||
|
if let Err(response) = authenticate(&relay, ¶ms) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
let relay_for_stream = relay.clone();
|
||||||
|
let member = params.member.clone();
|
||||||
|
let events = async_stream::stream! {
|
||||||
|
loop {
|
||||||
|
let (batch, lagged): (Vec<Envelope>, Option<u64>) = {
|
||||||
|
let mut inner = relay_for_stream.inner.lock().expect("relay lock");
|
||||||
|
let queue = inner.members.entry(member.clone()).or_default();
|
||||||
|
if queue.lagging {
|
||||||
|
let missed = queue.missed;
|
||||||
|
queue.lagging = false;
|
||||||
|
queue.missed = 0;
|
||||||
|
queue.items.clear();
|
||||||
|
(Vec::new(), Some(missed))
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
queue
|
||||||
|
.items
|
||||||
|
.drain(..)
|
||||||
|
.map(|(_, envelope)| envelope)
|
||||||
|
.collect(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(missed) = lagged {
|
||||||
|
yield Ok::<Event, Infallible>(
|
||||||
|
Event::default()
|
||||||
|
.event("lag")
|
||||||
|
.data(json!({ "missed": missed }).to_string()),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for envelope in batch {
|
||||||
|
if let Ok(data) = serde_json::to_string(&envelope) {
|
||||||
|
yield Ok::<Event, Infallible>(Event::default().event("envelope").data(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = tokio::time::timeout(Duration::from_secs(15), relay_for_stream.notify.notified())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Sse::new(events)
|
||||||
|
.keep_alive(KeepAlive::default())
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn poll(State(relay): State<Arc<Relay>>, Query(params): Query<PollParams>) -> Response {
|
||||||
|
if let Err(response) = authenticate(&relay, ¶ms) {
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
let timeout = Duration::from_millis(params.timeout_ms.unwrap_or(25_000).min(60_000));
|
let timeout = Duration::from_millis(params.timeout_ms.unwrap_or(25_000).min(60_000));
|
||||||
let deadline = Instant::now() + timeout;
|
let deadline = Instant::now() + timeout;
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
mod common;
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use common::{
|
||||||
|
ask, client, collection, config_for, poll_messages, publish, query_envelope, register,
|
||||||
|
spawn_relay,
|
||||||
|
};
|
||||||
|
use frxd::config::Config;
|
||||||
|
use frxd::crypto::{self, Keypair, now_ts};
|
||||||
|
use frxd::index::LocalIndex;
|
||||||
|
use frxd::message::{EXPOSURE_FULL, TYPE_RESPONSE};
|
||||||
|
use frxd::node::Node;
|
||||||
|
use frxd::registry::{self, KeyEntry, RegistryDoc, RegistryMember};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
fn member_with_enc(id: &str, ed: &Keypair, enc_public: &str) -> RegistryMember {
|
||||||
|
RegistryMember {
|
||||||
|
id: id.to_string(),
|
||||||
|
class: "source".to_string(),
|
||||||
|
keys: vec![KeyEntry {
|
||||||
|
key: ed.public_hex(),
|
||||||
|
not_before: 0,
|
||||||
|
not_after: None,
|
||||||
|
}],
|
||||||
|
enc_key: Some(enc_public.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_registry(dir: &Path, ma: &Keypair, members: Vec<RegistryMember>) -> PathBuf {
|
||||||
|
let path = dir.join("registry.json");
|
||||||
|
registry::save_registry(
|
||||||
|
&path,
|
||||||
|
®istry::sign_registry(
|
||||||
|
RegistryDoc {
|
||||||
|
version: 1,
|
||||||
|
issued_at: now_ts(),
|
||||||
|
ma_key: String::new(),
|
||||||
|
members,
|
||||||
|
relays: Vec::new(),
|
||||||
|
},
|
||||||
|
ma,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_config(
|
||||||
|
dir: &Path,
|
||||||
|
name: &str,
|
||||||
|
id: &str,
|
||||||
|
relay_url: &str,
|
||||||
|
registry_path: &Path,
|
||||||
|
ma_key: &str,
|
||||||
|
enc_secret: &str,
|
||||||
|
) -> Config {
|
||||||
|
let mut config = config_for(dir, name, relay_url);
|
||||||
|
config.node.id = Some(id.to_string());
|
||||||
|
config.node.registry = Some(registry_path.display().to_string());
|
||||||
|
config.node.ma_key = Some(ma_key.to_string());
|
||||||
|
config.node.dev_bootstrap = false;
|
||||||
|
config.save_enc_key(enc_secret).unwrap();
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn response_envelopes(
|
||||||
|
http: &reqwest::Client,
|
||||||
|
relay_url: &str,
|
||||||
|
asker: &Keypair,
|
||||||
|
text: &str,
|
||||||
|
timeout_ms: u64,
|
||||||
|
) -> Vec<Value> {
|
||||||
|
register(http, relay_url, asker).await;
|
||||||
|
let envelope = query_envelope(asker, "alice.frx.example", text, 5);
|
||||||
|
assert!(
|
||||||
|
publish(http, relay_url, &envelope)
|
||||||
|
.await
|
||||||
|
.status()
|
||||||
|
.is_success()
|
||||||
|
);
|
||||||
|
let start = Instant::now();
|
||||||
|
let deadline = Duration::from_millis(timeout_ms);
|
||||||
|
let mut responses = Vec::new();
|
||||||
|
while start.elapsed() < deadline {
|
||||||
|
let messages = poll_messages(http, relay_url, asker, 200).await;
|
||||||
|
responses.extend(
|
||||||
|
messages
|
||||||
|
.iter()
|
||||||
|
.filter(|m| m.get("type").and_then(Value::as_str) == Some(TYPE_RESPONSE))
|
||||||
|
.cloned(),
|
||||||
|
);
|
||||||
|
if !responses.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
responses
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
async fn relay_sees_ciphertext_and_recipient_decrypts() {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let relay_url = spawn_relay().await;
|
||||||
|
let ma = Keypair::generate();
|
||||||
|
|
||||||
|
let alice = Keypair::generate();
|
||||||
|
let (alice_enc_secret, alice_enc_public) = crypto::generate_enc_keypair();
|
||||||
|
let (bob_enc_secret, bob_enc_public) = {
|
||||||
|
let pair = crypto::generate_enc_keypair();
|
||||||
|
(pair.0, pair.1)
|
||||||
|
};
|
||||||
|
|
||||||
|
let bob_docs = root.path().join("bob-docs");
|
||||||
|
fs::create_dir_all(&bob_docs).unwrap();
|
||||||
|
fs::write(bob_docs.join("doc.txt"), "confidential rust document").unwrap();
|
||||||
|
let bob_config = node_config(
|
||||||
|
&root.path().join("bob"),
|
||||||
|
"bob",
|
||||||
|
"bob.frx.example",
|
||||||
|
&relay_url,
|
||||||
|
&root.path().join("registry.json"),
|
||||||
|
&ma.public_hex(),
|
||||||
|
&bob_enc_secret,
|
||||||
|
);
|
||||||
|
let registry_path = save_registry(
|
||||||
|
root.path(),
|
||||||
|
&ma,
|
||||||
|
vec![
|
||||||
|
member_with_enc("alice.frx.example", &alice, &alice_enc_public),
|
||||||
|
member_with_enc(
|
||||||
|
"bob.frx.example",
|
||||||
|
&bob_config.load_key().unwrap(),
|
||||||
|
&bob_enc_public,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let index = LocalIndex::open(&bob_config.index_dir()).unwrap();
|
||||||
|
index
|
||||||
|
.add_collection(&collection("docs", &bob_docs, true, EXPOSURE_FULL))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let _bob = Node::start(bob_config).await.unwrap();
|
||||||
|
let _ = ®istry_path;
|
||||||
|
|
||||||
|
let http = client();
|
||||||
|
let responses = response_envelopes(&http, &relay_url, &alice, "rust", 900).await;
|
||||||
|
assert_eq!(responses.len(), 1, "expected one encrypted response");
|
||||||
|
let response = &responses[0];
|
||||||
|
let raw = serde_json::to_string(response).unwrap();
|
||||||
|
assert!(
|
||||||
|
!raw.contains("\"results\""),
|
||||||
|
"relay-visible payload leaked plaintext: {raw}"
|
||||||
|
);
|
||||||
|
let enc = response.pointer("/body/enc").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
enc.get("alg").and_then(Value::as_str),
|
||||||
|
Some(crypto::ENC_ALG)
|
||||||
|
);
|
||||||
|
|
||||||
|
let context = crypto::unicast_context("bob.frx.example", &alice_enc_public);
|
||||||
|
let plaintext = crypto::decrypt_unicast(&alice_enc_secret, &context, enc).unwrap();
|
||||||
|
let body: Value = serde_json::from_slice(&plaintext).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
body.get("results").and_then(Value::as_array).map(Vec::len),
|
||||||
|
Some(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
async fn node_decrypts_encrypted_responses_end_to_end() {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let relay_url = spawn_relay().await;
|
||||||
|
let ma = Keypair::generate();
|
||||||
|
|
||||||
|
let (alice_enc_secret, alice_enc_public) = crypto::generate_enc_keypair();
|
||||||
|
let (bob_enc_secret, bob_enc_public) = crypto::generate_enc_keypair();
|
||||||
|
|
||||||
|
let bob_docs = root.path().join("bob-docs");
|
||||||
|
fs::create_dir_all(&bob_docs).unwrap();
|
||||||
|
fs::write(bob_docs.join("doc.txt"), "confidential rust document").unwrap();
|
||||||
|
let alice_config = node_config(
|
||||||
|
&root.path().join("alice"),
|
||||||
|
"alice",
|
||||||
|
"alice.frx.example",
|
||||||
|
&relay_url,
|
||||||
|
&root.path().join("registry.json"),
|
||||||
|
&ma.public_hex(),
|
||||||
|
&alice_enc_secret,
|
||||||
|
);
|
||||||
|
let bob_config = node_config(
|
||||||
|
&root.path().join("bob"),
|
||||||
|
"bob",
|
||||||
|
"bob.frx.example",
|
||||||
|
&relay_url,
|
||||||
|
&root.path().join("registry.json"),
|
||||||
|
&ma.public_hex(),
|
||||||
|
&bob_enc_secret,
|
||||||
|
);
|
||||||
|
save_registry(
|
||||||
|
root.path(),
|
||||||
|
&ma,
|
||||||
|
vec![
|
||||||
|
member_with_enc(
|
||||||
|
"alice.frx.example",
|
||||||
|
&alice_config.load_key().unwrap(),
|
||||||
|
&alice_enc_public,
|
||||||
|
),
|
||||||
|
member_with_enc(
|
||||||
|
"bob.frx.example",
|
||||||
|
&bob_config.load_key().unwrap(),
|
||||||
|
&bob_enc_public,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let index = LocalIndex::open(&bob_config.index_dir()).unwrap();
|
||||||
|
index
|
||||||
|
.add_collection(&collection("docs", &bob_docs, true, EXPOSURE_FULL))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let _bob = Node::start(bob_config).await.unwrap();
|
||||||
|
let alice = Node::start(alice_config).await.unwrap();
|
||||||
|
|
||||||
|
let (_status, _raw, value) = ask(&client(), &alice.addr.to_string(), "rust", 5).await;
|
||||||
|
let responses = value.get("responses").and_then(Value::as_array).unwrap();
|
||||||
|
assert_eq!(responses.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
responses[0]
|
||||||
|
.get("results")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.map(Vec::len),
|
||||||
|
Some(1),
|
||||||
|
"alice failed to decrypt the response: {value}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -115,6 +115,7 @@ fn member(id: &str, key: &Keypair, class: &str) -> RegistryMember {
|
|||||||
not_before: 0,
|
not_before: 0,
|
||||||
not_after: None,
|
not_after: None,
|
||||||
}],
|
}],
|
||||||
|
enc_key: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ fn member_entry(
|
|||||||
not_before,
|
not_before,
|
||||||
not_after,
|
not_after,
|
||||||
}],
|
}],
|
||||||
|
enc_key: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
mod common;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use common::{
|
||||||
|
challenge, client, publish, query_envelope, register, spawn_relay, spawn_relay_with_capacity,
|
||||||
|
};
|
||||||
|
use frxd::crypto::Keypair;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
|
||||||
|
async fn open_stream(http: &reqwest::Client, relay: &str, key: &Keypair) -> reqwest::Response {
|
||||||
|
let member = key.public_hex();
|
||||||
|
let nonce = challenge(http, relay, &member).await.expect("challenge");
|
||||||
|
let sig = key.sign(&frxd::crypto::poll_signing_bytes(&member, &nonce));
|
||||||
|
http.get(format!(
|
||||||
|
"{relay}/v1/stream?member={member}&nonce={nonce}&sig={sig}"
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_until<S>(mut stream: S, needle: &str, timeout: Duration) -> (String, S)
|
||||||
|
where
|
||||||
|
S: futures_util::Stream<Item = Result<bytes::Bytes, reqwest::Error>> + Unpin,
|
||||||
|
{
|
||||||
|
let mut buffer = String::new();
|
||||||
|
let deadline = tokio::time::Instant::now() + timeout;
|
||||||
|
loop {
|
||||||
|
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
|
||||||
|
let chunk = tokio::time::timeout(remaining, stream.next())
|
||||||
|
.await
|
||||||
|
.expect("timed out waiting for stream data");
|
||||||
|
match chunk {
|
||||||
|
Some(Ok(bytes)) => {
|
||||||
|
buffer.push_str(&String::from_utf8_lossy(&bytes));
|
||||||
|
if buffer.contains(needle) {
|
||||||
|
return (buffer, stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(error)) => panic!("stream error: {error}"),
|
||||||
|
None => panic!("stream closed before '{needle}'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
async fn stream_delivers_envelopes_live() {
|
||||||
|
let relay = spawn_relay().await;
|
||||||
|
let http = client();
|
||||||
|
let member = Keypair::generate();
|
||||||
|
let publisher = Keypair::generate();
|
||||||
|
|
||||||
|
let response = open_stream(&http, &relay, &member).await;
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
assert!(
|
||||||
|
response
|
||||||
|
.headers()
|
||||||
|
.get("content-type")
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.unwrap_or("")
|
||||||
|
.contains("text/event-stream")
|
||||||
|
);
|
||||||
|
|
||||||
|
let envelope = query_envelope(&publisher, &publisher.public_hex(), "live stream", 5);
|
||||||
|
assert!(
|
||||||
|
publish(&http, &relay, &envelope)
|
||||||
|
.await
|
||||||
|
.status()
|
||||||
|
.is_success()
|
||||||
|
);
|
||||||
|
|
||||||
|
let (buffer, _) = read_until(
|
||||||
|
response.bytes_stream(),
|
||||||
|
"live stream",
|
||||||
|
Duration::from_secs(3),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(buffer.contains("event: envelope"), "{buffer}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
|
async fn stream_reports_lag_then_recovers() {
|
||||||
|
let relay = spawn_relay_with_capacity(1).await;
|
||||||
|
let http = client();
|
||||||
|
let member = Keypair::generate();
|
||||||
|
let publisher = Keypair::generate();
|
||||||
|
register(&http, &relay, &member).await;
|
||||||
|
|
||||||
|
let first = query_envelope(&publisher, &publisher.public_hex(), "first", 5);
|
||||||
|
assert!(publish(&http, &relay, &first).await.status().is_success());
|
||||||
|
let second = query_envelope(&publisher, &publisher.public_hex(), "second", 5);
|
||||||
|
assert!(publish(&http, &relay, &second).await.status().is_success());
|
||||||
|
|
||||||
|
let response = open_stream(&http, &relay, &member).await;
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
let (buffer, stream) = read_until(
|
||||||
|
response.bytes_stream(),
|
||||||
|
"\"missed\"",
|
||||||
|
Duration::from_secs(3),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(buffer.contains("event: lag"), "{buffer}");
|
||||||
|
|
||||||
|
let third = query_envelope(&publisher, &publisher.public_hex(), "third", 5);
|
||||||
|
assert!(publish(&http, &relay, &third).await.status().is_success());
|
||||||
|
let (buffer, _) = read_until(stream, "third", Duration::from_secs(3)).await;
|
||||||
|
assert!(buffer.contains("event: envelope"), "{buffer}");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user