diff --git a/.gitignore b/.gitignore index 6a230f5..5de1c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ target/ .DS_Store +# Generated relay credentials for the ingest-router +relay-credentials.json + # Python virtual environment .venv/ __pycache__/ diff --git a/Cargo.lock b/Cargo.lock index 79ecce4..86a9d3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1073,11 +1073,24 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" version = "0.32.3" @@ -1333,6 +1346,15 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -1637,6 +1659,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1701,6 +1729,7 @@ dependencies = [ "base64", "chrono", "ed25519-dalek", + "getrandom 0.4.2", "http 1.3.1", "http-body-util", "hyper", @@ -1718,6 +1747,7 @@ dependencies = [ "tokio", "tracing", "url", + "uuid", ] [[package]] @@ -1813,6 +1843,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.177" @@ -2490,6 +2526,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -3813,13 +3855,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -3868,7 +3910,16 @@ version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] @@ -3929,6 +3980,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -3942,6 +4015,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "web-sys" version = "0.3.82" @@ -4258,6 +4343,94 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.13.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "writeable" version = "0.6.2" diff --git a/Makefile b/Makefile index d9423d5..0faa641 100644 --- a/Makefile +++ b/Makefile @@ -102,6 +102,10 @@ run-ingest-router: cargo run ingest-router --config-file-path example_config_ingest_router.yaml .PHONY: run-ingest-router +generate-credentials: + @test -f relay-credentials.json || cargo run generate-relay-credentials > relay-credentials.json +.PHONY: generate-credentials + run-mock-control-api: python scripts/mock_control_api.py .PHONY: run-mock-control-api diff --git a/devservices/devservices-relay-credentials.json b/devservices/devservices-relay-credentials.json new file mode 100644 index 0000000..44b3365 --- /dev/null +++ b/devservices/devservices-relay-credentials.json @@ -0,0 +1,5 @@ +{ + "id": "7835bea9-7df4-42d7-ab67-d344d026f9f6", + "public_key": "Pr9zR1197orWo8Ekw85tTje4zjAGpMdIg9DgrVhFQ70", + "secret_key": "oc00dWQcHUmh1qD9IZkuIaRb_EYJP9ZtU2WI5yHeY6o" +} diff --git a/devservices/ingest-router.yaml b/devservices/ingest-router.yaml index b66d7e5..5e8f81e 100644 --- a/devservices/ingest-router.yaml +++ b/devservices/ingest-router.yaml @@ -6,6 +6,11 @@ ingest_router: host: "0.0.0.0" port: 3001 + relay_keys: + # As defined in https://github.com/getsentry/relay/blob/526f63779017d108dbaef134f65a9205f702d8a4/devservices/config/devservices-credentials.json + "88888888-4444-4444-8444-cccccccccccc": + public_key: "SMSesqan65THCV6M4qs4kBzPai60LzuDn-xNsvYpuP8" + locator: type: in_process control_plane: diff --git a/ingest-router/Cargo.toml b/ingest-router/Cargo.toml index 4e3b98c..1bc604e 100644 --- a/ingest-router/Cargo.toml +++ b/ingest-router/Cargo.toml @@ -8,6 +8,7 @@ async-trait = { workspace = true } base64 = { workspace = true } chrono = { version = "0.4", features = ["clock", "serde"] } ed25519-dalek = "2" +getrandom = "0.4.2" http = { workspace = true } http-body-util = { workspace = true } hyper = { workspace = true } @@ -23,6 +24,7 @@ thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } url = { workspace = true } +uuid = { version = "1.23.3", features = ["v4"] } [dev-dependencies] serde_yaml = { workspace = true } diff --git a/ingest-router/src/auth.rs b/ingest-router/src/auth.rs index 1fbf3cb..38d6238 100644 --- a/ingest-router/src/auth.rs +++ b/ingest-router/src/auth.rs @@ -144,6 +144,26 @@ impl RelaySigner { } } +/// Generates a fresh relay `credentials.json`, matching the format produced by +/// `relay credentials generate` and consumed by [`RelaySigner::from_file`]: a new ed25519 +/// keypair (`secret_key` is the 32-byte seed, `public_key` the verifying key, both +/// base64url-nopad) plus a random UUIDv4 relay `id`. +/// +/// Returns the pretty-printed JSON to write to disk. The `public_key` must be registered with +/// the upstream (its `static_relays`) before it will accept synapse's signatures. +pub fn generate_credentials_json() -> String { + let mut seed = [0u8; 32]; + getrandom::fill(&mut seed).expect("OS entropy is available"); + let signing_key = SigningKey::from_bytes(&seed); + + let credentials = serde_json::json!({ + "secret_key": URL_SAFE_NO_PAD.encode(seed), + "public_key": URL_SAFE_NO_PAD.encode(signing_key.verifying_key().to_bytes()), + "id": uuid::Uuid::new_v4().to_string(), + }); + serde_json::to_string_pretty(&credentials).expect("credentials JSON serializes") +} + #[derive(thiserror::Error, Debug, PartialEq, Eq)] pub enum VerifyError { #[error("invalid trusted relay public key for {0}")] @@ -373,6 +393,50 @@ mod tests { )); } + fn write_tmp_file(s: &str) -> tempfile::NamedTempFile { + use std::io::Write as _; + let mut tmp = tempfile::NamedTempFile::new().expect("create temp file"); + write!(tmp, "{s}").expect("write temp file"); + tmp + } + + #[test] + fn from_file_missing_file_is_io_error() { + let result = RelaySigner::from_file(Path::new("/no/such/credentials.json")); + assert!(matches!(result, Err(SigningError::Io(_)))); + } + + #[test] + fn from_file_malformed_json_is_parse_error() { + let file = write_tmp_file("this is not json"); + let result = RelaySigner::from_file(file.path()); + assert!(matches!(result, Err(SigningError::Parse(_)))); + } + + #[test] + fn generated_credentials_round_trip() { + // Generated credentials must load via `from_file` and produce signatures that verify + // against the `public_key` embedded in the same file — locking the generator's output + // format to what synapse itself consumes. + let json = generate_credentials_json(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + + // `id` is a valid UUID, and the verifier trusts the generated relay's public key. + let id = parsed["id"].as_str().unwrap(); + assert!(uuid::Uuid::parse_str(id).is_ok()); + let public_key = parsed["public_key"].as_str().unwrap().to_string(); + + let file = write_tmp_file(&json); + let signer = RelaySigner::from_file(file.path()).unwrap(); + let verifier = + RelayVerifier::from_relays(HashMap::from([(id.to_string(), RelayInfo { public_key })])) + .unwrap(); + + let body = br#"{"publicKeys":["key1"]}"#; + let headers = signed_headers(&signer, body); + assert_eq!(verifier.verify_request(&headers, body), Ok(())); + } + const DOWNSTREAM_ID: &str = "00000000-0000-0000-0000-000000000000"; /// Builds a signer plus a verifier that trusts that signer's relay id + public key. diff --git a/ingest-router/src/config.rs b/ingest-router/src/config.rs index 18a0542..b912279 100644 --- a/ingest-router/src/config.rs +++ b/ingest-router/src/config.rs @@ -169,7 +169,6 @@ impl Locator { } } -/// Proxy configuration #[derive(Clone, Debug, Deserialize, PartialEq)] pub struct Config { /// Main listener for incoming requests diff --git a/synapse/src/main.rs b/synapse/src/main.rs index 510d384..477d920 100644 --- a/synapse/src/main.rs +++ b/synapse/src/main.rs @@ -15,6 +15,8 @@ enum CliCommand { Locator(LocatorArgs), Proxy(ProxyArgs), IngestRouter(IngestRouterArgs), + /// Generate a new ed25519 relay keypair and id as a credentials.json (printed to stdout) + GenerateRelayCredentials, /// Probe a URL and exit 0 on a 2xx response, non-zero otherwise. Healthcheck(HealthcheckArgs), /// Show all metrics definitions as markdown table @@ -86,6 +88,10 @@ fn cli() -> Result<(), CliError> { Ok(()) } + CliCommand::GenerateRelayCredentials => { + println!("{}", ingest_router::auth::generate_credentials_json()); + Ok(()) + } CliCommand::Healthcheck(args) => healthcheck::run(&args.base.config_file_path), CliCommand::ShowMetrics => { println!("## Locator Metrics\n");