From b459e6a3c1c548ee18213ce8b37ac729b24ef1f0 Mon Sep 17 00:00:00 2001 From: Daniel Lundin <dln@arity.se> Date: Sat, 18 Jan 2025 12:15:59 +0100 Subject: [PATCH] WIP: Registration --- Cargo.lock | 189 +++++++++++++++++++++++++++++- Cargo.toml | 13 +- agent/Cargo.toml | 1 + api.json | 65 ++++++++++ controller/Cargo.toml | 10 ++ controller/src/api.rs | 2 + controller/src/context.rs | 11 +- controller/src/lib.rs | 1 + controller/src/node/api.rs | 43 +++++++ controller/src/node/controller.rs | 179 ++++++++++++++++++++++++++++ controller/src/node/mod.rs | 5 + flake.nix | 1 + node-registration/Cargo.toml | 22 ++++ 13 files changed, 535 insertions(+), 7 deletions(-) create mode 100644 controller/src/node/api.rs create mode 100644 controller/src/node/controller.rs create mode 100644 controller/src/node/mod.rs create mode 100644 node-registration/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index 6346099..642a241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -305,8 +305,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -465,6 +467,69 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "debug-ignore" version = "1.0.5" @@ -489,6 +554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -612,6 +678,31 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -674,6 +765,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "flate2" version = "1.1.1" @@ -1268,6 +1365,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1297,6 +1400,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -1851,6 +1955,7 @@ name = "patagia-agent" version = "0.2.0" dependencies = [ "anyhow", + "chrono", "clap", "futures", "instrumentation", @@ -1868,12 +1973,21 @@ name = "patagia-controller" version = "0.2.0" dependencies = [ "anyhow", + "chrono", "clap", "dropshot", + "ed25519-dalek", + "futures", + "hex", + "hkdf", "http", "instrumentation", + "rand 0.8.5", "schemars", "serde", + "serde_json", + "serde_with", + "sha2", "slog", "slog-async", "sqlx", @@ -1882,6 +1996,7 @@ dependencies = [ "tracing", "tracing-slog", "uuid", + "x25519-dalek", ] [[package]] @@ -2201,9 +2316,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags", ] @@ -2371,6 +2486,15 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.0.7" @@ -2492,6 +2616,7 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" dependencies = [ + "bytes", "chrono", "dyn-clone", "schemars_derive", @@ -2649,6 +2774,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.9.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -4217,9 +4372,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e27d6ad3dac991091e4d35de9ba2d2d00647c5d0fc26c5496dee55984ae111b" +checksum = "d9fb597c990f03753e08d3c29efbfcf2019a003b4bf4ba19225c158e1549f0f3" dependencies = [ "memchr", ] @@ -4245,6 +4400,18 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "xtask" version = "0.2.0" @@ -4325,6 +4492,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerovec" diff --git a/Cargo.toml b/Cargo.toml index 4d870ed..f86ea05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ name = "patagia-run" [workspace.dependencies] anyhow = "1.0.98" +chrono = "0.4.39" clap = { version = "4.5.37", features = [ "derive", "deprecated", @@ -32,19 +33,27 @@ clap = { version = "4.5.37", features = [ "string", ] } dropshot = "0.16.0" +ed25519-dalek = { version = "2.1.1", features = ["pem", "rand_core"] } futures = "0.3" +hex = "0.4.3" +hkdf = "0.12.4" http = "1.3.1" once_cell = "1.21.3" progenitor = "0.9" +rand = "0.8.5" reqwest = { version = "0.12.15", features = ["json", "stream", "rustls-tls"] } -schemars = "0.8.22" +schemars = { version = "0.8.22", features = ["bytes", "chrono", "derive"] } semver = "1.0.26" +serde_json = "1.0.138" serde = { version = "1.0.219", features = ["derive"] } +serde_with = { version = "3.12.0", features = ["base64", "hex", "macros", "std"] } +sha2 = "0.10.8" slog = "2.7.0" slog-async = "2.8.0" tokio = { version = "1.44.2", features = ["full"] } tracing = "0.1.41" -tracing-core = "0.1.33" tracing-chrome = "0.7.2" +tracing-core = "0.1.33" tracing-slog = { git = "https://github.com/oxidecomputer/tracing-slog", default-features = false } uuid = { version = "1", features = [ "serde", "v4" ] } +x25519-dalek = { version = "2.0.1", features = ["serde", "static_secrets"] } diff --git a/agent/Cargo.toml b/agent/Cargo.toml index efe9bf7..8560153 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -6,6 +6,7 @@ version.workspace = true [dependencies] anyhow.workspace = true +chrono.workspace = true clap.workspace = true futures.workspace = true instrumentation = { path = "../instrumentation" } diff --git a/api.json b/api.json index 4ea6803..d7c3f25 100644 --- a/api.json +++ b/api.json @@ -5,6 +5,43 @@ "version": "1.0.0" }, "paths": { + "/nodes/register": { + "get": { + "tags": [ + "node" + ], + "summary": "Get registration info", + "operationId": "get_registration_info", + "parameters": [ + { + "in": "query", + "name": "key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegistrationInfo" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/users": { "get": { "tags": [ @@ -141,6 +178,31 @@ "request_id" ] }, + "RegistrationInfo": { + "type": "object", + "properties": { + "node_id": { + "type": "string", + "format": "uuid" + }, + "nonce": { + "type": "string" + }, + "server_dh_pub": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "node_id", + "nonce", + "server_dh_pub", + "timestamp" + ] + }, "User": { "description": "User", "type": "object", @@ -210,6 +272,9 @@ } }, "tags": [ + { + "name": "node" + }, { "name": "user" } diff --git a/controller/Cargo.toml b/controller/Cargo.toml index 1ff69da..ecd9aa4 100644 --- a/controller/Cargo.toml +++ b/controller/Cargo.toml @@ -7,12 +7,21 @@ version.workspace = true [dependencies] anyhow.workspace = true +chrono.workspace = true clap.workspace = true dropshot.workspace = true +ed25519-dalek.workspace = true +futures.workspace = true +hex.workspace = true +hkdf.workspace = true http.workspace = true instrumentation = { path = "../instrumentation" } +rand.workspace = true schemars.workspace = true serde.workspace = true +serde_json.workspace = true +serde_with.workspace = true +sha2.workspace = true slog-async.workspace = true slog.workspace = true sqlx = { version = "0.8.5", default-features = false, features = [ @@ -23,6 +32,7 @@ trace-request = { path = "../trace-request" } tracing-slog.workspace = true tracing.workspace = true uuid.workspace = true +x25519-dalek.workspace = true [package.metadata.cargo-machete] ignored = ["http"] diff --git a/controller/src/api.rs b/controller/src/api.rs index 5be86ce..2063611 100644 --- a/controller/src/api.rs +++ b/controller/src/api.rs @@ -4,6 +4,7 @@ use dropshot::ApiDescription; use std::sync::Arc; use crate::context::ControllerContext; +use crate::node; use crate::user; use crate::version; @@ -11,6 +12,7 @@ type ControllerApiDescription = ApiDescription<Arc<ControllerContext>>; pub fn api() -> Result<ControllerApiDescription> { let mut api = ControllerApiDescription::new(); + node::register_api(&mut api)?; user::register_api(&mut api)?; api.register(version::version)?; Ok(api) diff --git a/controller/src/context.rs b/controller/src/context.rs index b99d559..3d4d4e1 100644 --- a/controller/src/context.rs +++ b/controller/src/context.rs @@ -1,11 +1,20 @@ use sqlx::postgres::PgPool; +use crate::node::NodeController; + pub struct ControllerContext { pub pg_pool: PgPool, + // pub node_controller: Mutex<NodeController>, + pub node_controller: NodeController, } impl ControllerContext { pub fn new(pg_pool: PgPool) -> ControllerContext { - ControllerContext { pg_pool } + // let node_controller = Mutex::new(NodeController::new(pg_pool.to_owned())); + let node_controller = NodeController::new(pg_pool.to_owned()); + ControllerContext { + pg_pool, + node_controller, + } } } diff --git a/controller/src/lib.rs b/controller/src/lib.rs index 2d12df1..d408e5c 100644 --- a/controller/src/lib.rs +++ b/controller/src/lib.rs @@ -1,5 +1,6 @@ pub mod api; pub mod context; +mod node; mod user; mod version; diff --git a/controller/src/node/api.rs b/controller/src/node/api.rs new file mode 100644 index 0000000..63517a0 --- /dev/null +++ b/controller/src/node/api.rs @@ -0,0 +1,43 @@ +use dropshot::{endpoint, HttpError, HttpResponseOk, Query, RequestContext}; +use dropshot::{ApiDescription, ApiDescriptionRegisterError}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use trace_request::trace_request; +use x25519_dalek::PublicKey; + +use std::sync::Arc; + +use crate::context::ControllerContext; +use crate::node::controller::RegistrationInfo; + +#[serde_as] +#[derive(Clone, Debug, Default, Deserialize, JsonSchema, Serialize)] +pub struct RegistrationParams { + #[serde_as(as = "serde_with::base64::Base64<serde_with::base64::UrlSafe>")] + #[schemars(with = "String")] + key: [u8; 32], +} + +pub fn register_api( + api: &mut ApiDescription<Arc<ControllerContext>>, +) -> Result<(), ApiDescriptionRegisterError> { + api.register(get_registration_info) +} + +/// Get registration info +#[endpoint { + method = GET, + path = "/nodes/register", + tags = [ "node" ], +}] +#[trace_request] +async fn get_registration_info( + rqctx: RequestContext<Arc<ControllerContext>>, + params: Query<RegistrationParams>, +) -> Result<HttpResponseOk<RegistrationInfo>, HttpError> { + let key = PublicKey::from(params.into_inner().key); + tracing::debug!("Registration info for public key: {:?}", key); + let info = rqctx.context().node_controller.new_registration(&key); + Ok(HttpResponseOk(info)) +} diff --git a/controller/src/node/controller.rs b/controller/src/node/controller.rs new file mode 100644 index 0000000..1698dc7 --- /dev/null +++ b/controller/src/node/controller.rs @@ -0,0 +1,179 @@ +use chrono::DateTime; +use ed25519_dalek::ed25519::signature::SignerMut; +use hkdf::Hkdf; +use rand::Rng; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use sha2::Sha256; +use sqlx::postgres::PgPool; +use uuid::Uuid; +use x25519_dalek::{PublicKey, StaticSecret}; + +use std::sync::{Arc, RwLock}; + +#[serde_as] +#[derive(Debug, Deserialize, JsonSchema, Serialize)] +pub struct RegistrationInfo { + node_id: Uuid, + challenge: Challenge, + + #[serde_as(as = "serde_with::base64::Base64")] + #[schemars(with = "String")] + challenge_signature: [u8; 64], +} + +#[serde_as] +#[derive(Debug, Deserialize, JsonSchema, Serialize)] +pub struct Challenge { + timestamp: DateTime<chrono::Utc>, + + #[serde_as(as = "serde_with::base64::Base64")] + #[schemars(with = "String")] + nonce: [u8; 32], + + #[serde_as(as = "serde_with::base64::Base64")] + #[schemars(with = "String")] + server_dh_pub: [u8; 32], + + params: ArgonParams, +} + +#[serde_as] +#[derive(Debug, Deserialize, JsonSchema, Serialize)] +struct ArgonParams { + iterations: u32, + memory_kb: u32, + threads: u32, +} + +pub struct NodeController { + _pg_pool: PgPool, + signing_key: RwLock<Option<Arc<SecretKey>>>, +} + +type SecretKey = [u8; 32]; +type Nonce256 = [u8; 32]; + +trait SecretKeyExt { + fn derive_dh_secret(&self, nonce: &Nonce256) -> StaticSecret; +} + +impl SecretKeyExt for SecretKey { + fn derive_dh_secret(&self, nonce: &Nonce256) -> StaticSecret { + let hk = Hkdf::<Sha256>::new(Some(nonce), self); + let mut out = [0u8; 32]; + hk.expand(b"x25519-secret", &mut out).unwrap(); + StaticSecret::from(out) + } +} + +impl RegistrationInfo { + pub fn from(server_secret: &SecretKey, client_dh_public_key: &PublicKey) -> RegistrationInfo { + let mut rng = rand::thread_rng(); + let nonce = rng.gen(); + + let server_dh_secret = server_secret.derive_dh_secret(&nonce); + let _shared_secret = server_dh_secret.diffie_hellman(client_dh_public_key); + let server_dh_public_key = PublicKey::from(&server_dh_secret); + + let timestamp = chrono::Utc::now(); + + let params = ArgonParams { + iterations: 10, + memory_kb: 220 * 1024, // 220 MiB + threads: 4, + }; + + let challenge = Challenge { + timestamp, + nonce, + server_dh_pub: server_dh_public_key.to_bytes(), + params, + }; + + let challenge_json = serde_json::to_vec(&challenge).unwrap(); + + let mut sk = ed25519_dalek::SigningKey::from_bytes(server_secret); + let sig = sk.sign(challenge_json.as_slice()); + + let info = RegistrationInfo { + node_id: Uuid::new_v4(), + challenge, + challenge_signature: sig.to_bytes(), + }; + + tracing::debug!("Info: {:?}", info); + info + } +} + +impl NodeController { + pub fn new(pg_pool: PgPool) -> NodeController { + NodeController { + _pg_pool: pg_pool, + signing_key: RwLock::new(None), + } + } + + pub fn new_registration(&self, client_dh_public_key: &PublicKey) -> RegistrationInfo { + let server_secret = self.master_key(); + RegistrationInfo::from(&server_secret, client_dh_public_key) + } + + fn master_key(&self) -> Arc<SecretKey> { + self.signing_key + .write() + .unwrap() + .get_or_insert_with(|| { + tracing::debug!("Generating key"); + Arc::new(rand::rngs::OsRng.gen()) + }) + .clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_registration_info() { + let client_key: SecretKey = rand::rngs::OsRng.gen(); + let server_key = rand::rngs::OsRng.gen(); + let client_public_key = PublicKey::from(client_key); + + let info = RegistrationInfo::from(&server_key, &client_public_key); + let info2 = RegistrationInfo::from(&server_key, &client_public_key); + + // Randomness + assert_ne!(info.node_id, info2.node_id, "Node ID should be unique"); + assert_ne!( + info.challenge.nonce, info2.challenge.nonce, + "Nonce should be unique" + ); + assert_ne!( + info.challenge.server_dh_pub, info2.challenge.server_dh_pub, + "Server public key derived from nonce should be unique" + ); + + // Timestamp + let now = chrono::Utc::now(); + assert!( + info.challenge.timestamp <= now, + "Timestamp is in the future" + ); + assert!( + info.challenge.timestamp >= now - chrono::Duration::milliseconds(5), + "Timestamp is too old" + ); + + // Public key + let server_dh_secret = server_key.derive_dh_secret(&info.challenge.nonce); + let server_dh_public_key = PublicKey::from(&server_dh_secret).to_bytes(); + assert_eq!( + info.challenge.server_dh_pub, server_dh_public_key, + "Server public key should be derived from nonce" + ); + } +} diff --git a/controller/src/node/mod.rs b/controller/src/node/mod.rs new file mode 100644 index 0000000..c67ce0c --- /dev/null +++ b/controller/src/node/mod.rs @@ -0,0 +1,5 @@ +mod api; +mod controller; + +pub use self::api::register_api; +pub(crate) use self::controller::NodeController; diff --git a/flake.nix b/flake.nix index e2aa667..e4ca97c 100644 --- a/flake.nix +++ b/flake.nix @@ -167,6 +167,7 @@ rust-dev-toolchain sqls sqlx-cli + tpm2-tools watchexec ] ++ commonArgs.buildInputs; diff --git a/node-registration/Cargo.toml b/node-registration/Cargo.toml new file mode 100644 index 0000000..074de4e --- /dev/null +++ b/node-registration/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "node-registration" +edition = "2023" +license = "MPL-2.0" +version.workspace = true + +[dependencies] +anyhow.workspace = true +chrono.workspace = true +clap.workspace = true +futures.workspace = true +instrumentation = { path = "../instrumentation" } +progenitor.workspace = true +reqwest.workspace = true +schemars.workspace = true +serde.workspace = true +tokio.workspace = true +tracing.workspace = true +uuid.workspace = true + +[package.metadata.cargo-machete] +ignored = ["reqwest", "serde"]