From 3661fcf49fe9f83696c1c2844f193370b9593f85 Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Fri, 17 Apr 2026 16:55:24 -0700 Subject: [PATCH] app-server: add codex-device-key crate ## Why Device-key storage and signing are local security-sensitive operations with platform-specific behavior. Keeping the core API in codex-device-key keeps app-server focused on routing and business logic instead of owning key-management details. The crate also keeps the signing surface intentionally narrow: callers can create a bound key, fetch its public key, or sign one of the structured payloads accepted by the crate. It does not expose a generic arbitrary-byte signing API. Key IDs cross into platform-specific labels, tags, and metadata paths, so the crate constrains externally supplied IDs to the same auditable namespace it creates: dk_ plus unpadded base64url for 32 bytes. Remote-control target paths are tied to each signed payload shape so connection proofs cannot be reused for enrollment endpoints, or vice versa. ## What changed - Added the codex-device-key workspace crate. - Added account/client-bound key creation with stable dk_ key IDs. - Added strict key_id validation before public-key lookup or signing reaches a provider. - Added public-key lookup and structured signing APIs. - Split remote-control client endpoint allowlists by connection vs enrollment payload shape. - Added validation for key bindings, accepted payload fields, token expiration, and payload/key binding mismatches. - Added flow-oriented docs on the validation helpers that gate provider signing. - Added protection policy and protection-class types without wiring a platform provider yet. - Added an unsupported default provider so platforms without an implementation fail explicitly instead of silently falling back to software-backed keys. - Updated Cargo and Bazel lock metadata for the new crate and non-platform-specific dependencies. ## Validation - just fmt - cargo test -p codex-device-key - just fix -p codex-device-key - git diff --check --- MODULE.bazel.lock | 10 + codex-rs/Cargo.lock | 132 +++ codex-rs/Cargo.toml | 2 + codex-rs/device-key/BUILD.bazel | 6 + codex-rs/device-key/Cargo.toml | 20 + codex-rs/device-key/src/lib.rs | 1374 +++++++++++++++++++++++++++ codex-rs/device-key/src/platform.rs | 51 + 7 files changed, 1595 insertions(+) create mode 100644 codex-rs/device-key/BUILD.bazel create mode 100644 codex-rs/device-key/Cargo.toml create mode 100644 codex-rs/device-key/src/lib.rs create mode 100644 codex-rs/device-key/src/platform.rs diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 1dbc8737965f..c107884e1dfa 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -667,6 +667,7 @@ "axum_0.7.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.67\"},{\"name\":\"axum-core\",\"req\":\"^0.4.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.4.2\"},{\"features\":[\"__private\"],\"kind\":\"dev\",\"name\":\"axum-macros\",\"req\":\"^0.4.1\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"^0.7\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"rustversion\",\"req\":\"^1.0.9\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.24.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.24.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:serde_urlencoded\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:serde_urlencoded\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "axum_0.8.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"axum-core\",\"req\":\"^0.5.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"form_urlencoded\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"client\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"=0.8.4\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.211\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"name\":\"serde_core\",\"req\":\"^1.0.221\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.44\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.44.2\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.28.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.28.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private\":[\"tokio\",\"http1\",\"dep:reqwest\"],\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:serde\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "backtrace_0.3.76": "{\"dependencies\":[{\"default_features\":false,\"name\":\"addr2line\",\"req\":\"^0.25.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"cpp_demangle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.156\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libloading\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"miniz_oxide\",\"req\":\"^0.8\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"default_features\":false,\"features\":[\"read_core\",\"elf\",\"macho\",\"pe\",\"xcoff\",\"unaligned\",\"archive\"],\"name\":\"object\",\"req\":\"^0.37.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"rustc-demangle\",\"req\":\"^0.1.24\"},{\"default_features\":false,\"name\":\"ruzstd\",\"optional\":true,\"req\":\"^0.8.1\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(any(windows, target_os = \\\"cygwin\\\"))\"}],\"features\":{\"coresymbolication\":[],\"dbghelp\":[],\"default\":[\"std\"],\"dl_iterate_phdr\":[],\"dladdr\":[],\"kernel32\":[],\"libunwind\":[],\"ruzstd\":[\"dep:ruzstd\"],\"serialize-serde\":[\"serde\"],\"std\":[],\"unix-backtrace\":[]}}", + "base16ct_0.2.0": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", "base64_0.21.7": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "base64_0.22.1": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "base64ct_1.8.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"base64\",\"req\":\"^0.22\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.6\"}],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}", @@ -774,6 +775,7 @@ "crossbeam-utils_0.8.21": "{\"dependencies\":[{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7.1\",\"target\":\"cfg(crossbeam_loom)\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"std\":[]}}", "crossterm_winapi_0.9.1": "{\"dependencies\":[{\"features\":[\"winbase\",\"consoleapi\",\"processenv\",\"handleapi\",\"synchapi\",\"impl-default\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "crunchy_0.2.4": "{\"dependencies\":[],\"features\":{\"default\":[\"limit_128\"],\"limit_1024\":[],\"limit_128\":[],\"limit_2048\":[],\"limit_256\":[],\"limit_512\":[],\"limit_64\":[],\"std\":[]}}", + "crypto-bigint_0.5.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"der\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"generic-array\",\"optional\":true,\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-bigint\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"num-integer\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"num-traits\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rand_chacha\",\"req\":\"^0.3\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"rlp\",\"optional\":true,\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.4\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"serdect?/alloc\"],\"default\":[\"rand\"],\"extra-sizes\":[],\"rand\":[\"rand_core/std\"],\"serde\":[\"dep:serdect\"]}}", "crypto-common_0.1.7": "{\"dependencies\":[{\"features\":[\"more_lengths\"],\"name\":\"generic-array\",\"req\":\"=0.14.7\"},{\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"typenum\",\"req\":\"^1.14\"}],\"features\":{\"getrandom\":[\"rand_core/getrandom\"],\"std\":[]}}", "crypto_box_0.9.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"blake2\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"crypto_secretbox\",\"req\":\"^0.1.1\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rmp-serde\",\"req\":\"^1\"},{\"name\":\"salsa20\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"chacha20\":[\"dep:chacha20\",\"crypto_secretbox/chacha20\"],\"default\":[\"alloc\",\"getrandom\",\"salsa20\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"salsa20\":[\"dep:salsa20\",\"crypto_secretbox/salsa20\"],\"seal\":[\"dep:blake2\",\"alloc\"],\"serde\":[\"dep:serdect\"],\"std\":[\"aead/std\"]}}", "crypto_secretbox_0.1.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"aead\",\"req\":\"^0.5\"},{\"features\":[\"zeroize\"],\"name\":\"chacha20\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"cipher\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"generic-array\",\"req\":\"^0.14.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"poly1305\",\"req\":\"^0.8\"},{\"features\":[\"zeroize\"],\"name\":\"salsa20\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1\"}],\"features\":{\"alloc\":[\"aead/alloc\"],\"default\":[\"alloc\",\"getrandom\",\"salsa20\"],\"getrandom\":[\"aead/getrandom\",\"rand_core\"],\"heapless\":[\"aead/heapless\"],\"rand_core\":[\"aead/rand_core\"],\"std\":[\"aead/std\",\"alloc\"],\"stream\":[\"aead/stream\"]}}", @@ -846,9 +848,11 @@ "dylint_linting_5.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_cmd\",\"req\":\"^2.0\"},{\"name\":\"cargo_metadata\",\"req\":\"^0.23\"},{\"features\":[\"config\"],\"name\":\"dylint_internal\",\"req\":\"=5.0.0\"},{\"name\":\"paste\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rustc_version\",\"req\":\"^0.4\"},{\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.23\"},{\"name\":\"thiserror\",\"req\":\"^2.0\"},{\"name\":\"toml\",\"req\":\"^0.9\"},{\"kind\":\"build\",\"name\":\"toml\",\"req\":\"^0.9\"}],\"features\":{\"constituent\":[]}}", "dylint_testing_5.0.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"cargo_metadata\",\"req\":\"^0.23\"},{\"name\":\"compiletest_rs\",\"req\":\"^0.11\"},{\"name\":\"dylint\",\"req\":\"=5.0.0\"},{\"name\":\"dylint_internal\",\"req\":\"=5.0.0\"},{\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"once_cell\",\"req\":\"^1.21\"},{\"name\":\"regex\",\"req\":\"^1.11\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"tempfile\",\"req\":\"^3.23\"}],\"features\":{\"default\":[],\"deny_warnings\":[]}}", "dyn-clone_1.0.20": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.66\"}],\"features\":{}}", + "ecdsa_0.16.9": "{\"dependencies\":[{\"name\":\"der\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10.7\"},{\"default_features\":false,\"features\":[\"digest\",\"sec1\"],\"name\":\"elliptic-curve\",\"req\":\"^0.13.6\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"elliptic-curve\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"rfc6979\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"oid\"],\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"signature\",\"req\":\"^2.0, <2.3\"},{\"default_features\":false,\"name\":\"spki\",\"optional\":true,\"req\":\"^0.7.2\"}],\"features\":{\"alloc\":[\"elliptic-curve/alloc\",\"signature/alloc\",\"spki/alloc\"],\"arithmetic\":[\"elliptic-curve/arithmetic\"],\"default\":[\"digest\"],\"dev\":[\"arithmetic\",\"digest\",\"elliptic-curve/dev\",\"hazmat\"],\"digest\":[\"dep:digest\",\"signature/digest\"],\"hazmat\":[],\"pem\":[\"elliptic-curve/pem\",\"pkcs8\"],\"pkcs8\":[\"digest\",\"elliptic-curve/pkcs8\",\"der\"],\"serde\":[\"elliptic-curve/serde\",\"serdect\"],\"signing\":[\"arithmetic\",\"digest\",\"hazmat\",\"rfc6979\"],\"std\":[\"alloc\",\"elliptic-curve/std\",\"signature/std\"],\"verifying\":[\"arithmetic\",\"digest\",\"hazmat\"]}}", "ed25519-dalek_2.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"blake2\",\"req\":\"^0.10\"},{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"default_features\":false,\"features\":[\"digest\"],\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"features\":[\"digest\",\"rand_core\"],\"kind\":\"dev\",\"name\":\"curve25519-dalek\",\"req\":\"^4\"},{\"default_features\":false,\"name\":\"ed25519\",\"req\":\">=2.2, <2.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"merlin\",\"optional\":true,\"req\":\"^3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha3\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"signature\",\"optional\":true,\"req\":\">=2.0, <2.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.3.0\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"static_secrets\"],\"kind\":\"dev\",\"name\":\"x25519-dalek\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"alloc\":[\"curve25519-dalek/alloc\",\"ed25519/alloc\",\"serde?/alloc\",\"zeroize/alloc\"],\"asm\":[\"sha2/asm\"],\"batch\":[\"alloc\",\"merlin\",\"rand_core\"],\"default\":[\"fast\",\"std\",\"zeroize\"],\"digest\":[\"signature/digest\"],\"fast\":[\"curve25519-dalek/precomputed-tables\"],\"hazmat\":[],\"legacy_compatibility\":[\"curve25519-dalek/legacy_compatibility\"],\"pem\":[\"alloc\",\"ed25519/pem\",\"pkcs8\"],\"pkcs8\":[\"ed25519/pkcs8\"],\"rand_core\":[\"dep:rand_core\"],\"serde\":[\"dep:serde\",\"ed25519/serde\"],\"std\":[\"alloc\",\"ed25519/std\",\"serde?/std\",\"sha2/std\"],\"zeroize\":[\"dep:zeroize\",\"curve25519-dalek/zeroize\"]}}", "ed25519_2.2.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1\"},{\"features\":[\"rand_core\"],\"kind\":\"dev\",\"name\":\"ed25519-dalek\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"signature\"],\"kind\":\"dev\",\"name\":\"ring-compat\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"serde_bytes\",\"optional\":true,\"req\":\"^0.11\"},{\"default_features\":false,\"name\":\"signature\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"pkcs8?/alloc\"],\"default\":[\"std\"],\"pem\":[\"alloc\",\"pkcs8/pem\"],\"serde_bytes\":[\"serde\",\"dep:serde_bytes\"],\"std\":[\"pkcs8?/std\",\"signature/std\"]}}", "either_1.15.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\",\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.95\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[],\"use_std\":[\"std\"]}}", + "elliptic-curve_0.13.8": "{\"dependencies\":[{\"name\":\"base16ct\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"base64ct\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"rand_core\",\"generic-array\",\"zeroize\"],\"name\":\"crypto-bigint\",\"req\":\"^0.5\"},{\"name\":\"digest\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"ff\",\"optional\":true,\"req\":\"^0.13\"},{\"default_features\":false,\"features\":[\"zeroize\"],\"name\":\"generic-array\",\"req\":\"^0.14.6\"},{\"default_features\":false,\"name\":\"group\",\"optional\":true,\"req\":\"^0.13\"},{\"name\":\"hex-literal\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"hkdf\",\"optional\":true,\"req\":\"^0.12.1\"},{\"features\":[\"alloc\"],\"name\":\"pem-rfc7468\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10.2\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6.4\"},{\"features\":[\"subtle\",\"zeroize\"],\"name\":\"sec1\",\"optional\":true,\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.47\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"sha3\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"tap\",\"optional\":true,\"req\":\"^1.0.1\"},{\"default_features\":false,\"name\":\"zeroize\",\"req\":\"^1.7\"}],\"features\":{\"alloc\":[\"base16ct/alloc\",\"ff?/alloc\",\"group?/alloc\",\"pkcs8?/alloc\",\"sec1?/alloc\",\"zeroize/alloc\"],\"arithmetic\":[\"group\"],\"bits\":[\"arithmetic\",\"ff/bits\",\"dep:tap\"],\"default\":[\"arithmetic\"],\"dev\":[\"arithmetic\",\"dep:hex-literal\",\"pem\",\"pkcs8\"],\"ecdh\":[\"arithmetic\",\"digest\",\"dep:hkdf\"],\"group\":[\"dep:group\",\"ff\"],\"hash2curve\":[\"arithmetic\",\"digest\"],\"hazmat\":[],\"jwk\":[\"dep:base64ct\",\"dep:serde_json\",\"alloc\",\"serde\",\"zeroize/alloc\"],\"pem\":[\"dep:pem-rfc7468\",\"alloc\",\"arithmetic\",\"pkcs8\",\"sec1/pem\"],\"pkcs8\":[\"dep:pkcs8\",\"sec1\"],\"serde\":[\"dep:serdect\",\"alloc\",\"pkcs8\",\"sec1/serde\"],\"std\":[\"alloc\",\"rand_core/std\",\"pkcs8?/std\",\"sec1?/std\"],\"voprf\":[\"digest\"]}}", "ena_0.14.3": "{\"dependencies\":[{\"name\":\"dogged\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"log\",\"req\":\"^0.4\"}],\"features\":{\"bench\":[],\"persistent\":[\"dogged\"]}}", "encode_unicode_1.0.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ascii\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.0\",\"target\":\"cfg(unix)\"},{\"features\":[\"https-native\"],\"kind\":\"dev\",\"name\":\"minreq\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "encoding_rs_0.8.35": "{\"dependencies\":[{\"name\":\"any_all_workaround\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\"],\"fast-big5-hanzi-encode\":[],\"fast-gb-hanzi-encode\":[],\"fast-hangul-encode\":[],\"fast-hanja-encode\":[],\"fast-kanji-encode\":[],\"fast-legacy-encode\":[\"fast-hangul-encode\",\"fast-hanja-encode\",\"fast-kanji-encode\",\"fast-gb-hanzi-encode\",\"fast-big5-hanzi-encode\"],\"less-slow-big5-hanzi-encode\":[],\"less-slow-gb-hanzi-encode\":[],\"less-slow-kanji-encode\":[],\"simd-accel\":[\"any_all_workaround\"]}}", @@ -879,6 +883,7 @@ "fax_derive_0.2.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}", "fd-lock_4.0.4": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"features\":[\"fs\"],\"name\":\"rustix\",\"req\":\"^1.0.0\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.0.8\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.60.0\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "fdeflate_0.3.7": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"miniz_oxide\",\"req\":\"^0.7.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"name\":\"simd-adler32\",\"req\":\"^0.3.4\"}],\"features\":{}}", + "ff_0.13.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bitvec\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"blake2b_simd\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"byteorder\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"ff_derive\",\"optional\":true,\"req\":\"^0.13.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"features\":[\"i128\"],\"name\":\"subtle\",\"req\":\"^2.2.1\"}],\"features\":{\"alloc\":[],\"bits\":[\"bitvec\"],\"default\":[\"bits\",\"std\"],\"derive\":[\"byteorder\",\"ff_derive\"],\"derive_bits\":[\"bits\",\"ff_derive/bits\"],\"std\":[\"alloc\"]}}", "fiat-crypto_0.2.9": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "filedescriptor_0.8.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"features\":[\"winuser\",\"handleapi\",\"fileapi\",\"namedpipeapi\",\"processthreadsapi\",\"winsock2\",\"processenv\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "filetime_0.2.27": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.27\",\"target\":\"cfg(unix)\"},{\"name\":\"libredox\",\"req\":\"^0.1.0\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{}}", @@ -1006,6 +1011,7 @@ "glob_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"tempdir\",\"req\":\"^0.3\"}],\"features\":{}}", "globset_0.4.18": "{\"dependencies\":[{\"name\":\"aho-corasick\",\"req\":\"^1.1.1\"},{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3.2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3.1\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.20\"},{\"default_features\":false,\"features\":[\"std\",\"perf\",\"syntax\",\"meta\",\"nfa\",\"hybrid\"],\"name\":\"regex-automata\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"regex-syntax\",\"req\":\"^0.8.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.188\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.107\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"log\"],\"serde1\":[\"serde\"],\"simd-accel\":[]}}", "gobject-sys_0.21.5": "{\"dependencies\":[{\"name\":\"glib-sys\",\"req\":\"^0.21\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"shell-words\",\"req\":\"^1.0.0\"},{\"kind\":\"build\",\"name\":\"system-deps\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"v2_58\":[],\"v2_62\":[\"v2_58\"],\"v2_66\":[\"v2_62\"],\"v2_68\":[\"v2_66\"],\"v2_70\":[\"v2_68\"],\"v2_72\":[\"v2_70\"],\"v2_74\":[\"v2_72\"],\"v2_76\":[\"v2_74\"],\"v2_78\":[\"v2_74\"],\"v2_80\":[\"v2_78\"],\"v2_82\":[\"v2_80\"],\"v2_84\":[\"v2_82\"],\"v2_86\":[\"v2_84\"]}}", + "group_0.13.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ff\",\"req\":\"^0.13\"},{\"name\":\"memuse\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"name\":\"rand_xorshift\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2.2.1\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\"],\"tests\":[\"alloc\",\"rand\",\"rand_xorshift\"],\"wnaf-memuse\":[\"alloc\",\"memuse\"]}}", "gzip-header_1.0.0": "{\"dependencies\":[{\"name\":\"crc32fast\",\"req\":\"^1.2.1\"}],\"features\":{}}", "h2_0.4.13": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"req\":\"^1.0.0\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"}],\"features\":{\"stream\":[],\"unstable\":[]}}", "h2_0.4.6": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"req\":\"^1.0.0\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^0.26\"}],\"features\":{\"stream\":[],\"unstable\":[]}}", @@ -1249,6 +1255,7 @@ "os_info_3.14.0": "{\"dependencies\":[{\"name\":\"android_system_properties\",\"req\":\"^0.1\",\"target\":\"cfg(target_os = \\\"android\\\")\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"feature\"],\"name\":\"nix\",\"req\":\"^0.30\",\"target\":\"cfg(any(target_os = \\\"aix\\\", target_os = \\\"dragonfly\\\", target_os = \\\"freebsd\\\", target_os = \\\"illumos\\\", target_os = \\\"linux\\\", target_os = \\\"macos\\\", target_os = \\\"netbsd\\\", target_os = \\\"openbsd\\\", target_os = \\\"cygwin\\\"))\"},{\"name\":\"objc2\",\"req\":\"^0.6\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"features\":[\"NSString\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"features\":[\"NSData\",\"NSError\",\"NSEnumerator\",\"NSString\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"objc2-ui-kit\",\"req\":\"^0.3\",\"target\":\"cfg(target_os = \\\"ios\\\")\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1\"},{\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0.3\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_LibraryLoader\",\"Win32_System_Registry\",\"Win32_System_SystemInformation\",\"Win32_System_SystemServices\",\"Win32_System_Threading\",\"Win32_UI_WindowsAndMessaging\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"serde\"]}}", "os_pipe_1.2.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.62\",\"target\":\"cfg(not(windows))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Pipes\",\"Win32_Security\"],\"name\":\"windows-sys\",\"req\":\">=0.28, <=0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"io_safety\":[]}}", "owo-colors_4.3.0": "{\"dependencies\":[{\"name\":\"supports-color\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"supports-color-2\",\"optional\":true,\"package\":\"supports-color\",\"req\":\"^2.0\"}],\"features\":{\"alloc\":[],\"supports-colors\":[\"dep:supports-color-2\",\"supports-color\"]}}", + "p256_0.13.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"blobby\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"der\"],\"name\":\"ecdsa-core\",\"optional\":true,\"package\":\"ecdsa\",\"req\":\"^0.16\"},{\"default_features\":false,\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"ecdsa-core\",\"package\":\"ecdsa\",\"req\":\"^0.16\"},{\"default_features\":false,\"features\":[\"hazmat\",\"sec1\"],\"name\":\"elliptic-curve\",\"req\":\"^0.13.1\"},{\"name\":\"hex-literal\",\"optional\":true,\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"name\":\"primeorder\",\"optional\":true,\"req\":\"^0.13\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"primeorder\",\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"features\":[\"getrandom\"],\"kind\":\"dev\",\"name\":\"rand_core\",\"req\":\"^0.6\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"}],\"features\":{\"alloc\":[\"ecdsa-core?/alloc\",\"elliptic-curve/alloc\"],\"arithmetic\":[\"dep:primeorder\",\"elliptic-curve/arithmetic\"],\"bits\":[\"arithmetic\",\"elliptic-curve/bits\"],\"default\":[\"arithmetic\",\"ecdsa\",\"pem\",\"std\"],\"digest\":[\"ecdsa-core/digest\",\"ecdsa-core/hazmat\"],\"ecdh\":[\"arithmetic\",\"elliptic-curve/ecdh\"],\"ecdsa\":[\"arithmetic\",\"ecdsa-core/signing\",\"ecdsa-core/verifying\",\"sha256\"],\"expose-field\":[\"arithmetic\"],\"hash2curve\":[\"arithmetic\",\"elliptic-curve/hash2curve\"],\"jwk\":[\"elliptic-curve/jwk\"],\"pem\":[\"elliptic-curve/pem\",\"ecdsa-core/pem\",\"pkcs8\"],\"pkcs8\":[\"ecdsa-core?/pkcs8\",\"elliptic-curve/pkcs8\"],\"serde\":[\"ecdsa-core?/serde\",\"elliptic-curve/serde\",\"primeorder?/serde\",\"serdect\"],\"sha256\":[\"digest\",\"sha2\"],\"std\":[\"alloc\",\"ecdsa-core?/std\",\"elliptic-curve/std\"],\"test-vectors\":[\"dep:hex-literal\"],\"voprf\":[\"elliptic-curve/voprf\",\"sha2\"]}}", "parking_2.2.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.0.0\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\",\"target\":\"cfg(loom)\"}],\"features\":{}}", "parking_lot_0.12.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.3\"},{\"name\":\"lock_api\",\"req\":\"^0.4.14\"},{\"name\":\"parking_lot_core\",\"req\":\"^0.9.12\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"}],\"features\":{\"arc_lock\":[\"lock_api/arc_lock\"],\"deadlock_detection\":[\"parking_lot_core/deadlock_detection\"],\"default\":[],\"hardware-lock-elision\":[],\"nightly\":[\"parking_lot_core/nightly\",\"lock_api/nightly\"],\"owning_ref\":[\"lock_api/owning_ref\"],\"send_guard\":[],\"serde\":[\"lock_api/serde\"]}}", "parking_lot_core_0.9.12": "{\"dependencies\":[{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.60\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.95\",\"target\":\"cfg(unix)\"},{\"name\":\"petgraph\",\"optional\":true,\"req\":\"^0.6.0\"},{\"name\":\"redox_syscall\",\"req\":\"^0.5\",\"target\":\"cfg(target_os = \\\"redox\\\")\"},{\"name\":\"smallvec\",\"req\":\"^1.6.1\"},{\"name\":\"windows-link\",\"req\":\"^0.2.0\",\"target\":\"cfg(windows)\"}],\"features\":{\"deadlock_detection\":[\"petgraph\",\"backtrace\"],\"nightly\":[]}}", @@ -1298,6 +1305,7 @@ "predicates_3.1.3": "{\"dependencies\":[{\"name\":\"anstyle\",\"req\":\"^1.0.0\"},{\"name\":\"difflib\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"float-cmp\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"normalize-line-endings\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"predicates-core\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"predicates-tree\",\"req\":\"^1.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"color\":[],\"default\":[\"diff\",\"regex\",\"float-cmp\",\"normalize-line-endings\",\"color\"],\"diff\":[\"dep:difflib\"],\"unstable\":[]}}", "pretty_assertions_1.4.1": "{\"dependencies\":[{\"name\":\"diff\",\"req\":\"^0.1.12\"},{\"name\":\"yansi\",\"req\":\"^1.0.1\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[],\"unstable\":[]}}", "prettyplease_0.2.37": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"indoc\",\"req\":\"^2\"},{\"default_features\":false,\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.80\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.35\"},{\"default_features\":false,\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.105\"},{\"default_features\":false,\"features\":[\"clone-impls\",\"extra-traits\",\"parsing\",\"printing\",\"visit-mut\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.105\"}],\"features\":{\"verbatim\":[\"syn/parsing\"]}}", + "primeorder_0.13.6": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"arithmetic\",\"sec1\"],\"name\":\"elliptic-curve\",\"req\":\"^0.13.7\"},{\"default_features\":false,\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"}],\"features\":{\"alloc\":[\"elliptic-curve/alloc\"],\"dev\":[],\"serde\":[\"elliptic-curve/serde\",\"serdect\"],\"std\":[\"alloc\",\"elliptic-curve/std\"]}}", "proc-macro-crate_3.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"^1.0.94\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"^1.0.39\"},{\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.99\"},{\"default_features\":false,\"features\":[\"parse\"],\"name\":\"toml_edit\",\"req\":\"^0.23.2\"}],\"features\":{}}", "proc-macro-error-attr2_2.0.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"}],\"features\":{}}", "proc-macro-error2_2.0.1": "{\"dependencies\":[{\"name\":\"proc-macro-error-attr2\",\"req\":\"=2.0.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.99\"}],\"features\":{\"default\":[\"syn-error\"],\"nightly\":[],\"syn-error\":[\"dep:syn\"]}}", @@ -1379,6 +1387,7 @@ "reqwest_0.12.28": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"brotli_crate\",\"package\":\"brotli\",\"req\":\"^8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"cookie_crate\",\"optional\":true,\"package\":\"cookie\",\"req\":\"^0.18.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"cookie_store\",\"optional\":true,\"req\":\"^0.22.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.28\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.28\"},{\"default_features\":false,\"features\":[\"std\",\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.28\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3\",\"optional\":true,\"req\":\"^0.0.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"h3-quinn\",\"optional\":true,\"req\":\"^0.0.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"tokio\"],\"name\":\"hickory-resolver\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http\",\"req\":\"^1.1\"},{\"name\":\"http-body\",\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\"],\"name\":\"hyper\",\"req\":\"^1.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"http2\",\"client\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"http1\",\"tls12\"],\"name\":\"hyper-rustls\",\"optional\":true,\"req\":\"^0.27.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"hyper-tls\",\"optional\":true,\"req\":\"^0.6\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"client\",\"client-legacy\",\"client-proxy\",\"tokio\"],\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"http1\",\"http2\",\"client\",\"client-legacy\",\"server-auto\",\"server-graceful\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.12\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"js-sys\",\"req\":\"^0.3.77\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0\"},{\"name\":\"log\",\"req\":\"^0.4.17\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.16\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2.0\"},{\"name\":\"native-tls-crate\",\"optional\":true,\"package\":\"native-tls\",\"req\":\"^0.2.10\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"^1.0\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.18\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"rustls\",\"runtime-tokio\"],\"name\":\"quinn\",\"optional\":true,\"req\":\"^0.11.1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.4\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"features\":[\"std\"],\"name\":\"rustls-pki-types\",\"optional\":true,\"req\":\"^1.9.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"serde_json\",\"req\":\"^1.0\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_urlencoded\",\"req\":\"^0.7.1\"},{\"features\":[\"futures\"],\"name\":\"sync_wrapper\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"net\",\"time\"],\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tokio-native-tls\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.9\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"retry\",\"timeout\",\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"default_features\":false,\"features\":[\"follow-redirect\"],\"name\":\"tower-http\",\"req\":\"^0.6.8\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"tower-service\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"url\",\"req\":\"^2.4\"},{\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"serde-serialize\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen\",\"req\":\"^0.2.89\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4.18\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"wasm-streams\",\"optional\":true,\"req\":\"^0.4\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"features\":[\"AbortController\",\"AbortSignal\",\"Headers\",\"Request\",\"RequestInit\",\"RequestMode\",\"Response\",\"Window\",\"FormData\",\"Blob\",\"BlobPropertyBag\",\"ServiceWorkerGlobalScope\",\"RequestCredentials\",\"File\",\"ReadableStream\",\"RequestCache\"],\"name\":\"web-sys\",\"req\":\"^0.3.28\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"kind\":\"dev\",\"name\":\"zstd_crate\",\"package\":\"zstd\",\"req\":\"^0.13\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"}],\"features\":{\"__rustls\":[\"dep:hyper-rustls\",\"dep:tokio-rustls\",\"dep:rustls\",\"__tls\"],\"__rustls-ring\":[\"hyper-rustls?/ring\",\"tokio-rustls?/ring\",\"rustls?/ring\",\"quinn?/ring\"],\"__tls\":[\"dep:rustls-pki-types\",\"tokio/io-util\"],\"blocking\":[\"dep:futures-channel\",\"futures-channel?/sink\",\"dep:futures-util\",\"futures-util?/io\",\"futures-util?/sink\",\"tokio/sync\"],\"brotli\":[\"tower-http/decompression-br\"],\"charset\":[\"dep:encoding_rs\",\"dep:mime\"],\"cookies\":[\"dep:cookie_crate\",\"dep:cookie_store\"],\"default\":[\"default-tls\",\"charset\",\"http2\",\"system-proxy\"],\"default-tls\":[\"dep:hyper-tls\",\"dep:native-tls-crate\",\"__tls\",\"dep:tokio-native-tls\"],\"deflate\":[\"tower-http/decompression-deflate\"],\"gzip\":[\"tower-http/decompression-gzip\"],\"hickory-dns\":[\"dep:hickory-resolver\",\"dep:once_cell\"],\"http2\":[\"h2\",\"hyper/http2\",\"hyper-util/http2\",\"hyper-rustls?/http2\"],\"http3\":[\"rustls-tls-manual-roots\",\"dep:h3\",\"dep:h3-quinn\",\"dep:quinn\",\"tokio/macros\"],\"json\":[\"dep:serde_json\"],\"macos-system-configuration\":[\"system-proxy\"],\"multipart\":[\"dep:mime_guess\",\"dep:futures-util\"],\"native-tls\":[\"default-tls\"],\"native-tls-alpn\":[\"native-tls\",\"native-tls-crate?/alpn\",\"hyper-tls?/alpn\"],\"native-tls-vendored\":[\"native-tls\",\"native-tls-crate?/vendored\"],\"rustls-tls\":[\"rustls-tls-webpki-roots\"],\"rustls-tls-manual-roots\":[\"rustls-tls-manual-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-manual-roots-no-provider\":[\"__rustls\"],\"rustls-tls-native-roots\":[\"rustls-tls-native-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-native-roots-no-provider\":[\"dep:rustls-native-certs\",\"hyper-rustls?/native-tokio\",\"__rustls\"],\"rustls-tls-no-provider\":[\"rustls-tls-manual-roots-no-provider\"],\"rustls-tls-webpki-roots\":[\"rustls-tls-webpki-roots-no-provider\",\"__rustls-ring\"],\"rustls-tls-webpki-roots-no-provider\":[\"dep:webpki-roots\",\"hyper-rustls?/webpki-tokio\",\"__rustls\"],\"socks\":[],\"stream\":[\"tokio/fs\",\"dep:futures-util\",\"dep:tokio-util\",\"dep:wasm-streams\"],\"system-proxy\":[\"hyper-util/client-proxy-system\"],\"trust-dns\":[],\"zstd\":[\"tower-http/decompression-zstd\"]}}", "resb_0.1.1": "{\"dependencies\":[{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"log\",\"optional\":true,\"req\":\"^0.4.17\"},{\"name\":\"nom\",\"optional\":true,\"req\":\"^7.0.0\"},{\"default_features\":false,\"name\":\"potential_utf\",\"req\":\"^0.1.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"req\":\"^1.0.220\"}],\"features\":{\"default\":[],\"logging\":[\"dep:log\"],\"serialize\":[\"std\"],\"std\":[],\"text\":[\"dep:indexmap\",\"dep:nom\",\"std\"]}}", "resolv-conf_0.7.6": "{\"dependencies\":[],\"features\":{\"system\":[]}}", + "rfc6979_0.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"reset\"],\"name\":\"hmac\",\"req\":\"^0.12\"},{\"kind\":\"dev\",\"name\":\"sha2\",\"req\":\"^0.10\"},{\"default_features\":false,\"name\":\"subtle\",\"req\":\"^2\"}],\"features\":{}}", "ring_0.17.14": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.8\"},{\"default_features\":false,\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"getrandom\",\"req\":\"^0.2.10\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(all(any(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), all(target_arch = \\\"arm\\\", target_endian = \\\"little\\\")), any(target_os = \\\"android\\\", target_os = \\\"linux\\\")))\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.155\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_vendor = \\\"apple\\\", any(target_os = \\\"ios\\\", target_os = \\\"macos\\\", target_os = \\\"tvos\\\", target_os = \\\"visionos\\\", target_os = \\\"watchos\\\")))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.148\",\"target\":\"cfg(any(unix, windows, target_os = \\\"wasi\\\"))\"},{\"name\":\"untrusted\",\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.37\",\"target\":\"cfg(all(target_arch = \\\"wasm32\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(all(all(target_arch = \\\"aarch64\\\", target_endian = \\\"little\\\"), target_os = \\\"windows\\\"))\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\",\"dev_urandom_fallback\"],\"dev_urandom_fallback\":[],\"less-safe-getrandom-custom-or-rdrand\":[],\"less-safe-getrandom-espidf\":[],\"slow_tests\":[],\"std\":[\"alloc\"],\"test_logging\":[],\"unstable-testing-arm-no-hw\":[],\"unstable-testing-arm-no-neon\":[],\"wasm32_unknown_unknown_js\":[\"getrandom/js\"]}}", "rmcp-macros_0.15.0": "{\"dependencies\":[{\"name\":\"darling\",\"req\":\"^0.23\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "rmcp_0.15.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.89\"},{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1\"},{\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"serde\",\"clock\",\"std\",\"oldtime\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"serde\"],\"name\":\"chrono\",\"req\":\"^0.4.38\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"http\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"reqwest\"],\"name\":\"oauth2\",\"optional\":true,\"req\":\"^5.0\"},{\"name\":\"pastey\",\"optional\":true,\"req\":\"^0.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"features\":[\"tokio1\"],\"name\":\"process-wrap\",\"optional\":true,\"req\":\"^9.0\"},{\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9\"},{\"default_features\":false,\"features\":[\"json\",\"stream\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"name\":\"rmcp-macros\",\"optional\":true,\"req\":\"^0.15.0\"},{\"features\":[\"chrono04\"],\"name\":\"schemars\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"chrono04\"],\"kind\":\"dev\",\"name\":\"schemars\",\"req\":\"^1.1.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"sse-stream\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"features\":[\"sync\",\"macros\",\"rt\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"name\":\"tower-service\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\",\"std\",\"fmt\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"name\":\"url\",\"optional\":true,\"req\":\"^2.4\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"__reqwest\":[\"dep:reqwest\"],\"auth\":[\"dep:oauth2\",\"__reqwest\",\"dep:url\"],\"client\":[\"dep:tokio-stream\"],\"client-side-sse\":[\"dep:sse-stream\",\"dep:http\"],\"default\":[\"base64\",\"macros\",\"server\"],\"elicitation\":[\"dep:url\"],\"macros\":[\"dep:rmcp-macros\",\"dep:pastey\"],\"reqwest\":[\"__reqwest\",\"reqwest?/rustls-tls\"],\"reqwest-native-tls\":[\"__reqwest\",\"reqwest?/native-tls\"],\"reqwest-tls-no-provider\":[\"__reqwest\",\"reqwest?/rustls-tls-no-provider\"],\"schemars\":[\"dep:schemars\"],\"server\":[\"transport-async-rw\",\"dep:schemars\",\"dep:pastey\"],\"server-side-http\":[\"uuid\",\"dep:rand\",\"dep:tokio-stream\",\"dep:http\",\"dep:http-body\",\"dep:http-body-util\",\"dep:bytes\",\"dep:sse-stream\",\"dep:axum\",\"tower\"],\"tower\":[\"dep:tower-service\"],\"transport-async-rw\":[\"tokio/io-util\",\"tokio-util/codec\"],\"transport-child-process\":[\"transport-async-rw\",\"tokio/process\",\"dep:process-wrap\"],\"transport-io\":[\"transport-async-rw\",\"tokio/io-std\"],\"transport-streamable-http-client\":[\"client-side-sse\",\"transport-worker\"],\"transport-streamable-http-client-reqwest\":[\"transport-streamable-http-client\",\"__reqwest\"],\"transport-streamable-http-server\":[\"transport-streamable-http-server-session\",\"server-side-http\",\"transport-worker\"],\"transport-streamable-http-server-session\":[\"transport-async-rw\",\"dep:tokio-stream\"],\"transport-worker\":[\"dep:tokio-stream\"]}}", @@ -1421,6 +1430,7 @@ "scratch_1.0.9": "{\"dependencies\":[],\"features\":{}}", "scrypt_0.11.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"rand_core\"],\"name\":\"password-hash\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"rand_core\"],\"kind\":\"dev\",\"name\":\"password-hash\",\"req\":\"^0.5\"},{\"name\":\"pbkdf2\",\"req\":\"^0.12\"},{\"default_features\":false,\"name\":\"salsa20\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"name\":\"sha2\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"simple\",\"std\"],\"simple\":[\"password-hash\"],\"std\":[\"password-hash/std\"]}}", "sdd_3.0.10": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.6\"},{\"name\":\"loom\",\"optional\":true,\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"}],\"features\":{}}", + "sec1_0.7.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"base16ct\",\"optional\":true,\"req\":\"^0.2\"},{\"features\":[\"oid\"],\"name\":\"der\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"generic-array\",\"optional\":true,\"req\":\"^0.14.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serdect\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"alloc\":[\"der?/alloc\",\"pkcs8?/alloc\",\"zeroize?/alloc\"],\"default\":[\"der\",\"point\"],\"der\":[\"dep:der\",\"zeroize\"],\"pem\":[\"alloc\",\"der/pem\",\"pkcs8/pem\"],\"point\":[\"dep:base16ct\",\"dep:generic-array\"],\"serde\":[\"dep:serdect\"],\"std\":[\"alloc\",\"der?/std\"],\"zeroize\":[\"dep:zeroize\",\"der?/zeroize\"]}}", "seccompiler_0.5.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.153\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.27\"},{\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.9\"}],\"features\":{\"json\":[\"serde\",\"serde_json\"]}}", "secrecy_0.10.3": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"zeroize\",\"req\":\"^1.6\"}],\"features\":{}}", "secret-service_4.0.0": "{\"dependencies\":[{\"name\":\"aes\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"block-padding\",\"alloc\"],\"name\":\"cbc\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"generic-array\",\"req\":\"^0.14\"},{\"name\":\"hkdf\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"num\",\"req\":\"^0.4.0\"},{\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"openssl\",\"optional\":true,\"req\":\"^0.10.40\"},{\"name\":\"rand\",\"req\":\"^0.8.1\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0.103\"},{\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"test-with\",\"req\":\"^0.8\"},{\"features\":[\"rt\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"zbus\",\"req\":\"^4\"}],\"features\":{\"crypto-openssl\":[\"dep:openssl\"],\"crypto-rust\":[\"dep:aes\",\"dep:cbc\",\"dep:sha2\",\"dep:hkdf\"],\"rt-async-io-crypto-openssl\":[\"zbus/async-io\",\"crypto-openssl\"],\"rt-async-io-crypto-rust\":[\"zbus/async-io\",\"crypto-rust\"],\"rt-tokio-crypto-openssl\":[\"zbus/tokio\",\"crypto-openssl\"],\"rt-tokio-crypto-rust\":[\"zbus/tokio\",\"crypto-rust\"]}}", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 18c26e6a147b..2a4c2d0ffbe4 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -842,6 +842,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -2115,6 +2121,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "codex-device-key" +version = "0.0.0" +dependencies = [ + "base64 0.22.1", + "p256", + "pretty_assertions", + "rand 0.9.3", + "serde", + "serde_json", + "thiserror 2.0.18", + "url", +] + [[package]] name = "codex-exec" version = "0.0.0" @@ -3711,6 +3731,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -4415,6 +4447,20 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" @@ -4448,6 +4494,26 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "ena" version = "0.14.3" @@ -4701,6 +4767,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -6095,6 +6171,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "gzip-header" version = "1.0.0" @@ -8571,6 +8658,18 @@ dependencies = [ "supports-color 3.0.2", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.1" @@ -9000,6 +9099,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -9938,6 +10046,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -10397,6 +10515,20 @@ version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "seccompiler" version = "0.5.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 75e54abfba82..236412051177 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -24,6 +24,7 @@ members = [ "collaboration-mode-templates", "connectors", "config", + "device-key", "shell-command", "shell-escalation", "skills", @@ -283,6 +284,7 @@ os_info = "3.12.0" owo-colors = "4.3.0" path-absolutize = "3.1.1" pathdiff = "0.2" +p256 = "0.13.2" portable-pty = "0.9.0" predicates = "3" pretty_assertions = "1.4.1" diff --git a/codex-rs/device-key/BUILD.bazel b/codex-rs/device-key/BUILD.bazel new file mode 100644 index 000000000000..4ad47f84a0d2 --- /dev/null +++ b/codex-rs/device-key/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "device-key", + crate_name = "codex_device_key", +) diff --git a/codex-rs/device-key/Cargo.toml b/codex-rs/device-key/Cargo.toml new file mode 100644 index 000000000000..f61a886e0182 --- /dev/null +++ b/codex-rs/device-key/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "codex-device-key" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lints] +workspace = true + +[dependencies] +base64 = { workspace = true } +p256 = { workspace = true, features = ["ecdsa", "pkcs8"] } +rand = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } diff --git a/codex-rs/device-key/src/lib.rs b/codex-rs/device-key/src/lib.rs new file mode 100644 index 000000000000..61d34e034e7f --- /dev/null +++ b/codex-rs/device-key/src/lib.rs @@ -0,0 +1,1374 @@ +use base64::Engine; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use p256::pkcs8::EncodePublicKey; +use rand::random; +use serde::Deserialize; +use serde::Serialize; +use std::fmt; +use std::fmt::Debug; +use std::sync::Arc; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; +use thiserror::Error; +use url::Host; +use url::Url; + +mod platform; + +const SIGNING_DOMAIN: &str = "codex-device-key-sign-payload/v1"; +const DEVICE_KEY_ID_RANDOM_BYTES: usize = 32; +const DEVICE_KEY_ID_ENCODED_BYTES: usize = 43; +const DEVICE_KEY_ID_HARDWARE_SECURE_ENCLAVE_PREFIX: &str = "dk_hse_"; +const DEVICE_KEY_ID_HARDWARE_TPM_PREFIX: &str = "dk_tpm_"; +const DEVICE_KEY_ID_OS_PROTECTED_NONEXTRACTABLE_PREFIX: &str = "dk_osn_"; +const DEVICE_KEY_ID_PREFIX_LEN: usize = DEVICE_KEY_ID_HARDWARE_SECURE_ENCLAVE_PREFIX.len(); +const DEVICE_KEY_ID_LEN: usize = DEVICE_KEY_ID_PREFIX_LEN + DEVICE_KEY_ID_ENCODED_BYTES; +const INVALID_DEVICE_KEY_ID_MESSAGE: &str = + "keyId must be dk_hse_, dk_tpm_, or dk_osn_ followed by unpadded base64url-encoded 32 bytes"; +const REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE: &str = "remote_control_controller_websocket"; +const MAX_REMOTE_CONTROL_DEVICE_KEY_PROOF_TTL_SECONDS: i64 = 15 * 60; +const REMOTE_CONTROL_CLIENT_CONNECTION_PATHS: &[&str] = &[ + "/api/codex/remote/control/client", + "/wham/remote/control/client", +]; +const REMOTE_CONTROL_CLIENT_ENROLLMENT_PATHS: &[&str] = &[ + "/api/codex/remote/control/client/enroll", + "/wham/remote/control/client/enroll", +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DeviceKeyAlgorithm { + EcdsaP256Sha256, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DeviceKeyProtectionClass { + HardwareSecureEnclave, + HardwareTpm, + OsProtectedNonextractable, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DeviceKeyProtectionPolicy { + HardwareOnly, + AllowOsProtectedNonextractable, +} + +impl DeviceKeyProtectionPolicy { + fn allows(self, protection_class: DeviceKeyProtectionClass) -> bool { + match self { + Self::HardwareOnly => !protection_class.is_degraded(), + Self::AllowOsProtectedNonextractable => matches!( + protection_class, + DeviceKeyProtectionClass::HardwareSecureEnclave + | DeviceKeyProtectionClass::HardwareTpm + | DeviceKeyProtectionClass::OsProtectedNonextractable + ), + } + } +} + +impl DeviceKeyProtectionClass { + pub fn is_degraded(self) -> bool { + match self { + Self::HardwareSecureEnclave | Self::HardwareTpm => false, + Self::OsProtectedNonextractable => true, + } + } + + fn key_id_prefix(self) -> &'static str { + match self { + Self::HardwareSecureEnclave => DEVICE_KEY_ID_HARDWARE_SECURE_ENCLAVE_PREFIX, + Self::HardwareTpm => DEVICE_KEY_ID_HARDWARE_TPM_PREFIX, + Self::OsProtectedNonextractable => DEVICE_KEY_ID_OS_PROTECTED_NONEXTRACTABLE_PREFIX, + } + } +} + +impl fmt::Display for DeviceKeyProtectionClass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::HardwareSecureEnclave => f.write_str("hardware_secure_enclave"), + Self::HardwareTpm => f.write_str("hardware_tpm"), + Self::OsProtectedNonextractable => f.write_str("os_protected_nonextractable"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeyCreateRequest { + pub protection_policy: DeviceKeyProtectionPolicy, + pub binding: DeviceKeyBinding, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeyGetPublicRequest { + pub key_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeySignRequest { + pub key_id: String, + pub payload: DeviceKeySignPayload, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeyBinding { + pub account_user_id: String, + pub client_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeyInfo { + pub key_id: String, + pub public_key_spki_der: Vec, + pub algorithm: DeviceKeyAlgorithm, + pub protection_class: DeviceKeyProtectionClass, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceKeySignature { + pub signature_der: Vec, + /// Exact payload bytes covered by `signature_der`. + pub signed_payload: Vec, + pub algorithm: DeviceKeyAlgorithm, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ProviderSignature { + signature_der: Vec, + algorithm: DeviceKeyAlgorithm, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum DeviceKeySignPayload { + RemoteControlClientConnection(RemoteControlClientConnectionSignPayload), + RemoteControlClientEnrollment(RemoteControlClientEnrollmentSignPayload), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RemoteControlClientConnectionAudience { + RemoteControlClientWebsocket, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteControlClientConnectionSignPayload { + pub nonce: String, + pub audience: RemoteControlClientConnectionAudience, + pub session_id: String, + pub target_origin: String, + pub target_path: String, + pub account_user_id: String, + pub client_id: String, + pub token_sha256_base64url: String, + pub token_expires_at: i64, + pub scopes: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RemoteControlClientEnrollmentAudience { + RemoteControlClientEnrollment, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RemoteControlClientEnrollmentSignPayload { + pub nonce: String, + pub audience: RemoteControlClientEnrollmentAudience, + pub challenge_id: String, + pub target_origin: String, + pub target_path: String, + pub account_user_id: String, + pub client_id: String, + pub device_identity_sha256_base64url: String, + pub challenge_expires_at: i64, +} + +#[derive(Debug, Error)] +pub enum DeviceKeyError { + #[error( + "hardware-backed device keys are not available; set protectionPolicy to allow_os_protected_nonextractable to allow key protection class {available}" + )] + DegradedProtectionNotAllowed { available: DeviceKeyProtectionClass }, + #[error("hardware-backed device keys are not available on this platform")] + HardwareBackedKeysUnavailable, + #[error("device key not found")] + KeyNotFound, + #[error("invalid device key payload: {0}")] + InvalidPayload(&'static str), + #[error("device key platform error: {0}")] + Platform(String), + #[error("device key cryptography error: {0}")] + Crypto(String), +} + +#[derive(Debug, Clone)] +pub struct DeviceKeyStore { + provider: Arc, +} + +impl Default for DeviceKeyStore { + fn default() -> Self { + Self { + provider: platform::default_provider(), + } + } +} + +impl DeviceKeyStore { + pub fn create(&self, request: DeviceKeyCreateRequest) -> Result { + let key_id_random = random_key_id_random(); + validate_binding(&request.binding.account_user_id, &request.binding.client_id)?; + self.provider.create(ProviderCreateRequest { + key_id_random: &key_id_random, + protection_policy: request.protection_policy, + binding: &request.binding, + }) + } + + pub fn get_public( + &self, + request: DeviceKeyGetPublicRequest, + ) -> Result { + let protection_class = validate_key_id(&request.key_id)?; + self.provider.get_public(&request.key_id, protection_class) + } + + pub fn sign( + &self, + request: DeviceKeySignRequest, + ) -> Result { + let protection_class = validate_key_id(&request.key_id)?; + validate_payload(&request.payload)?; + let binding = self.provider.binding(&request.key_id, protection_class)?; + validate_payload_binding(&request.payload, &binding)?; + let signed_payload = device_key_signing_payload_bytes(&request.payload)?; + let signature = self + .provider + .sign(&request.key_id, protection_class, &signed_payload)?; + Ok(DeviceKeySignature { + signature_der: signature.signature_der, + signed_payload, + algorithm: signature.algorithm, + }) + } + + #[cfg(test)] + fn with_provider(provider: Arc) -> Self { + Self { provider } + } +} + +#[derive(Debug)] +struct ProviderCreateRequest<'a> { + key_id_random: &'a str, + protection_policy: DeviceKeyProtectionPolicy, + binding: &'a DeviceKeyBinding, +} + +impl ProviderCreateRequest<'_> { + fn key_id_for(&self, protection_class: DeviceKeyProtectionClass) -> String { + key_id_for_protection_class(protection_class, self.key_id_random) + } +} + +/// Owns platform-specific non-exportable key operations for device signing. +/// +/// Implementations must never expose a generic arbitrary-byte signing API outside this crate. The +/// crate validates and serializes accepted structured payloads before calling `sign`. +trait DeviceKeyProvider: Debug + Send + Sync { + fn create(&self, request: ProviderCreateRequest<'_>) -> Result; + fn get_public( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + ) -> Result; + fn binding( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + ) -> Result; + fn sign( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + payload: &[u8], + ) -> Result; +} + +fn random_key_id_random() -> String { + URL_SAFE_NO_PAD.encode(random::<[u8; DEVICE_KEY_ID_RANDOM_BYTES]>()) +} + +fn key_id_for_protection_class( + protection_class: DeviceKeyProtectionClass, + encoded_random: &str, +) -> String { + format!("{}{encoded_random}", protection_class.key_id_prefix()) +} + +/// Validates the account/client binding stored with a key or embedded in an accepted payload. +/// +/// Providers treat the binding as metadata, so this crate keeps empty values from entering the +/// store and later matching every other empty value by accident. +fn validate_binding(account_user_id: &str, client_id: &str) -> Result<(), DeviceKeyError> { + if account_user_id.is_empty() { + return Err(DeviceKeyError::InvalidPayload( + "accountUserId must not be empty", + )); + } + if client_id.is_empty() { + return Err(DeviceKeyError::InvalidPayload("clientId must not be empty")); + } + Ok(()) +} + +/// Keeps all externally supplied key IDs inside the random `dk_*_` namespaces created by this crate. +/// +/// Platform providers use the key ID in OS-specific labels, tags, and metadata paths. Requiring the +/// exact generated shape avoids path or tag surprises and makes the namespace auditable. +fn validate_key_id(key_id: &str) -> Result { + let (protection_class, encoded_key) = parse_key_id(key_id).ok_or( + DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE), + )?; + if key_id.len() != DEVICE_KEY_ID_LEN { + return Err(DeviceKeyError::InvalidPayload( + INVALID_DEVICE_KEY_ID_MESSAGE, + )); + } + if !URL_SAFE_NO_PAD + .decode(encoded_key) + .is_ok_and(|decoded| decoded.len() == DEVICE_KEY_ID_RANDOM_BYTES) + { + return Err(DeviceKeyError::InvalidPayload( + INVALID_DEVICE_KEY_ID_MESSAGE, + )); + } + Ok(protection_class) +} + +fn parse_key_id(key_id: &str) -> Option<(DeviceKeyProtectionClass, &str)> { + for protection_class in [ + DeviceKeyProtectionClass::HardwareSecureEnclave, + DeviceKeyProtectionClass::HardwareTpm, + DeviceKeyProtectionClass::OsProtectedNonextractable, + ] { + if let Some(encoded_key) = key_id.strip_prefix(protection_class.key_id_prefix()) { + return Some((protection_class, encoded_key)); + } + } + None +} + +/// Confirms the signed payload is for the same account/client binding as the selected device key. +/// +/// The provider can prove continuity of the key material, but app-server authorization depends on +/// binding that key to the same account and client identity used by the remote-control flow. +fn validate_payload_binding( + payload: &DeviceKeySignPayload, + binding: &DeviceKeyBinding, +) -> Result<(), DeviceKeyError> { + let (account_user_id, client_id) = match payload { + DeviceKeySignPayload::RemoteControlClientConnection(payload) => { + (&payload.account_user_id, &payload.client_id) + } + DeviceKeySignPayload::RemoteControlClientEnrollment(payload) => { + (&payload.account_user_id, &payload.client_id) + } + }; + if account_user_id != &binding.account_user_id || client_id != &binding.client_id { + return Err(DeviceKeyError::InvalidPayload( + "payload accountUserId/clientId does not match device key binding", + )); + } + Ok(()) +} + +/// Dispatches validation by accepted payload shape before any provider sees bytes to sign. +/// +/// The enum is intentionally narrow so adding another signing use case requires defining and +/// validating a new structured payload variant here. +fn validate_payload(payload: &DeviceKeySignPayload) -> Result<(), DeviceKeyError> { + match payload { + DeviceKeySignPayload::RemoteControlClientConnection(payload) => { + validate_remote_control_client_connection_payload(payload) + } + DeviceKeySignPayload::RemoteControlClientEnrollment(payload) => { + validate_remote_control_client_enrollment_payload(payload) + } + } +} + +/// Validates payloads used to prove device-key ownership while opening `/client`. +/// +/// This shape is scoped to a single controller websocket connection and is only allowed to target +/// the non-enrollment remote-control client endpoints. +fn validate_remote_control_client_connection_payload( + payload: &RemoteControlClientConnectionSignPayload, +) -> Result<(), DeviceKeyError> { + validate_nonce(&payload.nonce)?; + validate_remote_control_target( + &payload.target_origin, + &payload.target_path, + REMOTE_CONTROL_CLIENT_CONNECTION_PATHS, + )?; + if payload.session_id.is_empty() { + return Err(DeviceKeyError::InvalidPayload( + "sessionId must not be empty", + )); + } + validate_binding(&payload.account_user_id, &payload.client_id)?; + if !is_base64url_sha256(&payload.token_sha256_base64url) { + return Err(DeviceKeyError::InvalidPayload( + "tokenSha256Base64url must be a SHA-256 digest encoded as unpadded base64url", + )); + } + if payload.scopes != [REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE] { + return Err(DeviceKeyError::InvalidPayload( + "scopes must contain exactly remote_control_controller_websocket", + )); + } + validate_remote_control_expiry(payload.token_expires_at, "remote-control token")?; + Ok(()) +} + +/// Validates payloads used during device-key enrollment. +/// +/// Enrollment has a distinct payload shape and challenge identifier, so it also carries a distinct +/// endpoint allowlist from connection proofs. +fn validate_remote_control_client_enrollment_payload( + payload: &RemoteControlClientEnrollmentSignPayload, +) -> Result<(), DeviceKeyError> { + validate_nonce(&payload.nonce)?; + if payload.challenge_id.is_empty() { + return Err(DeviceKeyError::InvalidPayload( + "challengeId must not be empty", + )); + } + validate_remote_control_target( + &payload.target_origin, + &payload.target_path, + REMOTE_CONTROL_CLIENT_ENROLLMENT_PATHS, + )?; + validate_binding(&payload.account_user_id, &payload.client_id)?; + if !is_base64url_sha256(&payload.device_identity_sha256_base64url) { + return Err(DeviceKeyError::InvalidPayload( + "deviceIdentitySha256Base64url must be a SHA-256 digest encoded as unpadded base64url", + )); + } + validate_remote_control_expiry(payload.challenge_expires_at, "enrollment challenge")?; + Ok(()) +} + +/// Requires a fresh server-issued challenge with enough entropy to prevent replay guessing. +fn validate_nonce(nonce: &str) -> Result<(), DeviceKeyError> { + if !URL_SAFE_NO_PAD + .decode(nonce) + .is_ok_and(|decoded| decoded.len() >= 32) + { + return Err(DeviceKeyError::InvalidPayload( + "nonce must be at least 32 random bytes encoded as unpadded base64url", + )); + } + Ok(()) +} + +/// Validates the remote backend origin and the endpoint set for the specific signed payload shape. +/// +/// Keeping the path allowlist as an argument makes it hard to accidentally let enrollment payloads +/// sign connection endpoints, or connection payloads sign enrollment endpoints. +fn validate_remote_control_target( + target_origin: &str, + target_path: &str, + allowed_target_paths: &[&str], +) -> Result<(), DeviceKeyError> { + if !is_allowed_remote_control_origin(target_origin) { + return Err(DeviceKeyError::InvalidPayload( + "targetOrigin must be an allowed remote-control backend origin", + )); + } + if !allowed_target_paths.contains(&target_path) { + return Err(DeviceKeyError::InvalidPayload( + "targetPath must match the signed payload type's remote-control endpoint", + )); + } + Ok(()) +} + +/// Mirrors the remote-control transport allowlist for origins that may receive signed proofs. +fn is_allowed_remote_control_origin(target_origin: &str) -> bool { + let Ok(url) = Url::parse(target_origin) else { + return false; + }; + if url.path() != "/" || url.query().is_some() || url.fragment().is_some() { + return false; + } + let host = url.host(); + match url.scheme() { + "https" if is_localhost(&host) || is_allowed_chatgpt_host(&host) => true, + "http" if is_localhost(&host) => true, + _ => false, + } +} + +/// Accepts first-party chatgpt.com hosts and staging equivalents, including subdomains. +fn is_allowed_chatgpt_host(host: &Option>) -> bool { + let Some(Host::Domain(host)) = *host else { + return false; + }; + host == "chatgpt.com" + || host == "chatgpt-staging.com" + || host.ends_with(".chatgpt.com") + || host.ends_with(".chatgpt-staging.com") +} + +/// Allows local development endpoints without opening access to arbitrary private-network hosts. +fn is_localhost(host: &Option>) -> bool { + match host { + Some(Host::Domain("localhost")) => true, + Some(Host::Ipv4(ip)) => ip.is_loopback(), + Some(Host::Ipv6(ip)) => ip.is_loopback(), + _ => false, + } +} + +/// Bounds remote-control proofs to the connection or enrollment attempt that requested them. +fn validate_remote_control_expiry( + expires_at: i64, + label: &'static str, +) -> Result<(), DeviceKeyError> { + let now = current_unix_seconds()?; + if expires_at <= now { + return Err(DeviceKeyError::InvalidPayload(match label { + "enrollment challenge" => "enrollment challenge is expired", + _ => "remote-control token is expired", + })); + } + if expires_at > now + MAX_REMOTE_CONTROL_DEVICE_KEY_PROOF_TTL_SECONDS { + return Err(DeviceKeyError::InvalidPayload(match label { + "enrollment challenge" => "enrollment challenge expires too far in the future", + _ => "remote-control token expires too far in the future", + })); + } + Ok(()) +} + +/// Checks the exact digest encoding used in remote-control challenge and token bindings. +fn is_base64url_sha256(value: &str) -> bool { + URL_SAFE_NO_PAD + .decode(value) + .is_ok_and(|digest| digest.len() == 32) +} + +fn current_unix_seconds() -> Result { + let duration = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| DeviceKeyError::InvalidPayload("system clock is before Unix epoch"))?; + i64::try_from(duration.as_secs()) + .map_err(|_| DeviceKeyError::InvalidPayload("current time does not fit in i64")) +} + +/// Returns the exact bytes that device-key providers sign and verifiers must check. +/// +/// The representation is UTF-8 JSON with an explicit domain separator, sorted object keys, no +/// insignificant whitespace, and the accepted structured payload. Test vectors in this crate +/// intentionally lock the field names and ordering so non-Rust verifiers can reproduce the same +/// bytes. +pub fn device_key_signing_payload_bytes( + payload: &DeviceKeySignPayload, +) -> Result, DeviceKeyError> { + let mut canonical = serde_json::to_value(SignedPayload { + domain: SIGNING_DOMAIN, + payload, + }) + .map_err(|err| DeviceKeyError::Crypto(err.to_string()))?; + canonical.sort_all_objects(); + serde_json::to_vec(&canonical).map_err(|err| DeviceKeyError::Crypto(err.to_string())) +} + +#[derive(Serialize)] +struct SignedPayload<'a> { + domain: &'static str, + payload: &'a DeviceKeySignPayload, +} + +#[allow(dead_code)] +fn sec1_public_key_to_spki_der(sec1_public_key: &[u8]) -> Result, DeviceKeyError> { + let public_key = p256::PublicKey::from_sec1_bytes(sec1_public_key) + .map_err(|err| DeviceKeyError::Crypto(err.to_string()))?; + public_key + .to_public_key_der() + .map(|der| der.as_bytes().to_vec()) + .map_err(|err| DeviceKeyError::Crypto(err.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use p256::ecdsa::Signature; + use p256::ecdsa::SigningKey; + use p256::ecdsa::VerifyingKey; + use p256::ecdsa::signature::Signer; + use p256::ecdsa::signature::Verifier; + use p256::elliptic_curve::rand_core::OsRng; + use p256::pkcs8::DecodePublicKey; + use pretty_assertions::assert_eq; + use std::collections::HashMap; + use std::sync::Mutex; + + const TEST_TOKEN_SHA256_BASE64URL: &str = "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"; + const TEST_NONCE_BASE64URL: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + + #[derive(Debug)] + struct MemoryProvider { + class: DeviceKeyProtectionClass, + keys: Mutex>, + bindings: Mutex>, + } + + impl MemoryProvider { + fn new(class: DeviceKeyProtectionClass) -> Self { + Self { + class, + keys: Mutex::new(HashMap::new()), + bindings: Mutex::new(HashMap::new()), + } + } + } + + impl DeviceKeyProvider for MemoryProvider { + fn create( + &self, + request: ProviderCreateRequest<'_>, + ) -> Result { + if !request.protection_policy.allows(self.class) { + return Err(DeviceKeyError::DegradedProtectionNotAllowed { + available: self.class, + }); + } + let key_id = request.key_id_for(self.class); + let mut keys = self + .keys + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; + let signing_key = keys + .entry(key_id.clone()) + .or_insert_with(|| SigningKey::random(&mut OsRng)); + self.bindings + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))? + .insert(key_id.clone(), request.binding.clone()); + memory_key_info(&key_id, signing_key, self.class) + } + + fn get_public( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + ) -> Result { + if protection_class != self.class { + return Err(DeviceKeyError::KeyNotFound); + } + let keys = self + .keys + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; + let signing_key = keys.get(key_id).ok_or(DeviceKeyError::KeyNotFound)?; + memory_key_info(key_id, signing_key, self.class) + } + + fn binding( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + ) -> Result { + if protection_class != self.class { + return Err(DeviceKeyError::KeyNotFound); + } + self.bindings + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))? + .get(key_id) + .cloned() + .ok_or(DeviceKeyError::KeyNotFound) + } + + fn sign( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + payload: &[u8], + ) -> Result { + if protection_class != self.class { + return Err(DeviceKeyError::KeyNotFound); + } + let keys = self + .keys + .lock() + .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; + let signing_key = keys.get(key_id).ok_or(DeviceKeyError::KeyNotFound)?; + let signature: Signature = signing_key.sign(payload); + Ok(ProviderSignature { + signature_der: signature.to_der().as_bytes().to_vec(), + algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, + }) + } + } + + fn memory_key_info( + key_id: &str, + signing_key: &SigningKey, + class: DeviceKeyProtectionClass, + ) -> Result { + let public_key_spki_der = signing_key + .verifying_key() + .to_public_key_der() + .map_err(|err| DeviceKeyError::Crypto(err.to_string()))? + .as_bytes() + .to_vec(); + Ok(DeviceKeyInfo { + key_id: key_id.to_string(), + public_key_spki_der, + algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, + protection_class: class, + }) + } + + fn store(class: DeviceKeyProtectionClass) -> DeviceKeyStore { + DeviceKeyStore::with_provider(Arc::new(MemoryProvider::new(class))) + } + + fn create_request(protection_policy: DeviceKeyProtectionPolicy) -> DeviceKeyCreateRequest { + DeviceKeyCreateRequest { + protection_policy, + binding: DeviceKeyBinding { + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + }, + } + } + + fn remote_control_client_connection_payload() -> DeviceKeySignPayload { + DeviceKeySignPayload::RemoteControlClientConnection( + RemoteControlClientConnectionSignPayload { + nonce: TEST_NONCE_BASE64URL.to_string(), + audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, + session_id: "wssess_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/api/codex/remote/control/client".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + token_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), + token_expires_at: current_unix_seconds().expect("time should be valid") + 60, + scopes: vec![REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE.to_string()], + }, + ) + } + + fn remote_control_client_enrollment_payload() -> DeviceKeySignPayload { + DeviceKeySignPayload::RemoteControlClientEnrollment( + RemoteControlClientEnrollmentSignPayload { + nonce: TEST_NONCE_BASE64URL.to_string(), + audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment, + challenge_id: "rch_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/wham/remote/control/client/enroll".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + device_identity_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), + challenge_expires_at: current_unix_seconds().expect("time should be valid") + 60, + }, + ) + } + + fn assert_valid_generated_key_id(key_id: &str, expected_class: DeviceKeyProtectionClass) { + assert_eq!(key_id.len(), DEVICE_KEY_ID_LEN); + assert_eq!( + validate_key_id(key_id).expect("generated key id should be valid"), + expected_class + ); + let encoded_key = key_id + .strip_prefix(expected_class.key_id_prefix()) + .expect("generated key id should use protection-class prefix"); + assert_eq!(encoded_key.len(), DEVICE_KEY_ID_ENCODED_BYTES); + assert_eq!( + URL_SAFE_NO_PAD + .decode(encoded_key) + .expect("generated key id should be base64url") + .len(), + DEVICE_KEY_ID_RANDOM_BYTES + ); + } + + #[test] + fn create_requires_explicit_degraded_protection() { + let err = store(DeviceKeyProtectionClass::OsProtectedNonextractable) + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect_err("OS-protected fallback should require opt-in"); + + assert!( + matches!( + err, + DeviceKeyError::DegradedProtectionNotAllowed { + available: DeviceKeyProtectionClass::OsProtectedNonextractable, + } + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn create_allows_os_protected_nonextractable_policy() { + let info = store(DeviceKeyProtectionClass::OsProtectedNonextractable) + .create(create_request( + DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable, + )) + .expect("OS-protected fallback should be allowed by policy"); + + assert_eq!( + info.protection_class, + DeviceKeyProtectionClass::OsProtectedNonextractable + ); + assert_valid_generated_key_id( + &info.key_id, + DeviceKeyProtectionClass::OsProtectedNonextractable, + ); + } + + #[test] + fn create_generates_distinct_key_ids() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let first = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let second = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + + assert_ne!(second.key_id, first.key_id); + assert_valid_generated_key_id(&first.key_id, DeviceKeyProtectionClass::HardwareTpm); + assert_valid_generated_key_id(&second.key_id, DeviceKeyProtectionClass::HardwareTpm); + } + + #[test] + fn key_id_validation_rejects_untrusted_namespaces() { + let valid_suffix = URL_SAFE_NO_PAD.encode([0_u8; DEVICE_KEY_ID_RANDOM_BYTES]); + + for key_id in [ + String::new(), + "dk_".to_string(), + "dk_hse_".to_string(), + format!("bad_{valid_suffix}"), + format!("dk_bad_{valid_suffix}"), + format!( + "{}{}", + DeviceKeyProtectionClass::HardwareSecureEnclave.key_id_prefix(), + &valid_suffix[..DEVICE_KEY_ID_ENCODED_BYTES - 1] + ), + format!( + "{}{valid_suffix}A", + DeviceKeyProtectionClass::HardwareTpm.key_id_prefix() + ), + format!( + "{}{}=", + DeviceKeyProtectionClass::OsProtectedNonextractable.key_id_prefix(), + &valid_suffix[..DEVICE_KEY_ID_ENCODED_BYTES - 1] + ), + format!( + "{}{}+", + DeviceKeyProtectionClass::HardwareSecureEnclave.key_id_prefix(), + &valid_suffix[..DEVICE_KEY_ID_ENCODED_BYTES - 1] + ), + ] { + let err = validate_key_id(&key_id).expect_err("malformed key id should fail"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE) + ), + "unexpected error for {key_id:?}: {err:?}" + ); + } + } + + #[test] + fn public_operations_reject_malformed_key_id_before_provider_use() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let malformed_key_id = "not-a-device-key".to_string(); + + let err = store + .get_public(DeviceKeyGetPublicRequest { + key_id: malformed_key_id.clone(), + }) + .expect_err("malformed get_public key id should fail"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE) + ), + "unexpected get_public error: {err:?}" + ); + + let err = store + .sign(DeviceKeySignRequest { + key_id: malformed_key_id, + payload: remote_control_client_connection_payload(), + }) + .expect_err("malformed sign key id should fail"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE) + ), + "unexpected sign error: {err:?}" + ); + } + + #[test] + fn sign_rejects_empty_account_user_id() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.account_user_id.clear(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("empty account user id should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload("accountUserId must not be empty") + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_uses_structured_payload() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let payload = remote_control_client_connection_payload(); + let signed_payload = + device_key_signing_payload_bytes(&payload).expect("payload should serialize"); + let signature = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect("sign should succeed"); + assert_eq!(signature.signed_payload, signed_payload); + + let verifying_key = VerifyingKey::from_public_key_der(&info.public_key_spki_der) + .expect("public key should decode"); + let signature = + Signature::from_der(&signature.signature_der).expect("signature should decode"); + verifying_key + .verify(&signed_payload, &signature) + .expect("signature should verify against structured payload"); + } + + #[test] + fn signing_payload_bytes_are_stable() { + let payload = DeviceKeySignPayload::RemoteControlClientConnection( + RemoteControlClientConnectionSignPayload { + nonce: TEST_NONCE_BASE64URL.to_string(), + audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, + session_id: "wssess_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/api/codex/remote/control/client".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + token_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), + token_expires_at: 1_700_000_000, + scopes: vec![REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE.to_string()], + }, + ); + + let bytes = device_key_signing_payload_bytes(&payload).expect("payload should serialize"); + + assert_eq!( + String::from_utf8(bytes).expect("payload should be utf-8"), + concat!( + "{\"domain\":\"codex-device-key-sign-payload/v1\",", + "\"payload\":{\"accountUserId\":\"account-user-1\",", + "\"audience\":\"remote_control_client_websocket\",", + "\"clientId\":\"cli_123\",", + "\"nonce\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",", + "\"scopes\":[\"remote_control_controller_websocket\"],", + "\"sessionId\":\"wssess_123\",", + "\"targetOrigin\":\"https://chatgpt.com\",", + "\"targetPath\":\"/api/codex/remote/control/client\",", + "\"tokenExpiresAt\":1700000000,", + "\"tokenSha256Base64url\":\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\",", + "\"type\":\"remoteControlClientConnection\"}}" + ) + ); + } + + #[test] + fn enrollment_signing_payload_bytes_are_stable() { + let payload = DeviceKeySignPayload::RemoteControlClientEnrollment( + RemoteControlClientEnrollmentSignPayload { + nonce: TEST_NONCE_BASE64URL.to_string(), + audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment, + challenge_id: "rch_123".to_string(), + target_origin: "https://chatgpt.com".to_string(), + target_path: "/wham/remote/control/client/enroll".to_string(), + account_user_id: "account-user-1".to_string(), + client_id: "cli_123".to_string(), + device_identity_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), + challenge_expires_at: 1_700_000_060, + }, + ); + + let bytes = device_key_signing_payload_bytes(&payload).expect("payload should serialize"); + + assert_eq!( + String::from_utf8(bytes).expect("payload should be utf-8"), + concat!( + "{\"domain\":\"codex-device-key-sign-payload/v1\",", + "\"payload\":{\"accountUserId\":\"account-user-1\",", + "\"audience\":\"remote_control_client_enrollment\",", + "\"challengeExpiresAt\":1700000060,", + "\"challengeId\":\"rch_123\",", + "\"clientId\":\"cli_123\",", + "\"deviceIdentitySha256Base64url\":\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\",", + "\"nonce\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",", + "\"targetOrigin\":\"https://chatgpt.com\",", + "\"targetPath\":\"/wham/remote/control/client/enroll\",", + "\"type\":\"remoteControlClientEnrollment\"}}" + ) + ); + } + + #[test] + fn sign_rejects_malformed_token_hash() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.token_sha256_base64url = "not-a-sha256".to_string(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("malformed token hash should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "tokenSha256Base64url must be a SHA-256 digest encoded as unpadded base64url" + ) + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_unexpected_scopes() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.scopes = vec!["other_scope".to_string()]; + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("unexpected scope should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "scopes must contain exactly remote_control_controller_websocket" + ) + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_malformed_enrollment_identity_hash() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_enrollment_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientEnrollment(enrollment_payload) => { + enrollment_payload.device_identity_sha256_base64url = "not-a-sha256".to_string(); + } + DeviceKeySignPayload::RemoteControlClientConnection(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("malformed device identity hash should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "deviceIdentitySha256Base64url must be a SHA-256 digest encoded as unpadded base64url" + ) + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_empty_target_binding() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.target_origin.clear(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("empty target origin should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "targetOrigin must be an allowed remote-control backend origin" + ) + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_remote_control_paths_for_other_payload_shapes() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut connection_payload = remote_control_client_connection_payload(); + match &mut connection_payload { + DeviceKeySignPayload::RemoteControlClientConnection(payload) => { + payload.target_path = "/api/codex/remote/control/client/enroll".to_string(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id.clone(), + payload: connection_payload, + }) + .expect_err("connection payload should reject enrollment path"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "targetPath must match the signed payload type's remote-control endpoint" + ) + ), + "unexpected connection path error: {err:?}" + ); + + let mut enrollment_payload = remote_control_client_enrollment_payload(); + match &mut enrollment_payload { + DeviceKeySignPayload::RemoteControlClientEnrollment(payload) => { + payload.target_path = "/wham/remote/control/client".to_string(); + } + DeviceKeySignPayload::RemoteControlClientConnection(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload: enrollment_payload, + }) + .expect_err("enrollment payload should reject connection path"); + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "targetPath must match the signed payload type's remote-control endpoint" + ) + ), + "unexpected enrollment path error: {err:?}" + ); + } + + #[test] + fn remote_control_origin_matches_remote_transport_allowlist() { + for origin in [ + "https://chatgpt.com", + "https://chatgpt-staging.com", + "https://ab.chatgpt.com", + "https://ab.chatgpt-staging.com", + "http://localhost:8080", + "https://localhost:8443", + "http://127.0.0.1:8080", + "http://[::1]:8080", + ] { + assert!( + is_allowed_remote_control_origin(origin), + "expected allowed origin: {origin}" + ); + } + + for origin in [ + "http://chatgpt.com", + "https://chat.openai.com", + "https://api.openai.com", + "https://chatgpt.com.evil.com", + "https://evilchatgpt.com", + "https://foo.localhost", + "https://localhost.evil.com", + "https://192.168.1.2", + "https://chatgpt.com/backend-api", + "https://chatgpt.com?query=1", + ] { + assert!( + !is_allowed_remote_control_origin(origin), + "expected rejected origin: {origin}" + ); + } + } + + #[test] + fn sign_rejects_empty_session_binding() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.session_id.clear(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("empty session id should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload("sessionId must not be empty") + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_empty_client_id() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.client_id.clear(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("empty client id should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload("clientId must not be empty") + ), + "unexpected error: {err:?}" + ); + } + + #[test] + fn sign_rejects_mismatched_binding() { + let store = store(DeviceKeyProtectionClass::HardwareTpm); + let info = store + .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)) + .expect("create should succeed"); + let mut payload = remote_control_client_connection_payload(); + match &mut payload { + DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { + connection_payload.account_user_id = "other-account-user".to_string(); + } + DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), + } + + let err = store + .sign(DeviceKeySignRequest { + key_id: info.key_id, + payload, + }) + .expect_err("mismatched binding should fail"); + + assert!( + matches!( + err, + DeviceKeyError::InvalidPayload( + "payload accountUserId/clientId does not match device key binding" + ) + ), + "unexpected error: {err:?}" + ); + } +} diff --git a/codex-rs/device-key/src/platform.rs b/codex-rs/device-key/src/platform.rs new file mode 100644 index 000000000000..3dbcb168e7ed --- /dev/null +++ b/codex-rs/device-key/src/platform.rs @@ -0,0 +1,51 @@ +use crate::DeviceKeyBinding; +use crate::DeviceKeyError; +use crate::DeviceKeyInfo; +use crate::DeviceKeyProtectionClass; +use crate::DeviceKeyProvider; +use crate::ProviderCreateRequest; +use crate::ProviderSignature; +use std::sync::Arc; + +pub(crate) fn default_provider() -> Arc { + Arc::new(UnsupportedDeviceKeyProvider) +} + +#[derive(Debug)] +pub(crate) struct UnsupportedDeviceKeyProvider; + +impl DeviceKeyProvider for UnsupportedDeviceKeyProvider { + fn create(&self, request: ProviderCreateRequest<'_>) -> Result { + let _ = request.key_id_for(DeviceKeyProtectionClass::HardwareTpm); + let _ = request + .protection_policy + .allows(DeviceKeyProtectionClass::HardwareTpm); + let _ = request.binding; + Err(DeviceKeyError::HardwareBackedKeysUnavailable) + } + + fn get_public( + &self, + _key_id: &str, + _protection_class: DeviceKeyProtectionClass, + ) -> Result { + Err(DeviceKeyError::KeyNotFound) + } + + fn binding( + &self, + _key_id: &str, + _protection_class: DeviceKeyProtectionClass, + ) -> Result { + Err(DeviceKeyError::KeyNotFound) + } + + fn sign( + &self, + _key_id: &str, + _protection_class: DeviceKeyProtectionClass, + _payload: &[u8], + ) -> Result { + Err(DeviceKeyError::KeyNotFound) + } +}