diff --git a/.cargo/xtask.toml b/.cargo/xtask.toml index 0a5c6960a0b..0acf476cf8f 100644 --- a/.cargo/xtask.toml +++ b/.cargo/xtask.toml @@ -36,6 +36,7 @@ binary_allow_list = [ "omicron-dev", "sled-agent", "sled-agent-sim", + "measurement-diagnose", ] # libnvme is a global zone only library and therefore we must be sure that only diff --git a/Cargo.lock b/Cargo.lock index b9a64c0bf99..4ffb465a8b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,9 +147,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" dependencies = [ "backtrace", ] @@ -517,25 +517,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "attest-data" -version = "0.5.0" -source = "git+https://github.com/oxidecomputer/dice-util?rev=10952e8d9599b735b85d480af3560a11700e5b64#10952e8d9599b735b85d480af3560a11700e5b64" -dependencies = [ - "const-oid", - "der", - "getrandom 0.3.4", - "hex", - "hubpack", - "rats-corim 0.1.0 (git+https://github.com/oxidecomputer/rats-corim)", - "salty", - "serde", - "serde_with", - "sha3", - "static_assertions", - "thiserror 2.0.17", -] - [[package]] name = "attest-data" version = "0.5.0" @@ -558,10 +539,10 @@ dependencies = [ [[package]] name = "attest-mock" version = "0.1.0" -source = "git+https://github.com/oxidecomputer/dice-util?rev=10952e8d9599b735b85d480af3560a11700e5b64#10952e8d9599b735b85d480af3560a11700e5b64" +source = "git+https://github.com/oxidecomputer/dice-util?rev=6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd#6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd" dependencies = [ "anyhow", - "attest-data 0.5.0 (git+https://github.com/oxidecomputer/dice-util?rev=10952e8d9599b735b85d480af3560a11700e5b64)", + "attest-data", "clap", "hex", "hubpack", @@ -1445,9 +1426,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.48" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -1455,9 +1436,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -1468,9 +1449,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2723,7 +2704,7 @@ name = "dice-verifier" version = "0.3.0-pre0" source = "git+https://github.com/oxidecomputer/dice-util?rev=6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd#6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd" dependencies = [ - "attest-data 0.5.0 (git+https://github.com/oxidecomputer/dice-util?rev=6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd)", + "attest-data", "const-oid", "ed25519-dalek", "env_logger", @@ -3438,7 +3419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5685,7 +5666,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -8345,6 +8326,38 @@ dependencies = [ "toml 0.8.23", ] +[[package]] +name = "omicron-measurement-diagnose" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "attest-data", + "camino", + "clap", + "der", + "dice-verifier", + "dropshot", + "futures", + "omicron-rpaths", + "omicron-workspace-hack", + "oxide-tokio-rt", + "pem", + "pq-sys", + "rats-corim 0.1.0 (git+https://github.com/oxidecomputer/rats-corim.git?rev=f0d5d5168d3d31487a56df32c676b0c6240bcc6b)", + "reqwest", + "serde", + "serde_json", + "sled-agent-client", + "sled-agent-types", + "slog", + "slog-error-chain", + "strum 0.27.2", + "tokio", + "tufaceous-artifact", + "vergen-gitcl", +] + [[package]] name = "omicron-nexus" version = "0.1.0" @@ -9034,7 +9047,6 @@ dependencies = [ "aws-lc-rs", "base16ct", "base64 0.22.1", - "base64ct", "bitflags 1.3.2", "bitflags 2.9.4", "bstr", @@ -9057,10 +9069,12 @@ dependencies = [ "daft", "data-encoding", "der", + "dice-verifier", "digest", "dof 0.3.0", "dof 0.4.0", "ecdsa", + "ed25519", "ed25519-dalek", "either", "elliptic-curve", @@ -9164,6 +9178,7 @@ dependencies = [ "subtle", "syn 1.0.109", "syn 2.0.111", + "tempfile", "time", "time-macros", "tokio", @@ -12073,7 +12088,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -13600,7 +13615,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.111", @@ -13683,7 +13698,7 @@ version = "0.1.0" source = "git+https://github.com/oxidecomputer/sprockets.git?rev=8ba93f6e785e11175059b3303bfd7e8b52ad12f8#8ba93f6e785e11175059b3303bfd7e8b52ad12f8" dependencies = [ "anyhow", - "attest-data 0.5.0 (git+https://github.com/oxidecomputer/dice-util?rev=6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd)", + "attest-data", "camino", "cfg-if", "clap", @@ -14245,7 +14260,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8051d8d55ab..04c47d88821 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ members = [ "dev-tools/downloader", "dev-tools/dropshot-apis", "dev-tools/ls-apis", + "dev-tools/measurement-diagnose", "dev-tools/mgs-dev", "dev-tools/omdb", "dev-tools/omicron-dev", @@ -211,6 +212,7 @@ default-members = [ "dev-tools/downloader", "dev-tools/dropshot-apis", "dev-tools/ls-apis", + "dev-tools/measurement-diagnose", "dev-tools/mgs-dev", "dev-tools/omdb", "dev-tools/omicron-dev", @@ -396,7 +398,8 @@ assert_cmd = "2.0.17" async-bb8-diesel = "0.2" async-recursion = "1.1.1" async-trait = "0.1.89" -attest-mock = { git = "https://github.com/oxidecomputer/dice-util", rev = "10952e8d9599b735b85d480af3560a11700e5b64" } +attest-mock = { git = "https://github.com/oxidecomputer/dice-util", rev = "6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd" } +attest-data = { git = "https://github.com/oxidecomputer/dice-util", rev = "6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd" } atomicwrites = "0.4.4" authz-macros = { path = "nexus/authz-macros" } backoff = { version = "0.4.0", features = [ "tokio" ] } @@ -450,6 +453,7 @@ crucible-common = { git = "https://github.com/oxidecomputer/crucible", rev = "71 csv = "1.3.1" curve25519-dalek = "4" daft = { version = "0.1.4", features = ["derive", "newtype-uuid1", "oxnet01", "uuid1"] } +der = { version = "0.7.10", default-features = false } display-error-chain = "0.2.2" omicron-ddm-admin-client = { path = "clients/ddm-admin-client" } datatest-stable = "0.3.2" @@ -458,6 +462,7 @@ debug-ignore = "1.0.5" derive_more = "0.99.20" derive-where = "1.5.0" dev-tools-common = { path = "dev-tools/common" } +dice-verifier = { git = "https://github.com/oxidecomputer/dice-util", rev = "6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd", features = ["ipcc"] } # Having the i-implement-... feature here makes diesel go away from the workspace-hack diesel = { version = "2.2.12", features = ["i-implement-a-third-party-backend-and-opt-into-breaking-changes", "postgres", "r2d2", "chrono", "serde_json", "network-address", "uuid"] } diesel-dtrace = "0.4.2" diff --git a/dev-tools/measurement-diagnose/Cargo.toml b/dev-tools/measurement-diagnose/Cargo.toml new file mode 100644 index 00000000000..1f84c991fd3 --- /dev/null +++ b/dev-tools/measurement-diagnose/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "omicron-measurement-diagnose" +version = "0.1.0" +edition.workspace = true +license = "MPL-2.0" + +[lints] +workspace = true + +[build-dependencies] +omicron-rpaths.workspace = true +vergen-gitcl.workspace = true + +[dependencies] +anyhow.workspace = true +async-trait.workspace = true +attest-data.workspace = true +camino.workspace = true +clap.workspace = true +dice-verifier.workspace = true +der.workspace = true +dropshot.workspace = true +futures.workspace = true +omicron-workspace-hack.workspace = true +oxide-tokio-rt.workspace = true +pem.workspace = true +# See omicron-rpaths for more about the "pq-sys" dependency. +pq-sys = "*" +rats-corim.workspace = true +reqwest.workspace = true +serde.workspace = true +serde_json.workspace = true +sled-agent-client.workspace = true +sled-agent-types.workspace = true +slog.workspace = true +slog-error-chain.workspace = true +strum.workspace = true +tokio = { workspace = true, features = ["full"] } +tufaceous-artifact.workspace = true + +[dev-dependencies] + +# Disable doc builds by default for our binaries to work around issue +# rust-lang/cargo#8373. These docs would not be very useful anyway. +[[bin]] +name = "measurement-diagnose" +doc = false diff --git a/dev-tools/measurement-diagnose/src/bin/measurement-diagnose.rs b/dev-tools/measurement-diagnose/src/bin/measurement-diagnose.rs new file mode 100644 index 00000000000..35383e0fcf2 --- /dev/null +++ b/dev-tools/measurement-diagnose/src/bin/measurement-diagnose.rs @@ -0,0 +1,160 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Diagnose problems with reference measurements on a specific sled +//! This is designed to be run from the global zone of the specific sled. +//! +use anyhow::Context; +use anyhow::bail; +use attest_data::MeasurementLog; +use clap::Parser; +use clap::Subcommand; +use dice_verifier::ipcc::AttestIpcc; +use dice_verifier::{ + Attest, MeasurementSet, ReferenceMeasurements, VerifyMeasurementsError, + verify_measurements, +}; +use sled_agent_types::inventory::ConfigReconcilerInventoryResult; + +fn main() -> Result<(), anyhow::Error> { + let args = Args::parse(); + oxide_tokio_rt::run(args.exec()) +} + +#[derive(Debug, Subcommand)] +enum Command { + /// List the measurements from the inventory + List, + /// Attempt to diagnose what measurement is incorrect + Diagnose, +} + +#[derive(Debug, Parser)] +struct Args { + /// log level filter + #[arg( + env, + long, + value_parser = parse_dropshot_log_level, + default_value = "warn", + global = true, + )] + log_level: dropshot::ConfigLoggingLevel, + + /// URL of the Sled internal API + #[clap(long, env = "OMDB_SLED_AGENT_URL", global = true)] + sled_agent_url: Option, + + #[command(subcommand)] + command: Command, +} + +impl Args { + async fn exec(&self) -> Result<(), anyhow::Error> { + // This is a little goofy. The sled URL is required, but can come + // from the environment, in which case it won't be on the command line. + let Some(ref sled_agent_url) = self.sled_agent_url else { + bail!( + "sled URL must be specified with --sled-agent-url or \ + OMDB_SLED_AGENT_URL" + ); + }; + + let log = dropshot::ConfigLogging::StderrTerminal { + level: self.log_level.clone(), + } + .to_logger("measurement-diagnose") + .context("failed to create logger")?; + + let client = + sled_agent_client::Client::new(&sled_agent_url, log.clone()); + + match self.command { + Command::List => { + list_measurements(&client).await?; + } + Command::Diagnose => { + diagnose_measurements(&client).await?; + } + } + + Ok(()) + } +} + +fn parse_dropshot_log_level( + s: &str, +) -> Result { + serde_json::from_str(&format!("{:?}", s)).context("parsing log level") +} + +async fn diagnose_measurements( + client: &sled_agent_client::Client, +) -> Result<(), anyhow::Error> { + // We access our certs over IPCC + let ipcc = AttestIpcc::new()?; + + let certs = ipcc.get_certificates()?; + + let log = ipcc.get_measurement_log()?; + + // We do this intentionally to get two sets + let cert_set = + MeasurementSet::from_artifacts(&certs, &MeasurementLog::default())?; + + let measurement_set = MeasurementSet::from_artifacts(&Vec::new(), &log)?; + + let response = client.inventory().await.context("inventory")?; + let inventory = response.into_inner(); + let reference_measurements = inventory.reference_measurements; + + let mut corims = Vec::new(); + + for m in reference_measurements { + match m.result { + ConfigReconcilerInventoryResult::Ok => { + corims.push(dice_verifier::Corim::from_file(m.path)?); + } + ConfigReconcilerInventoryResult::Err { message: _ } => {} + } + } + + let reference = ReferenceMeasurements::try_from(corims.as_slice())?; + + if let Err(VerifyMeasurementsError::NotSubset(s)) = + verify_measurements(&cert_set, &reference) + { + println!("The measurement from the certificate is missing: {s}"); + } + + if let Err(VerifyMeasurementsError::NotSubset(s)) = + verify_measurements(&measurement_set, &reference) + { + println!("The measurements from the log are missing: {s}"); + } + + Ok(()) +} + +async fn list_measurements( + client: &sled_agent_client::Client, +) -> Result<(), anyhow::Error> { + let response = client.inventory().await.context("inventory")?; + let inventory = response.into_inner(); + let reference_measurements = inventory.reference_measurements; + println!("reference measurements"); + for m in reference_measurements { + match m.result { + ConfigReconcilerInventoryResult::Ok => { + let corim = dice_verifier::Corim::from_file(m.path)?; + println!("{corim}"); + } + ConfigReconcilerInventoryResult::Err { message } => { + println!("Error: {message}"); + } + } + } + + Ok(()) +} diff --git a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs index 280a288095a..feea5443d2c 100644 --- a/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs +++ b/dev-tools/omdb/src/bin/omdb/mgs/sensors.rs @@ -512,7 +512,7 @@ fn sp_info_csv( "line {}: unrecognized value \ \"{}\" in field {}", position.line(), - record[ndx + len].to_string(), + &record[ndx + len], ndx + len ); } diff --git a/workspace-hack/Cargo.toml b/workspace-hack/Cargo.toml index 8f9ecfe6e18..9d7f444f426 100644 --- a/workspace-hack/Cargo.toml +++ b/workspace-hack/Cargo.toml @@ -20,11 +20,10 @@ workspace = true [dependencies] ahash = { version = "0.8.12" } aho-corasick = { version = "1.1.3" } -anyhow = { version = "1.0.99", features = ["backtrace"] } +anyhow = { version = "1.0.100", features = ["backtrace"] } aws-lc-rs = { version = "1.12.4", features = ["prebuilt-nasm"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } base64 = { version = "0.22.1" } -base64ct = { version = "1.6.0", default-features = false, features = ["std"] } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1.3.2" } bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["serde"] } bstr = { version = "1.10.0" } @@ -33,8 +32,8 @@ byteorder = { version = "1.5.0" } bytes = { version = "1.10.1", features = ["serde"] } chrono = { version = "0.4.42", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.5.48", features = ["cargo", "derive", "env", "wrap_help"] } -clap_builder = { version = "4.5.48", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.5.54", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.5.54", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } const-oid = { version = "0.9.6", default-features = false, features = ["db", "std"] } crossbeam-epoch = { version = "0.9.18" } crossbeam-utils = { version = "0.8.21" } @@ -44,8 +43,10 @@ curve25519-dalek = { version = "4.1.3", features = ["digest", "legacy_compatibil daft = { version = "0.1.4", features = ["derive", "newtype-uuid1", "oxnet01", "uuid1"] } data-encoding = { version = "2.9.0" } der = { version = "0.7.10", default-features = false, features = ["derive", "flagset", "oid", "pem", "std"] } +dice-verifier = { git = "https://github.com/oxidecomputer/dice-util", rev = "6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd", default-features = false, features = ["ipcc", "mock"] } digest = { version = "0.10.7", features = ["mac", "oid", "std"] } ecdsa = { version = "0.16.9", features = ["pem", "signing", "std", "verifying"] } +ed25519 = { version = "2.2.3", default-features = false, features = ["pem", "std"] } ed25519-dalek = { version = "2.1.1", features = ["digest", "pem", "rand_core"] } either = { version = "1.15.0", features = ["use_std"] } elliptic-curve = { version = "0.13.8", features = ["ecdh", "hazmat", "pem", "std"] } @@ -135,6 +136,7 @@ strum-2f80eeee3b1b6c7e = { package = "strum", version = "0.26.3", features = ["d strum-754bda37e0fb3874 = { package = "strum", version = "0.27.2", features = ["derive"] } subtle = { version = "2.6.1" } syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.111", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } +tempfile = { version = "3.24.0" } time = { version = "0.3.43", features = ["formatting", "local-offset", "macros", "parsing"] } tokio = { version = "1.48.0", features = ["full", "test-util"] } tokio-postgres = { version = "0.7.13", features = ["with-chrono-0_4", "with-serde_json-1", "with-uuid-1"] } @@ -160,11 +162,10 @@ zip-3b31131e45eafb45 = { package = "zip", version = "0.6.6", default-features = [build-dependencies] ahash = { version = "0.8.12" } aho-corasick = { version = "1.1.3" } -anyhow = { version = "1.0.99", features = ["backtrace"] } +anyhow = { version = "1.0.100", features = ["backtrace"] } aws-lc-rs = { version = "1.12.4", features = ["prebuilt-nasm"] } base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } base64 = { version = "0.22.1" } -base64ct = { version = "1.6.0", default-features = false, features = ["std"] } bitflags-dff4ba8e3ae991db = { package = "bitflags", version = "1.3.2" } bitflags-f595c2ba2a3f28df = { package = "bitflags", version = "2.9.4", default-features = false, features = ["serde"] } bstr = { version = "1.10.0" } @@ -174,8 +175,8 @@ bytes = { version = "1.10.1", features = ["serde"] } cc = { version = "1.2.30", default-features = false, features = ["parallel"] } chrono = { version = "0.4.42", features = ["serde"] } cipher = { version = "0.4.4", default-features = false, features = ["block-padding", "zeroize"] } -clap = { version = "4.5.48", features = ["cargo", "derive", "env", "wrap_help"] } -clap_builder = { version = "4.5.48", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } +clap = { version = "4.5.54", features = ["cargo", "derive", "env", "wrap_help"] } +clap_builder = { version = "4.5.54", default-features = false, features = ["cargo", "color", "env", "std", "suggestions", "usage", "wrap_help"] } const-oid = { version = "0.9.6", default-features = false, features = ["db", "std"] } crossbeam-epoch = { version = "0.9.18" } crossbeam-utils = { version = "0.8.21" } @@ -185,8 +186,10 @@ curve25519-dalek = { version = "4.1.3", features = ["digest", "legacy_compatibil daft = { version = "0.1.4", features = ["derive", "newtype-uuid1", "oxnet01", "uuid1"] } data-encoding = { version = "2.9.0" } der = { version = "0.7.10", default-features = false, features = ["derive", "flagset", "oid", "pem", "std"] } +dice-verifier = { git = "https://github.com/oxidecomputer/dice-util", rev = "6e0ef48f72ff85ba50fc8286c8e89dc5f9c822dd", default-features = false, features = ["ipcc", "mock"] } digest = { version = "0.10.7", features = ["mac", "oid", "std"] } ecdsa = { version = "0.16.9", features = ["pem", "signing", "std", "verifying"] } +ed25519 = { version = "2.2.3", default-features = false, features = ["pem", "std"] } ed25519-dalek = { version = "2.1.1", features = ["digest", "pem", "rand_core"] } either = { version = "1.15.0", features = ["use_std"] } elliptic-curve = { version = "0.13.8", features = ["ecdh", "hazmat", "pem", "std"] } @@ -279,6 +282,7 @@ strum-754bda37e0fb3874 = { package = "strum", version = "0.27.2", features = ["d subtle = { version = "2.6.1" } syn-dff4ba8e3ae991db = { package = "syn", version = "1.0.109", features = ["extra-traits", "fold", "full", "visit"] } syn-f595c2ba2a3f28df = { package = "syn", version = "2.0.111", features = ["extra-traits", "fold", "full", "visit", "visit-mut"] } +tempfile = { version = "3.24.0" } time = { version = "0.3.43", features = ["formatting", "local-offset", "macros", "parsing"] } time-macros = { version = "0.2.24", default-features = false, features = ["formatting", "parsing"] } tokio = { version = "1.48.0", features = ["full", "test-util"] }