From 6030f309017e3647fce405d2f3b0e25e10021b99 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 14 Mar 2026 12:14:30 +0100 Subject: [PATCH 1/7] feat(user-agent-auth): add RSA and ECDSA auth key types Extend user-agent authentication to support Ed25519, ECDSA (secp256k1), and RSA (PSS+SHA-256) with minimal protocol and storage changes. Add key_type to auth requests and useragent_client, update key parsing/signature verification paths, and keep backward compatibility by treating UNSPECIFIED as Ed25519. --- protobufs/user_agent.proto | 8 + server/.cargo/config.toml | 4 + server/Cargo.lock | 237 ++++++++++++++++-- server/Cargo.toml | 4 + server/crates/arbiter-proto/Cargo.toml | 1 + server/crates/arbiter-proto/build.rs | 6 +- server/crates/arbiter-server/Cargo.toml | 5 +- .../2026-03-13-000000_add_key_type/down.sql | 2 + .../2026-03-13-000000_add_key_type/up.sql | 1 + .../src/actors/user_agent/auth.rs | 52 ++-- .../src/actors/user_agent/auth/state.rs | 149 +++++++++-- .../src/actors/user_agent/session.rs | 31 +-- server/crates/arbiter-server/src/db/mod.rs | 8 +- server/crates/arbiter-server/src/db/models.rs | 40 ++- server/crates/arbiter-server/src/db/schema.rs | 1 + .../arbiter-server/tests/user_agent/auth.rs | 11 +- server/crates/arbiter-useragent/Cargo.toml | 5 + server/crates/arbiter-useragent/src/grpc.rs | 8 +- server/crates/arbiter-useragent/src/lib.rs | 80 +++++- server/crates/arbiter-useragent/tests/auth.rs | 27 +- 20 files changed, 556 insertions(+), 124 deletions(-) create mode 100644 server/.cargo/config.toml create mode 100644 server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/down.sql create mode 100644 server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/up.sql diff --git a/protobufs/user_agent.proto b/protobufs/user_agent.proto index cfc00ae..fcf508d 100644 --- a/protobufs/user_agent.proto +++ b/protobufs/user_agent.proto @@ -5,9 +5,17 @@ package arbiter.user_agent; import "google/protobuf/empty.proto"; import "evm.proto"; +enum KeyType { + KEY_TYPE_UNSPECIFIED = 0; + KEY_TYPE_ED25519 = 1; + KEY_TYPE_ECDSA_SECP256K1 = 2; + KEY_TYPE_RSA = 3; +} + message AuthChallengeRequest { bytes pubkey = 1; optional string bootstrap_token = 2; + KeyType key_type = 3; } message AuthChallenge { diff --git a/server/.cargo/config.toml b/server/.cargo/config.toml new file mode 100644 index 0000000..8390ed5 --- /dev/null +++ b/server/.cargo/config.toml @@ -0,0 +1,4 @@ +[profile.dev] +# Override global Cranelift backend: Cranelift does not propagate cargo:rustc-link-lib +# from native dependencies (aws-lc-sys etc.) to lld-link, causing undefined symbol errors. +codegen-backend = "llvm" diff --git a/server/Cargo.lock b/server/Cargo.lock index e13fa32..1586320 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -654,7 +654,7 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fa0c53e8c1e1ef4d01066b01c737fb62fc9397ab52c6e7bb5669f97d281b9bc" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.117", @@ -691,6 +691,7 @@ dependencies = [ "miette", "prost", "prost-types", + "protoc-bin-vendored", "rand 0.10.0", "rcgen", "rstest", @@ -730,9 +731,12 @@ dependencies = [ "rand 0.10.0", "rcgen", "restructed", + "rsa", "rustls", "secrecy", + "sha2 0.10.9", "smlang", + "spki", "strum", "test-log", "thiserror", @@ -760,9 +764,14 @@ dependencies = [ "async-trait", "ed25519-dalek", "http", + "k256", "kameo", + "rand 0.10.0", + "rsa", "rustls-webpki", + "sha2 0.10.9", "smlang", + "spki", "thiserror", "tokio", "tokio-stream", @@ -1334,9 +1343,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "2.1.6" +version = "2.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0f582957c24870b7bfd12bf562c40b4734b533cafbaf8ded31d6d85f462c01" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" dependencies = [ "blst", "cc", @@ -1349,9 +1358,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -1615,7 +1624,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest 0.11.1", + "digest 0.11.2", "fiat-crypto 0.3.0", "rustc_version 0.4.1", "subtle", @@ -1639,8 +1648,18 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -1658,13 +1677,37 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + [[package]] name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn 2.0.117", ] @@ -1696,6 +1739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1759,9 +1803,9 @@ dependencies = [ [[package]] name = "diesel" -version = "2.3.6" +version = "2.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b6c2fc184a6fb6ebcf5f9a5e3bbfa84d8fd268cdfcce4ed508979a6259494d" +checksum = "f4ae09a41a4b89f94ec1e053623da8340d996bc32c6517d325a9daad9b239358" dependencies = [ "chrono", "diesel_derives", @@ -1844,9 +1888,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "285743a676ccb6b3e116bc14cc69319b957867930ae9c4822f8e0f54509d7243" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "block-buffer 0.12.0", "crypto-common 0.2.1", @@ -1875,7 +1919,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e" dependencies = [ - "darling", + "darling 0.21.3", "either", "heck", "proc-macro2", @@ -2876,6 +2920,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -2897,9 +2944,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libsqlite3-sys" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ "pkg-config", "vcpkg", @@ -3109,6 +3156,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -3124,6 +3187,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3199,9 +3273,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "opaque-debug" @@ -3293,6 +3367,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3352,6 +3435,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -3567,6 +3661,70 @@ dependencies = [ "prost", ] +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + [[package]] name = "pulldown-cmark" version = "0.13.1" @@ -3935,6 +4093,27 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature 2.2.0", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -4302,9 +4481,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64", "chrono", @@ -4321,11 +4500,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -4360,7 +4539,7 @@ checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest 0.11.1", + "digest 0.11.2", ] [[package]] @@ -4482,6 +4661,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.3" @@ -5065,9 +5250,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", diff --git a/server/Cargo.toml b/server/Cargo.toml index cd97da5..03ffbbe 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -36,3 +36,7 @@ rcgen = { version = "0.14.7", features = [ "x509-parser", "zeroize", ], default-features = false } +k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] } +rsa = { version = "0.9", features = ["sha2"] } +sha2 = "0.10" +spki = "0.7" diff --git a/server/crates/arbiter-proto/Cargo.toml b/server/crates/arbiter-proto/Cargo.toml index 0673f8a..ca45b97 100644 --- a/server/crates/arbiter-proto/Cargo.toml +++ b/server/crates/arbiter-proto/Cargo.toml @@ -24,6 +24,7 @@ async-trait.workspace = true [build-dependencies] tonic-prost-build = "0.14.3" +protoc-bin-vendored = "3" [dev-dependencies] rstest.workspace = true diff --git a/server/crates/arbiter-proto/build.rs b/server/crates/arbiter-proto/build.rs index 18de523..e61600d 100644 --- a/server/crates/arbiter-proto/build.rs +++ b/server/crates/arbiter-proto/build.rs @@ -3,6 +3,11 @@ use tonic_prost_build::configure; static PROTOBUF_DIR: &str = "../../../protobufs"; fn main() -> Result<(), Box> { + if std::env::var("PROTOC").is_err() { + println!("cargo:warning=PROTOC environment variable not set, using vendored protoc"); + let protoc = protoc_bin_vendored::protoc_bin_path().unwrap(); + unsafe { std::env::set_var("PROTOC", protoc) }; + } println!("cargo::rerun-if-changed={PROTOBUF_DIR}"); @@ -17,7 +22,6 @@ fn main() -> Result<(), Box> { ], &[PROTOBUF_DIR.to_string()], ) - .unwrap(); Ok(()) } diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index 5a2856c..4fcb05e 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -42,7 +42,10 @@ argon2 = { version = "0.5.3", features = ["zeroize"] } restructed = "0.2.2" strum = { version = "0.27.2", features = ["derive"] } pem = "3.0.6" -k256 = "0.13.4" +k256.workspace = true +rsa.workspace = true +sha2.workspace = true +spki.workspace = true alloy.workspace = true arbiter-tokens-registry.path = "../arbiter-tokens-registry" diff --git a/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/down.sql b/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/down.sql new file mode 100644 index 0000000..41b541c --- /dev/null +++ b/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/down.sql @@ -0,0 +1,2 @@ +-- Not reversible without data loss; drop the column to revert +ALTER TABLE useragent_client DROP COLUMN key_type; diff --git a/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/up.sql b/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/up.sql new file mode 100644 index 0000000..1d494f0 --- /dev/null +++ b/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/up.sql @@ -0,0 +1 @@ +ALTER TABLE useragent_client ADD COLUMN key_type INTEGER NOT NULL DEFAULT 1; diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth.rs b/server/crates/arbiter-server/src/actors/user_agent/auth.rs index 543dc87..3da8291 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth.rs @@ -1,13 +1,12 @@ use arbiter_proto::proto::user_agent::{ - AuthChallengeRequest, AuthChallengeSolution, UserAgentRequest, + AuthChallengeRequest, AuthChallengeSolution, KeyType as ProtoKeyType, UserAgentRequest, user_agent_request::Payload as UserAgentRequestPayload, }; -use ed25519_dalek::VerifyingKey; use tracing::error; use crate::actors::user_agent::{ UserAgentConnection, - auth::state::{AuthContext, AuthStateMachine}, session::UserAgentSession, + auth::state::{AuthContext, AuthPublicKey, AuthStateMachine}, session::UserAgentSession, }; #[derive(thiserror::Error, Debug, PartialEq)] @@ -37,28 +36,50 @@ pub enum Error { mod state; use state::*; +fn parse_pubkey(key_type: ProtoKeyType, pubkey: Vec) -> Result { + match key_type { + // UNSPECIFIED treated as Ed25519 for backward compatibility + ProtoKeyType::Unspecified | ProtoKeyType::Ed25519 => { + let pubkey_bytes = pubkey.as_array().ok_or(Error::InvalidClientPubkeyLength)?; + let key = ed25519_dalek::VerifyingKey::from_bytes(pubkey_bytes) + .map_err(|_| Error::InvalidAuthPubkeyEncoding)?; + Ok(AuthPublicKey::Ed25519(key)) + } + ProtoKeyType::EcdsaSecp256k1 => { + // Public key is sent as 33-byte SEC1 compressed point + let key = k256::ecdsa::VerifyingKey::from_sec1_bytes(&pubkey) + .map_err(|_| Error::InvalidAuthPubkeyEncoding)?; + Ok(AuthPublicKey::EcdsaSecp256k1(key)) + } + ProtoKeyType::Rsa => { + use rsa::pkcs8::DecodePublicKey as _; + let key = rsa::RsaPublicKey::from_public_key_der(&pubkey) + .map_err(|_| Error::InvalidAuthPubkeyEncoding)?; + Ok(AuthPublicKey::Rsa(key)) + } + } +} + fn parse_auth_event(payload: UserAgentRequestPayload) -> Result { match payload { UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest { pubkey, bootstrap_token: None, + key_type, }) => { - let pubkey_bytes = pubkey.as_array().ok_or(Error::InvalidClientPubkeyLength)?; - let pubkey = VerifyingKey::from_bytes(pubkey_bytes) - .map_err(|_| Error::InvalidAuthPubkeyEncoding)?; + let kt = ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified); Ok(AuthEvents::AuthRequest(ChallengeRequest { - pubkey: pubkey.into(), + pubkey: parse_pubkey(kt, pubkey)?, })) } UserAgentRequestPayload::AuthChallengeRequest(AuthChallengeRequest { pubkey, bootstrap_token: Some(token), + key_type, }) => { - let pubkey_bytes = pubkey.as_array().ok_or(Error::InvalidClientPubkeyLength)?; - let pubkey = VerifyingKey::from_bytes(pubkey_bytes) - .map_err(|_| Error::InvalidAuthPubkeyEncoding)?; + let kt = ProtoKeyType::try_from(key_type).unwrap_or(ProtoKeyType::Unspecified); Ok(AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { - pubkey: pubkey.into(), + pubkey: parse_pubkey(kt, pubkey)?, token, })) } @@ -71,11 +92,11 @@ fn parse_auth_event(payload: UserAgentRequestPayload) -> Result Result { +pub async fn authenticate(props: &mut UserAgentConnection) -> Result { let mut state = AuthStateMachine::new(AuthContext::new(props)); loop { - // This is needed because `state` now holds mutable reference to `ConnectionProps`, so we can't directly access `props` here + // `state` holds a mutable reference to `props` so we can't access it directly here let transport = state.context_mut().conn.transport.as_mut(); let Some(UserAgentRequest { payload: Some(payload), @@ -110,9 +131,8 @@ pub async fn authenticate(props: &mut UserAgentConnection) -> Result Result { - let key = authenticate(&mut props).await?; - let session = UserAgentSession::new(props, key.clone()); + let _key = authenticate(&mut props).await?; + let session = UserAgentSession::new(props); Ok(session) } diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs index 9a4cf0c..2acd45b 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -1,30 +1,64 @@ use arbiter_proto::proto::user_agent::{ - AuthChallenge, UserAgentResponse, - user_agent_response::Payload as UserAgentResponsePayload, + AuthChallenge, UserAgentResponse, user_agent_response::Payload as UserAgentResponsePayload, }; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update}; use diesel_async::RunQueryDsl; -use ed25519_dalek::VerifyingKey; use tracing::error; use super::Error; use crate::{ actors::{bootstrap::ConsumeToken, user_agent::UserAgentConnection}, - db::schema, + db::{models::KeyType, schema}, }; +/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake. +#[derive(Clone)] +pub enum AuthPublicKey { + Ed25519(ed25519_dalek::VerifyingKey), + /// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s). + EcdsaSecp256k1(k256::ecdsa::VerifyingKey), + /// RSA-2048+ public key; signature bytes are PSS+SHA-256. + Rsa(rsa::RsaPublicKey), +} + +impl AuthPublicKey { + /// Canonical bytes stored in DB and echoed back in the challenge. + /// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI. + pub fn to_stored_bytes(&self) -> Vec { + match self { + AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(), + // SEC1 compressed (33 bytes) is the natural compact format for secp256k1 + AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(), + AuthPublicKey::Rsa(k) => { + use rsa::pkcs8::EncodePublicKey as _; + k.to_public_key_der() + .expect("rsa SPKI encoding is infallible") + .to_vec() + } + } + } + + pub fn key_type(&self) -> KeyType { + match self { + AuthPublicKey::Ed25519(_) => KeyType::Ed25519, + AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1, + AuthPublicKey::Rsa(_) => KeyType::Rsa, + } + } +} + pub struct ChallengeRequest { - pub pubkey: VerifyingKey, + pub pubkey: AuthPublicKey, } pub struct BootstrapAuthRequest { - pub pubkey: VerifyingKey, + pub pubkey: AuthPublicKey, pub token: String, } pub struct ChallengeContext { pub challenge: AuthChallenge, - pub key: VerifyingKey, + pub key: AuthPublicKey, } pub struct ChallengeSolution { @@ -36,8 +70,8 @@ smlang::statemachine!( custom_error: true, transitions: { *Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext), - Init + BootstrapAuthRequest(BootstrapAuthRequest) [async verify_bootstrap_token] / provide_key_bootstrap = AuthOk(VerifyingKey), - SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) [async verify_solution] / provide_key = AuthOk(VerifyingKey), + Init + BootstrapAuthRequest(BootstrapAuthRequest) [async verify_bootstrap_token] / provide_key_bootstrap = AuthOk(AuthPublicKey), + SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) [async verify_solution] / provide_key = AuthOk(AuthPublicKey), } ); @@ -76,7 +110,9 @@ async fn create_nonce(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Resu }) } -async fn register_key(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Result<(), Error> { +async fn register_key(db: &crate::db::DatabasePool, pubkey: &AuthPublicKey) -> Result<(), Error> { + let pubkey_bytes = pubkey.to_stored_bytes(); + let key_type = pubkey.key_type(); let mut conn = db.get().await.map_err(|e| { error!(error = ?e, "Database pool error"); Error::DatabasePoolUnavailable @@ -84,8 +120,9 @@ async fn register_key(db: &crate::db::DatabasePool, pubkey_bytes: &[u8]) -> Resu diesel::insert_into(schema::useragent_client::table) .values(( - schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()), + schema::useragent_client::public_key.eq(pubkey_bytes), schema::useragent_client::nonce.eq(1), + schema::useragent_client::key_type.eq(key_type), )) .execute(&mut conn) .await @@ -115,15 +152,34 @@ impl AuthStateMachineContext for AuthContext<'_> { ChallengeContext { challenge, key }: &ChallengeContext, ChallengeSolution { solution }: &ChallengeSolution, ) -> Result { - let formatted_challenge = - arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey); + let formatted = arbiter_proto::format_challenge(challenge.nonce, &challenge.pubkey); - let signature = solution.as_slice().try_into().map_err(|_| { - error!(?solution, "Invalid signature length"); - Error::InvalidChallengeSolution - })?; - - let valid = key.verify_strict(&formatted_challenge, &signature).is_ok(); + let valid = match key { + AuthPublicKey::Ed25519(vk) => { + let sig = solution.as_slice().try_into().map_err(|_| { + error!(?solution, "Invalid Ed25519 signature length"); + Error::InvalidChallengeSolution + })?; + vk.verify_strict(&formatted, &sig).is_ok() + } + AuthPublicKey::EcdsaSecp256k1(vk) => { + use k256::ecdsa::signature::Verifier as _; + let sig = k256::ecdsa::Signature::try_from(solution.as_slice()).map_err(|_| { + error!(?solution, "Invalid ECDSA signature bytes"); + Error::InvalidChallengeSolution + })?; + vk.verify(&formatted, &sig).is_ok() + } + AuthPublicKey::Rsa(pk) => { + use rsa::signature::Verifier as _; + let verifying_key = rsa::pss::VerifyingKey::::new(pk.clone()); + let sig = rsa::pss::Signature::try_from(solution.as_slice()).map_err(|_| { + error!(?solution, "Invalid RSA signature bytes"); + Error::InvalidChallengeSolution + })?; + verifying_key.verify(&formatted, &sig).is_ok() + } + }; Ok(valid) } @@ -132,10 +188,11 @@ impl AuthStateMachineContext for AuthContext<'_> { &mut self, ChallengeRequest { pubkey }: ChallengeRequest, ) -> Result { - let nonce = create_nonce(&self.conn.db, pubkey.as_bytes()).await?; + let stored_bytes = pubkey.to_stored_bytes(); + let nonce = create_nonce(&self.conn.db, &stored_bytes).await?; let challenge = AuthChallenge { - pubkey: pubkey.as_bytes().to_vec(), + pubkey: stored_bytes, nonce, }; @@ -171,16 +228,16 @@ impl AuthStateMachineContext for AuthContext<'_> { }) .await .map_err(|e| { - error!(?pubkey, "Failed to consume bootstrap token: {e}"); + error!(?e, "Failed to consume bootstrap token"); Error::BootstrapperActorUnreachable })?; if !token_ok { - error!(?pubkey, "Invalid bootstrap token provided"); + error!("Invalid bootstrap token provided"); return Err(Error::InvalidBootstrapToken); } - register_key(&self.conn.db, pubkey.as_bytes()).await?; + register_key(&self.conn.db, pubkey).await?; Ok(true) } @@ -188,7 +245,7 @@ impl AuthStateMachineContext for AuthContext<'_> { fn provide_key_bootstrap( &mut self, event_data: BootstrapAuthRequest, - ) -> Result { + ) -> Result { Ok(event_data.pubkey) } @@ -196,7 +253,45 @@ impl AuthStateMachineContext for AuthContext<'_> { &mut self, state_data: &ChallengeContext, _: ChallengeSolution, - ) -> Result { - Ok(state_data.key) + ) -> Result { + // ChallengeContext.key cannot be taken by value because smlang passes it by ref; + // we reconstruct stored bytes and return them wrapped in Ed25519 placeholder. + // Session uses only the raw bytes, so we carry them via a Vec. + // IMPORTANT: do NOT simplify this by storing the key type separately — the + // `AuthPublicKey` enum IS the source of truth for key bytes and type. + // + // smlang state-machine trait requires returning an owned value from `provide_key`, + // but `state_data` is only available by shared reference here. We extract the + // stored bytes and re-wrap as the correct variant so the caller can call + // `to_stored_bytes()` / `key_type()` without losing information. + let bytes = state_data.challenge.pubkey.clone(); + let key_type = state_data.key.key_type(); + let rebuilt = match key_type { + crate::db::models::KeyType::Ed25519 => { + let arr: &[u8; 32] = bytes + .as_slice() + .try_into() + .expect("ed25519 pubkey must be 32 bytes in challenge"); + AuthPublicKey::Ed25519( + ed25519_dalek::VerifyingKey::from_bytes(arr) + .expect("key was already validated in parse_auth_event"), + ) + } + crate::db::models::KeyType::EcdsaSecp256k1 => { + // bytes are SEC1 compressed (33 bytes produced by to_encoded_point(true)) + AuthPublicKey::EcdsaSecp256k1( + k256::ecdsa::VerifyingKey::from_sec1_bytes(&bytes) + .expect("ecdsa key was already validated in parse_auth_event"), + ) + } + crate::db::models::KeyType::Rsa => { + use rsa::pkcs8::DecodePublicKey as _; + AuthPublicKey::Rsa( + rsa::RsaPublicKey::from_public_key_der(&bytes) + .expect("rsa key was already validated in parse_auth_event"), + ) + } + }; + Ok(rebuilt) } } diff --git a/server/crates/arbiter-server/src/actors/user_agent/session.rs b/server/crates/arbiter-server/src/actors/user_agent/session.rs index ae4cde8..b686796 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/session.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/session.rs @@ -3,17 +3,15 @@ use std::{ops::DerefMut, sync::Mutex}; use arbiter_proto::proto::{ evm as evm_proto, user_agent::{ - ClientConnectionCancel, ClientConnectionRequest, UnsealEncryptedKey, UnsealResult, UnsealStart, UnsealStartResponse, UserAgentRequest, - UserAgentResponse, user_agent_request::Payload as UserAgentRequestPayload, + ClientConnectionCancel, ClientConnectionRequest, UnsealEncryptedKey, UnsealResult, + UnsealStart, UnsealStartResponse, UserAgentRequest, UserAgentResponse, + user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, }, }; use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use ed25519_dalek::VerifyingKey; -use kameo::{ - Actor, - error::SendError, messages, prelude::Context, -}; +use kameo::{Actor, error::SendError, messages, prelude::Context}; use memsafe::MemSafe; use tokio::{select, sync::watch}; use tracing::{error, info}; @@ -41,15 +39,13 @@ pub enum Error { pub struct UserAgentSession { props: UserAgentConnection, - key: VerifyingKey, state: UserAgentStateMachine, } impl UserAgentSession { - pub(crate) fn new(props: UserAgentConnection, key: VerifyingKey) -> Self { + pub(crate) fn new(props: UserAgentConnection) -> Self { Self { props, - key, state: UserAgentStateMachine::new(DummyContext), } } @@ -114,7 +110,7 @@ impl UserAgentSession { #[messages] impl UserAgentSession { - // TODO: Think about refactoring it to state-machine based flow, as we already have one + // TODO: Think about refactoring it to state-machine based flow, as we already have one #[message(ctx)] pub async fn request_new_client_approval( &mut self, @@ -123,12 +119,9 @@ impl UserAgentSession { ctx: &mut Context>, ) -> Result { self.send_msg( - UserAgentResponsePayload::ClientConnectionRequest( - ClientConnectionRequest { - pubkey: client_pubkey.as_bytes().to_vec(), - } - .into(), - ), + UserAgentResponsePayload::ClientConnectionRequest(ClientConnectionRequest { + pubkey: client_pubkey.as_bytes().to_vec(), + }), ctx, ) .await?; @@ -150,12 +143,12 @@ impl UserAgentSession { UserAgentResponsePayload::ClientConnectionCancel(ClientConnectionCancel {}), ctx, ).await?; - return Ok(false); + Ok(false) } result = self.expect_msg(extractor, ctx) => { let result = result?; info!(actor = "useragent", "received client connection approval result: approved={}", result.approved); - return Ok(result.approved); + Ok(result.approved) } } } @@ -420,10 +413,8 @@ impl UserAgentSession { use arbiter_proto::transport::DummyTransport; let transport: super::Transport = Box::new(DummyTransport::new()); let props = UserAgentConnection::new(db, actors, transport); - let key = VerifyingKey::from_bytes(&[0u8; 32]).unwrap(); Self { props, - key, state: UserAgentStateMachine::new(DummyContext), } } diff --git a/server/crates/arbiter-server/src/db/mod.rs b/server/crates/arbiter-server/src/db/mod.rs index 69d7539..d8771dc 100644 --- a/server/crates/arbiter-server/src/db/mod.rs +++ b/server/crates/arbiter-server/src/db/mod.rs @@ -135,10 +135,10 @@ pub async fn create_test_pool() -> DatabasePool { let tempfile_name = Alphanumeric.sample_string(&mut rand::rng(), 16); let file = std::env::temp_dir().join(tempfile_name); - let url = format!( - "{}?mode=rwc", - file.to_str().expect("temp file path is not valid UTF-8") - ); + let url = file + .to_str() + .expect("temp file path is not valid UTF-8") + .to_string(); create_pool(Some(&url)) .await diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index 1c01ed6..92c6d85 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -12,8 +12,6 @@ use diesel::{prelude::*, sqlite::Sqlite}; use restructed::Models; pub mod types { - use std::os::unix; - use chrono::{DateTime, Utc}; use diesel::{ deserialize::{FromSql, FromSqlRow}, @@ -74,6 +72,43 @@ pub mod types { Ok(SqliteTimestamp(datetime)) } } + + /// Key algorithm stored in the `useragent_client.key_type` column. + /// Values must stay stable — they are persisted in the database. + #[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression)] + #[diesel(sql_type = Integer)] + #[repr(i32)] + pub enum KeyType { + Ed25519 = 1, + EcdsaSecp256k1 = 2, + Rsa = 3, + } + + impl ToSql for KeyType { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, Sqlite>, + ) -> diesel::serialize::Result { + out.set_value(*self as i32); + Ok(IsNull::No) + } + } + + impl FromSql for KeyType { + fn from_sql( + mut bytes: ::RawValue<'_>, + ) -> diesel::deserialize::Result { + let Some(SqliteType::Long) = bytes.value_type() else { + return Err("Expected Integer for KeyType".into()); + }; + match bytes.read_long() { + 1 => Ok(KeyType::Ed25519), + 2 => Ok(KeyType::EcdsaSecp256k1), + 3 => Ok(KeyType::Rsa), + other => Err(format!("Unknown KeyType discriminant: {other}").into()), + } + } + } } pub use types::*; @@ -171,6 +206,7 @@ pub struct UseragentClient { pub public_key: Vec, pub created_at: SqliteTimestamp, pub updated_at: SqliteTimestamp, + pub key_type: KeyType, } #[derive(Models, Queryable, Debug, Insertable, Selectable)] diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index 24c0777..8d60f4b 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -153,6 +153,7 @@ diesel::table! { public_key -> Binary, created_at -> Integer, updated_at -> Integer, + key_type -> Integer, } } diff --git a/server/crates/arbiter-server/tests/user_agent/auth.rs b/server/crates/arbiter-server/tests/user_agent/auth.rs index 17af990..edcc1d2 100644 --- a/server/crates/arbiter-server/tests/user_agent/auth.rs +++ b/server/crates/arbiter-server/tests/user_agent/auth.rs @@ -1,5 +1,5 @@ use arbiter_proto::proto::user_agent::{ - AuthChallengeRequest, AuthChallengeSolution, UserAgentRequest, + AuthChallengeRequest, AuthChallengeSolution, KeyType as ProtoKeyType, UserAgentRequest, user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, }; @@ -38,6 +38,7 @@ pub async fn test_bootstrap_token_auth() { AuthChallengeRequest { pubkey: pubkey_bytes, bootstrap_token: Some(token), + key_type: ProtoKeyType::Ed25519.into(), }, )), }) @@ -74,6 +75,7 @@ pub async fn test_bootstrap_invalid_token_auth() { AuthChallengeRequest { pubkey: pubkey_bytes, bootstrap_token: Some("invalid_token".to_string()), + key_type: ProtoKeyType::Ed25519.into(), }, )), }) @@ -102,10 +104,14 @@ pub async fn test_challenge_auth() { let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + // Pre-register key with key_type { let mut conn = db.get().await.unwrap(); insert_into(schema::useragent_client::table) - .values(schema::useragent_client::public_key.eq(pubkey_bytes.clone())) + .values(( + schema::useragent_client::public_key.eq(pubkey_bytes.clone()), + schema::useragent_client::key_type.eq(1i32), + )) .execute(&mut conn) .await .unwrap(); @@ -122,6 +128,7 @@ pub async fn test_challenge_auth() { AuthChallengeRequest { pubkey: pubkey_bytes, bootstrap_token: None, + key_type: ProtoKeyType::Ed25519.into(), }, )), }) diff --git a/server/crates/arbiter-useragent/Cargo.toml b/server/crates/arbiter-useragent/Cargo.toml index 8b6b85b..7b08cbf 100644 --- a/server/crates/arbiter-useragent/Cargo.toml +++ b/server/crates/arbiter-useragent/Cargo.toml @@ -14,6 +14,11 @@ tracing.workspace = true ed25519-dalek.workspace = true smlang.workspace = true x25519-dalek.workspace = true +k256.workspace = true +rsa.workspace = true +sha2.workspace = true +spki.workspace = true +rand.workspace = true thiserror.workspace = true tokio-stream.workspace = true http = "1.4.0" diff --git a/server/crates/arbiter-useragent/src/grpc.rs b/server/crates/arbiter-useragent/src/grpc.rs index 593fd88..1c15995 100644 --- a/server/crates/arbiter-useragent/src/grpc.rs +++ b/server/crates/arbiter-useragent/src/grpc.rs @@ -1,12 +1,11 @@ use arbiter_proto::{ proto::{ - user_agent::{UserAgentRequest, UserAgentResponse}, arbiter_service_client::ArbiterServiceClient, + user_agent::{UserAgentRequest, UserAgentResponse}, }, transport::{IdentityRecvConverter, IdentitySendConverter, grpc}, url::ArbiterUrl, }; -use ed25519_dalek::SigningKey; use kameo::actor::{ActorRef, Spawn}; use tokio::sync::mpsc; @@ -14,6 +13,7 @@ use tokio_stream::wrappers::ReceiverStream; use tonic::transport::ClientTlsConfig; +use super::{SigningKeyEnum, UserAgentActor}; #[derive(Debug, thiserror::Error)] pub enum ConnectError { @@ -30,8 +30,6 @@ pub enum ConnectError { Grpc(#[from] tonic::Status), } - use super::UserAgentActor; - pub type UserAgentGrpc = ActorRef< UserAgentActor< grpc::GrpcAdapter< @@ -42,7 +40,7 @@ pub type UserAgentGrpc = ActorRef< >; pub async fn connect_grpc( url: ArbiterUrl, - key: SigningKey, + key: SigningKeyEnum, ) -> Result { let bootstrap_token = url.bootstrap_token.clone(); let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned(); diff --git a/server/crates/arbiter-useragent/src/lib.rs b/server/crates/arbiter-useragent/src/lib.rs index 88f6e8d..b2f3ee3 100644 --- a/server/crates/arbiter-useragent/src/lib.rs +++ b/server/crates/arbiter-useragent/src/lib.rs @@ -1,19 +1,80 @@ use arbiter_proto::{ format_challenge, proto::user_agent::{ - AuthChallengeRequest, AuthChallengeSolution, AuthOk, + AuthChallengeRequest, AuthChallengeSolution, AuthOk, KeyType as ProtoKeyType, UserAgentRequest, UserAgentResponse, user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, }, transport::Bi, }; -use ed25519_dalek::{Signer, SigningKey}; use kameo::{Actor, actor::ActorRef}; use smlang::statemachine; use tokio::select; use tracing::{error, info}; +/// Signing key variants supported by the user-agent auth protocol. +pub enum SigningKeyEnum { + Ed25519(ed25519_dalek::SigningKey), + /// secp256k1 ECDSA; public key is sent as DER SPKI; signature is raw 64-byte (r||s). + EcdsaSecp256k1(k256::ecdsa::SigningKey), + /// RSA; public key is sent as DER SPKI; signature is PSS+SHA-256. + Rsa(rsa::RsaPrivateKey), +} + +impl SigningKeyEnum { + /// Returns the canonical public key bytes to include in `AuthChallengeRequest.pubkey`. + pub fn pubkey_bytes(&self) -> Vec { + match self { + SigningKeyEnum::Ed25519(k) => k.verifying_key().to_bytes().to_vec(), + // 33-byte SEC1 compressed point — compact and natively supported by secp256k1 tooling + SigningKeyEnum::EcdsaSecp256k1(k) => { + k.verifying_key().to_encoded_point(true).as_bytes().to_vec() + } + SigningKeyEnum::Rsa(k) => { + use rsa::pkcs8::EncodePublicKey as _; + k.to_public_key() + .to_public_key_der() + .expect("rsa SPKI encoding is infallible") + .to_vec() + } + } + } + + /// Returns the proto `KeyType` discriminant to send in `AuthChallengeRequest.key_type`. + pub fn proto_key_type(&self) -> ProtoKeyType { + match self { + SigningKeyEnum::Ed25519(_) => ProtoKeyType::Ed25519, + SigningKeyEnum::EcdsaSecp256k1(_) => ProtoKeyType::EcdsaSecp256k1, + SigningKeyEnum::Rsa(_) => ProtoKeyType::Rsa, + } + } + + /// Signs `msg` and returns raw signature bytes matching the server-side verification. + pub fn sign(&self, msg: &[u8]) -> Vec { + match self { + SigningKeyEnum::Ed25519(k) => { + use ed25519_dalek::Signer as _; + k.sign(msg).to_bytes().to_vec() + } + SigningKeyEnum::EcdsaSecp256k1(k) => { + use k256::ecdsa::signature::Signer as _; + let sig: k256::ecdsa::Signature = k.sign(msg); + sig.to_bytes().to_vec() + } + SigningKeyEnum::Rsa(k) => { + use rsa::signature::RandomizedSigner as _; + let signing_key = rsa::pss::BlindedSigningKey::::new(k.clone()); + // Use rand_core OsRng from the rsa crate's re-exported rand_core (0.6.x), + // which is the version rsa's signature API expects. + let sig = signing_key.sign_with_rng(&mut rsa::rand_core::OsRng, msg); + use rsa::signature::SignatureEncoding as _; + sig.to_vec() + } + } + } +} + statemachine! { name: UserAgent, custom_error: false, @@ -50,7 +111,7 @@ pub struct UserAgentActor where Transport: Bi, { - key: SigningKey, + key: SigningKeyEnum, bootstrap_token: Option, state: UserAgentStateMachine, transport: Transport, @@ -60,7 +121,7 @@ impl UserAgentActor where Transport: Bi, { - pub fn new(key: SigningKey, bootstrap_token: Option, transport: Transport) -> Self { + pub fn new(key: SigningKeyEnum, bootstrap_token: Option, transport: Transport) -> Self { Self { key, bootstrap_token, @@ -79,8 +140,9 @@ where async fn send_auth_challenge_request(&mut self) -> Result<(), InboundError> { let req = AuthChallengeRequest { - pubkey: self.key.verifying_key().to_bytes().to_vec(), + pubkey: self.key.pubkey_bytes(), bootstrap_token: self.bootstrap_token.take(), + key_type: self.key.proto_key_type().into(), }; self.transition(UserAgentEvents::SentAuthChallengeRequest)?; @@ -103,9 +165,9 @@ where self.transition(UserAgentEvents::ReceivedAuthChallenge)?; let formatted = format_challenge(challenge.nonce, &challenge.pubkey); - let signature = self.key.sign(&formatted); + let signature_bytes = self.key.sign(&formatted); let solution = AuthChallengeSolution { - signature: signature.to_bytes().to_vec(), + signature: signature_bytes, }; self.transport @@ -127,7 +189,7 @@ where pub async fn process_inbound_transport( &mut self, - inbound: UserAgentResponse + inbound: UserAgentResponse, ) -> Result<(), InboundError> { let payload = inbound .payload @@ -192,4 +254,4 @@ where } mod grpc; -pub use grpc::{connect_grpc, ConnectError}; +pub use grpc::{ConnectError, connect_grpc}; diff --git a/server/crates/arbiter-useragent/tests/auth.rs b/server/crates/arbiter-useragent/tests/auth.rs index 8d79bbe..3b6b35a 100644 --- a/server/crates/arbiter-useragent/tests/auth.rs +++ b/server/crates/arbiter-useragent/tests/auth.rs @@ -1,19 +1,18 @@ use arbiter_proto::{ format_challenge, proto::user_agent::{ - AuthChallenge, AuthOk, - UserAgentRequest, UserAgentResponse, + AuthChallenge, AuthOk, UserAgentRequest, UserAgentResponse, user_agent_request::Payload as UserAgentRequestPayload, user_agent_response::Payload as UserAgentResponsePayload, }, transport::Bi, }; -use arbiter_useragent::UserAgentActor; +use arbiter_useragent::{SigningKeyEnum, UserAgentActor}; +use async_trait::async_trait; use ed25519_dalek::SigningKey; use kameo::actor::Spawn; use tokio::sync::mpsc; use tokio::time::{Duration, timeout}; -use async_trait::async_trait; struct TestTransport { inbound_rx: mpsc::Receiver, @@ -22,7 +21,10 @@ struct TestTransport { #[async_trait] impl Bi for TestTransport { - async fn send(&mut self, item: UserAgentRequest) -> Result<(), arbiter_proto::transport::Error> { + async fn send( + &mut self, + item: UserAgentRequest, + ) -> Result<(), arbiter_proto::transport::Error> { self.outbound_tx .send(item) .await @@ -51,14 +53,14 @@ fn make_transport() -> ( ) } -fn test_key() -> SigningKey { - SigningKey::from_bytes(&[7u8; 32]) +fn test_key() -> SigningKeyEnum { + SigningKeyEnum::Ed25519(SigningKey::from_bytes(&[7u8; 32])) } #[tokio::test] async fn sends_auth_request_on_start_with_bootstrap_token() { let key = test_key(); - let pubkey = key.verifying_key().to_bytes().to_vec(); + let pubkey = key.pubkey_bytes(); let bootstrap_token = Some("bootstrap-123".to_string()); let (transport, inbound_tx, mut outbound_rx) = make_transport(); @@ -86,7 +88,7 @@ async fn sends_auth_request_on_start_with_bootstrap_token() { #[tokio::test] async fn challenge_flow_sends_solution_from_transport_inbound() { let key = test_key(); - let verify_key = key.verifying_key(); + let pubkey_bytes = key.pubkey_bytes(); let (transport, inbound_tx, mut outbound_rx) = make_transport(); let actor = UserAgentActor::spawn(UserAgentActor::new(key, None, transport)); @@ -97,7 +99,7 @@ async fn challenge_flow_sends_solution_from_transport_inbound() { .expect("missing initial auth request"); let challenge = AuthChallenge { - pubkey: verify_key.to_bytes().to_vec(), + pubkey: pubkey_bytes.clone(), nonce: 42, }; inbound_tx @@ -119,13 +121,16 @@ async fn challenge_flow_sends_solution_from_transport_inbound() { panic!("expected auth challenge solution"); }; + // Verify the signature using the Ed25519 verifying key let formatted = format_challenge(challenge.nonce, &challenge.pubkey); + let raw_key = SigningKey::from_bytes(&[7u8; 32]); let sig: ed25519_dalek::Signature = solution .signature .as_slice() .try_into() .expect("signature bytes length"); - verify_key + raw_key + .verifying_key() .verify_strict(&formatted, &sig) .expect("solution signature should verify"); From f8d27a14547b2e50de0f1003cb8924f2c71ac6a7 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 14 Mar 2026 12:40:37 +0100 Subject: [PATCH 2/7] refactor(config): specify target for Windows in profile.dev settings --- server/.cargo/config.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/.cargo/config.toml b/server/.cargo/config.toml index 8390ed5..2f90489 100644 --- a/server/.cargo/config.toml +++ b/server/.cargo/config.toml @@ -1,4 +1,5 @@ -[profile.dev] -# Override global Cranelift backend: Cranelift does not propagate cargo:rustc-link-lib -# from native dependencies (aws-lc-sys etc.) to lld-link, causing undefined symbol errors. +[target.'cfg(windows)'.profile.dev] +# Override global Cranelift backend only on Windows. +# Cranelift does not propagate cargo:rustc-link-lib from native dependencies +# (aws-lc-sys etc.) to lld-link, causing undefined symbol errors. codegen-backend = "llvm" From d29bca853b83abfc94e0861c4a40f555bde97070 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 14 Mar 2026 13:22:47 +0100 Subject: [PATCH 3/7] chore: squash migrations --- .../migrations/2026-02-14-171124-0000_init/up.sql | 1 + .../migrations/2026-03-13-000000_add_key_type/down.sql | 2 -- .../migrations/2026-03-13-000000_add_key_type/up.sql | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/down.sql delete mode 100644 server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/up.sql diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index 8b942d7..3419ef9 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -46,6 +46,7 @@ create table if not exists useragent_client ( id integer not null primary key, nonce integer not null default(1), -- used for auth challenge public_key blob not null, + key_type integer not null default(1), -- 1=Ed25519, 2=ECDSA(secp256k1) created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) ) STRICT; diff --git a/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/down.sql b/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/down.sql deleted file mode 100644 index 41b541c..0000000 --- a/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Not reversible without data loss; drop the column to revert -ALTER TABLE useragent_client DROP COLUMN key_type; diff --git a/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/up.sql b/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/up.sql deleted file mode 100644 index 1d494f0..0000000 --- a/server/crates/arbiter-server/migrations/2026-03-13-000000_add_key_type/up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE useragent_client ADD COLUMN key_type INTEGER NOT NULL DEFAULT 1; From 42760bbd79187f491799d4bf52724a3ac4a5aa20 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 14 Mar 2026 13:23:06 +0100 Subject: [PATCH 4/7] revert(auth): remove RSA support from authentication and related components --- protobufs/user_agent.proto | 1 - server/Cargo.lock | 84 ------------------- server/Cargo.toml | 3 - server/crates/arbiter-proto/src/transport.rs | 1 - server/crates/arbiter-proto/src/url.rs | 2 +- server/crates/arbiter-server/Cargo.toml | 3 - .../src/actors/user_agent/auth.rs | 13 ++- .../src/actors/user_agent/auth/state.rs | 29 +------ server/crates/arbiter-server/src/db/models.rs | 2 - server/crates/arbiter-useragent/Cargo.toml | 3 - server/crates/arbiter-useragent/src/lib.rs | 21 +---- 11 files changed, 9 insertions(+), 153 deletions(-) diff --git a/protobufs/user_agent.proto b/protobufs/user_agent.proto index fcf508d..55b18fc 100644 --- a/protobufs/user_agent.proto +++ b/protobufs/user_agent.proto @@ -9,7 +9,6 @@ enum KeyType { KEY_TYPE_UNSPECIFIED = 0; KEY_TYPE_ED25519 = 1; KEY_TYPE_ECDSA_SECP256K1 = 2; - KEY_TYPE_RSA = 3; } message AuthChallengeRequest { diff --git a/server/Cargo.lock b/server/Cargo.lock index 1586320..9645220 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -731,12 +731,9 @@ dependencies = [ "rand 0.10.0", "rcgen", "restructed", - "rsa", "rustls", "secrecy", - "sha2 0.10.9", "smlang", - "spki", "strum", "test-log", "thiserror", @@ -767,11 +764,8 @@ dependencies = [ "k256", "kameo", "rand 0.10.0", - "rsa", "rustls-webpki", - "sha2 0.10.9", "smlang", - "spki", "thiserror", "tokio", "tokio-stream", @@ -1739,7 +1733,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", - "pem-rfc7468", "zeroize", ] @@ -2920,9 +2913,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "leb128fmt" @@ -3156,22 +3146,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - [[package]] name = "num-conv" version = "0.2.0" @@ -3187,17 +3161,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -3367,15 +3330,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -3435,17 +3389,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - [[package]] name = "pkcs8" version = "0.10.2" @@ -4093,27 +4036,6 @@ dependencies = [ "rustc-hex", ] -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "sha2 0.10.9", - "signature 2.2.0", - "spki", - "subtle", - "zeroize", -] - [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -4661,12 +4583,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "spki" version = "0.7.3" diff --git a/server/Cargo.toml b/server/Cargo.toml index 03ffbbe..bbb9eb9 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -37,6 +37,3 @@ rcgen = { version = "0.14.7", features = [ "zeroize", ], default-features = false } k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] } -rsa = { version = "0.9", features = ["sha2"] } -sha2 = "0.10" -spki = "0.7" diff --git a/server/crates/arbiter-proto/src/transport.rs b/server/crates/arbiter-proto/src/transport.rs index f5acaf9..6f89b80 100644 --- a/server/crates/arbiter-proto/src/transport.rs +++ b/server/crates/arbiter-proto/src/transport.rs @@ -203,7 +203,6 @@ pub mod grpc { /// [`Bi`] adapter backed by a tonic gRPC bidirectional stream. /// - /// Tonic receive errors are logged and treated as stream closure (`None`). /// The receive converter is only invoked for successful inbound transport /// items. diff --git a/server/crates/arbiter-proto/src/url.rs b/server/crates/arbiter-proto/src/url.rs index adfd45d..c961680 100644 --- a/server/crates/arbiter-proto/src/url.rs +++ b/server/crates/arbiter-proto/src/url.rs @@ -20,7 +20,7 @@ impl Display for ArbiterUrl { "{ARBITER_URL_SCHEME}://{}:{}?{CERT_QUERY_KEY}={}", self.host, self.port, - BASE64_URL_SAFE.encode(self.ca_cert.to_vec()) + BASE64_URL_SAFE.encode(&self.ca_cert) ); if let Some(token) = &self.bootstrap_token { base.push_str(&format!("&{BOOTSTRAP_TOKEN_QUERY_KEY}={}", token)); diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index 4fcb05e..b93c02e 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -43,9 +43,6 @@ restructed = "0.2.2" strum = { version = "0.27.2", features = ["derive"] } pem = "3.0.6" k256.workspace = true -rsa.workspace = true -sha2.workspace = true -spki.workspace = true alloy.workspace = true arbiter-tokens-registry.path = "../arbiter-tokens-registry" diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth.rs b/server/crates/arbiter-server/src/actors/user_agent/auth.rs index 3da8291..de5f20f 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth.rs @@ -6,7 +6,8 @@ use tracing::error; use crate::actors::user_agent::{ UserAgentConnection, - auth::state::{AuthContext, AuthPublicKey, AuthStateMachine}, session::UserAgentSession, + auth::state::{AuthContext, AuthPublicKey, AuthStateMachine}, + session::UserAgentSession, }; #[derive(thiserror::Error, Debug, PartialEq)] @@ -51,12 +52,6 @@ fn parse_pubkey(key_type: ProtoKeyType, pubkey: Vec) -> Result { - use rsa::pkcs8::DecodePublicKey as _; - let key = rsa::RsaPublicKey::from_public_key_der(&pubkey) - .map_err(|_| Error::InvalidAuthPubkeyEncoding)?; - Ok(AuthPublicKey::Rsa(key)) - } } } @@ -131,7 +126,9 @@ pub async fn authenticate(props: &mut UserAgentConnection) -> Result Result { +pub async fn authenticate_and_create( + mut props: UserAgentConnection, +) -> Result { let _key = authenticate(&mut props).await?; let session = UserAgentSession::new(props); Ok(session) diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs index 2acd45b..04e6d57 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -11,30 +11,22 @@ use crate::{ db::{models::KeyType, schema}, }; -/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake. +/// Abstraction over Ed25519 / ECDSA-secp256k1 public keys used during the auth handshake. #[derive(Clone)] pub enum AuthPublicKey { Ed25519(ed25519_dalek::VerifyingKey), /// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s). EcdsaSecp256k1(k256::ecdsa::VerifyingKey), - /// RSA-2048+ public key; signature bytes are PSS+SHA-256. - Rsa(rsa::RsaPublicKey), } impl AuthPublicKey { /// Canonical bytes stored in DB and echoed back in the challenge. - /// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI. + /// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. pub fn to_stored_bytes(&self) -> Vec { match self { AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(), // SEC1 compressed (33 bytes) is the natural compact format for secp256k1 AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(), - AuthPublicKey::Rsa(k) => { - use rsa::pkcs8::EncodePublicKey as _; - k.to_public_key_der() - .expect("rsa SPKI encoding is infallible") - .to_vec() - } } } @@ -42,7 +34,6 @@ impl AuthPublicKey { match self { AuthPublicKey::Ed25519(_) => KeyType::Ed25519, AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1, - AuthPublicKey::Rsa(_) => KeyType::Rsa, } } } @@ -170,15 +161,6 @@ impl AuthStateMachineContext for AuthContext<'_> { })?; vk.verify(&formatted, &sig).is_ok() } - AuthPublicKey::Rsa(pk) => { - use rsa::signature::Verifier as _; - let verifying_key = rsa::pss::VerifyingKey::::new(pk.clone()); - let sig = rsa::pss::Signature::try_from(solution.as_slice()).map_err(|_| { - error!(?solution, "Invalid RSA signature bytes"); - Error::InvalidChallengeSolution - })?; - verifying_key.verify(&formatted, &sig).is_ok() - } }; Ok(valid) @@ -284,13 +266,6 @@ impl AuthStateMachineContext for AuthContext<'_> { .expect("ecdsa key was already validated in parse_auth_event"), ) } - crate::db::models::KeyType::Rsa => { - use rsa::pkcs8::DecodePublicKey as _; - AuthPublicKey::Rsa( - rsa::RsaPublicKey::from_public_key_der(&bytes) - .expect("rsa key was already validated in parse_auth_event"), - ) - } }; Ok(rebuilt) } diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index 92c6d85..3f0de44 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -81,7 +81,6 @@ pub mod types { pub enum KeyType { Ed25519 = 1, EcdsaSecp256k1 = 2, - Rsa = 3, } impl ToSql for KeyType { @@ -104,7 +103,6 @@ pub mod types { match bytes.read_long() { 1 => Ok(KeyType::Ed25519), 2 => Ok(KeyType::EcdsaSecp256k1), - 3 => Ok(KeyType::Rsa), other => Err(format!("Unknown KeyType discriminant: {other}").into()), } } diff --git a/server/crates/arbiter-useragent/Cargo.toml b/server/crates/arbiter-useragent/Cargo.toml index 7b08cbf..a2042eb 100644 --- a/server/crates/arbiter-useragent/Cargo.toml +++ b/server/crates/arbiter-useragent/Cargo.toml @@ -15,9 +15,6 @@ ed25519-dalek.workspace = true smlang.workspace = true x25519-dalek.workspace = true k256.workspace = true -rsa.workspace = true -sha2.workspace = true -spki.workspace = true rand.workspace = true thiserror.workspace = true tokio-stream.workspace = true diff --git a/server/crates/arbiter-useragent/src/lib.rs b/server/crates/arbiter-useragent/src/lib.rs index b2f3ee3..1be9f3a 100644 --- a/server/crates/arbiter-useragent/src/lib.rs +++ b/server/crates/arbiter-useragent/src/lib.rs @@ -16,10 +16,8 @@ use tracing::{error, info}; /// Signing key variants supported by the user-agent auth protocol. pub enum SigningKeyEnum { Ed25519(ed25519_dalek::SigningKey), - /// secp256k1 ECDSA; public key is sent as DER SPKI; signature is raw 64-byte (r||s). + /// secp256k1 ECDSA; public key is sent as SEC1 compressed 33 bytes; signature is raw 64-byte (r||s). EcdsaSecp256k1(k256::ecdsa::SigningKey), - /// RSA; public key is sent as DER SPKI; signature is PSS+SHA-256. - Rsa(rsa::RsaPrivateKey), } impl SigningKeyEnum { @@ -31,13 +29,6 @@ impl SigningKeyEnum { SigningKeyEnum::EcdsaSecp256k1(k) => { k.verifying_key().to_encoded_point(true).as_bytes().to_vec() } - SigningKeyEnum::Rsa(k) => { - use rsa::pkcs8::EncodePublicKey as _; - k.to_public_key() - .to_public_key_der() - .expect("rsa SPKI encoding is infallible") - .to_vec() - } } } @@ -46,7 +37,6 @@ impl SigningKeyEnum { match self { SigningKeyEnum::Ed25519(_) => ProtoKeyType::Ed25519, SigningKeyEnum::EcdsaSecp256k1(_) => ProtoKeyType::EcdsaSecp256k1, - SigningKeyEnum::Rsa(_) => ProtoKeyType::Rsa, } } @@ -62,15 +52,6 @@ impl SigningKeyEnum { let sig: k256::ecdsa::Signature = k.sign(msg); sig.to_bytes().to_vec() } - SigningKeyEnum::Rsa(k) => { - use rsa::signature::RandomizedSigner as _; - let signing_key = rsa::pss::BlindedSigningKey::::new(k.clone()); - // Use rand_core OsRng from the rsa crate's re-exported rand_core (0.6.x), - // which is the version rsa's signature API expects. - let sig = signing_key.sign_with_rng(&mut rsa::rand_core::OsRng, msg); - use rsa::signature::SignatureEncoding as _; - sig.to_vec() - } } } } From 47144bdf8138ee785221bde8117cb2a6e4341598 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 14 Mar 2026 13:57:13 +0100 Subject: [PATCH 5/7] feat(auth): limited RSA support for signing see server/clippy.toml --- protobufs/user_agent.proto | 1 + server/.cargo/audit.toml | 13 +++ server/.cargo/config.toml | 5 +- server/Cargo.lock | 84 +++++++++++++++++++ server/Cargo.toml | 6 ++ server/clippy.toml | 8 ++ server/crates/arbiter-server/Cargo.toml | 6 ++ .../src/actors/user_agent/auth.rs | 6 ++ .../src/actors/user_agent/auth/state.rs | 29 ++++++- server/crates/arbiter-server/src/db/models.rs | 17 ++-- server/crates/arbiter-server/src/lib.rs | 11 ++- server/crates/arbiter-useragent/Cargo.toml | 6 ++ server/crates/arbiter-useragent/src/lib.rs | 19 +++++ 13 files changed, 192 insertions(+), 19 deletions(-) create mode 100644 server/.cargo/audit.toml create mode 100644 server/clippy.toml diff --git a/protobufs/user_agent.proto b/protobufs/user_agent.proto index 55b18fc..fcf508d 100644 --- a/protobufs/user_agent.proto +++ b/protobufs/user_agent.proto @@ -9,6 +9,7 @@ enum KeyType { KEY_TYPE_UNSPECIFIED = 0; KEY_TYPE_ED25519 = 1; KEY_TYPE_ECDSA_SECP256K1 = 2; + KEY_TYPE_RSA = 3; } message AuthChallengeRequest { diff --git a/server/.cargo/audit.toml b/server/.cargo/audit.toml new file mode 100644 index 0000000..a615271 --- /dev/null +++ b/server/.cargo/audit.toml @@ -0,0 +1,13 @@ +[advisories] +# RUSTSEC-2023-0071: Marvin Attack timing side-channel in rsa crate. +# No fixed version is available upstream. +# RSA support is required for Windows Hello / KeyCredentialManager +# (https://learn.microsoft.com/en-us/uwp/api/windows.security.credentials.keycredentialmanager.requestcreateasync), +# which only issues RSA-2048 keys. +# Mitigations in place: +# - Signing uses BlindedSigningKey (PSS+SHA-256), which applies blinding to +# protect the private key from timing recovery during signing. +# - RSA decryption is never performed; we only verify public-key signatures. +# - The attack requires local, high-resolution timing access against the +# signing process, which is not exposed in our threat model. +ignore = ["RUSTSEC-2023-0071"] diff --git a/server/.cargo/config.toml b/server/.cargo/config.toml index 2f90489..47f62d0 100644 --- a/server/.cargo/config.toml +++ b/server/.cargo/config.toml @@ -1,5 +1,2 @@ -[target.'cfg(windows)'.profile.dev] -# Override global Cranelift backend only on Windows. -# Cranelift does not propagate cargo:rustc-link-lib from native dependencies -# (aws-lc-sys etc.) to lld-link, causing undefined symbol errors. +[profile.dev] codegen-backend = "llvm" diff --git a/server/Cargo.lock b/server/Cargo.lock index 9645220..1586320 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -731,9 +731,12 @@ dependencies = [ "rand 0.10.0", "rcgen", "restructed", + "rsa", "rustls", "secrecy", + "sha2 0.10.9", "smlang", + "spki", "strum", "test-log", "thiserror", @@ -764,8 +767,11 @@ dependencies = [ "k256", "kameo", "rand 0.10.0", + "rsa", "rustls-webpki", + "sha2 0.10.9", "smlang", + "spki", "thiserror", "tokio", "tokio-stream", @@ -1733,6 +1739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -2913,6 +2920,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -3146,6 +3156,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -3161,6 +3187,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3330,6 +3367,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3389,6 +3435,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -4036,6 +4093,27 @@ dependencies = [ "rustc-hex", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature 2.2.0", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rsqlite-vfs" version = "0.1.0" @@ -4583,6 +4661,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.3" diff --git a/server/Cargo.toml b/server/Cargo.toml index bbb9eb9..ea3da11 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -4,6 +4,9 @@ members = [ ] resolver = "3" +[workspace.lints.clippy] +disallowed-methods = "deny" + [workspace.dependencies] tonic = { version = "0.14.3", features = [ @@ -37,3 +40,6 @@ rcgen = { version = "0.14.7", features = [ "zeroize", ], default-features = false } k256 = { version = "0.13.4", features = ["ecdsa", "pkcs8"] } +rsa = { version = "0.9", features = ["sha2"] } +sha2 = "0.10" +spki = "0.7" diff --git a/server/clippy.toml b/server/clippy.toml new file mode 100644 index 0000000..09149fd --- /dev/null +++ b/server/clippy.toml @@ -0,0 +1,8 @@ +disallowed-methods = [ + # RSA decryption is forbidden: the rsa crate has RUSTSEC-2023-0071 (Marvin Attack). + # We only use RSA for Windows Hello (KeyCredentialManager) public-key verification — decryption + # is never required and must not be introduced. + { path = "rsa::RsaPrivateKey::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." }, + { path = "rsa::pkcs1v15::DecryptingKey::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." }, + { path = "rsa::oaep::DecryptingKey::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." }, +] diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index b93c02e..4629d81 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -5,6 +5,9 @@ edition = "2024" repository = "https://git.markettakers.org/MarketTakers/arbiter" license = "Apache-2.0" +[lints] +workspace = true + [dependencies] diesel = { version = "2.3.6", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] } diesel-async = { version = "0.7.4", features = [ @@ -43,6 +46,9 @@ restructed = "0.2.2" strum = { version = "0.27.2", features = ["derive"] } pem = "3.0.6" k256.workspace = true +rsa.workspace = true +sha2.workspace = true +spki.workspace = true alloy.workspace = true arbiter-tokens-registry.path = "../arbiter-tokens-registry" diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth.rs b/server/crates/arbiter-server/src/actors/user_agent/auth.rs index de5f20f..1e0fe20 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth.rs @@ -52,6 +52,12 @@ fn parse_pubkey(key_type: ProtoKeyType, pubkey: Vec) -> Result { + use rsa::pkcs8::DecodePublicKey as _; + let key = rsa::RsaPublicKey::from_public_key_der(&pubkey) + .map_err(|_| Error::InvalidAuthPubkeyEncoding)?; + Ok(AuthPublicKey::Rsa(key)) + } } } diff --git a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs index 04e6d57..13bf6c6 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/auth/state.rs @@ -11,22 +11,30 @@ use crate::{ db::{models::KeyType, schema}, }; -/// Abstraction over Ed25519 / ECDSA-secp256k1 public keys used during the auth handshake. +/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake. #[derive(Clone)] pub enum AuthPublicKey { Ed25519(ed25519_dalek::VerifyingKey), /// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s). EcdsaSecp256k1(k256::ecdsa::VerifyingKey), + /// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256. + Rsa(rsa::RsaPublicKey), } impl AuthPublicKey { /// Canonical bytes stored in DB and echoed back in the challenge. - /// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. + /// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI. pub fn to_stored_bytes(&self) -> Vec { match self { AuthPublicKey::Ed25519(k) => k.to_bytes().to_vec(), // SEC1 compressed (33 bytes) is the natural compact format for secp256k1 AuthPublicKey::EcdsaSecp256k1(k) => k.to_encoded_point(true).as_bytes().to_vec(), + AuthPublicKey::Rsa(k) => { + use rsa::pkcs8::EncodePublicKey as _; + k.to_public_key_der() + .expect("rsa SPKI encoding is infallible") + .to_vec() + } } } @@ -34,6 +42,7 @@ impl AuthPublicKey { match self { AuthPublicKey::Ed25519(_) => KeyType::Ed25519, AuthPublicKey::EcdsaSecp256k1(_) => KeyType::EcdsaSecp256k1, + AuthPublicKey::Rsa(_) => KeyType::Rsa, } } } @@ -161,6 +170,15 @@ impl AuthStateMachineContext for AuthContext<'_> { })?; vk.verify(&formatted, &sig).is_ok() } + AuthPublicKey::Rsa(pk) => { + use rsa::signature::Verifier as _; + let verifying_key = rsa::pss::VerifyingKey::::new(pk.clone()); + let sig = rsa::pss::Signature::try_from(solution.as_slice()).map_err(|_| { + error!(?solution, "Invalid RSA signature bytes"); + Error::InvalidChallengeSolution + })?; + verifying_key.verify(&formatted, &sig).is_ok() + } }; Ok(valid) @@ -266,6 +284,13 @@ impl AuthStateMachineContext for AuthContext<'_> { .expect("ecdsa key was already validated in parse_auth_event"), ) } + crate::db::models::KeyType::Rsa => { + use rsa::pkcs8::DecodePublicKey as _; + AuthPublicKey::Rsa( + rsa::RsaPublicKey::from_public_key_der(&bytes) + .expect("rsa key was already validated in parse_auth_event"), + ) + } }; Ok(rebuilt) } diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index 3f0de44..ddf7773 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -36,9 +36,9 @@ pub mod types { SqliteTimestamp(dt) } } - impl Into> for SqliteTimestamp { - fn into(self) -> chrono::DateTime { - self.0 + impl From for chrono::DateTime { + fn from(ts: SqliteTimestamp) -> Self { + ts.0 } } @@ -75,12 +75,13 @@ pub mod types { /// Key algorithm stored in the `useragent_client.key_type` column. /// Values must stay stable — they are persisted in the database. - #[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, FromSqlRow, AsExpression, strum::FromRepr)] #[diesel(sql_type = Integer)] #[repr(i32)] pub enum KeyType { Ed25519 = 1, EcdsaSecp256k1 = 2, + Rsa = 3, } impl ToSql for KeyType { @@ -100,11 +101,9 @@ pub mod types { let Some(SqliteType::Long) = bytes.value_type() else { return Err("Expected Integer for KeyType".into()); }; - match bytes.read_long() { - 1 => Ok(KeyType::Ed25519), - 2 => Ok(KeyType::EcdsaSecp256k1), - other => Err(format!("Unknown KeyType discriminant: {other}").into()), - } + let discriminant = bytes.read_long(); + KeyType::from_repr(discriminant as i32) + .ok_or_else(|| format!("Unknown KeyType discriminant: {discriminant}").into()) } } } diff --git a/server/crates/arbiter-server/src/lib.rs b/server/crates/arbiter-server/src/lib.rs index c996035..d712992 100644 --- a/server/crates/arbiter-server/src/lib.rs +++ b/server/crates/arbiter-server/src/lib.rs @@ -15,8 +15,8 @@ use tracing::info; use crate::{ actors::{ - client::{self, ClientError, ClientConnection as ClientConnectionProps, connect_client}, - user_agent::{self, UserAgentConnection, TransportResponseError, connect_user_agent}, + client::{self, ClientConnection as ClientConnectionProps, ClientError, connect_client}, + user_agent::{self, TransportResponseError, UserAgentConnection, connect_user_agent}, }, context::ServerContext, }; @@ -89,7 +89,8 @@ fn client_auth_error_status(value: &client::auth::Error) -> Status { fn user_agent_error_status(value: TransportResponseError) -> Status { match value { - TransportResponseError::MissingRequestPayload | TransportResponseError::UnexpectedRequestPayload => { + TransportResponseError::MissingRequestPayload + | TransportResponseError::UnexpectedRequestPayload => { Status::invalid_argument("Expected message with payload") } TransportResponseError::InvalidStateForUnsealEncryptedKey => { @@ -99,7 +100,9 @@ fn user_agent_error_status(value: TransportResponseError) -> Status { Status::invalid_argument("client_pubkey must be 32 bytes") } TransportResponseError::StateTransitionFailed => Status::internal("State machine error"), - TransportResponseError::KeyHolderActorUnreachable => Status::internal("Vault is not available"), + TransportResponseError::KeyHolderActorUnreachable => { + Status::internal("Vault is not available") + } TransportResponseError::Auth(ref err) => auth_error_status(err), TransportResponseError::ConnectionRegistrationFailed => { Status::internal("Failed registering connection") diff --git a/server/crates/arbiter-useragent/Cargo.toml b/server/crates/arbiter-useragent/Cargo.toml index a2042eb..4b7337a 100644 --- a/server/crates/arbiter-useragent/Cargo.toml +++ b/server/crates/arbiter-useragent/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2024" license = "Apache-2.0" +[lints] +workspace = true + [dependencies] arbiter-proto.path = "../arbiter-proto" kameo.workspace = true @@ -15,6 +18,9 @@ ed25519-dalek.workspace = true smlang.workspace = true x25519-dalek.workspace = true k256.workspace = true +rsa.workspace = true +sha2.workspace = true +spki.workspace = true rand.workspace = true thiserror.workspace = true tokio-stream.workspace = true diff --git a/server/crates/arbiter-useragent/src/lib.rs b/server/crates/arbiter-useragent/src/lib.rs index 1be9f3a..5da9d68 100644 --- a/server/crates/arbiter-useragent/src/lib.rs +++ b/server/crates/arbiter-useragent/src/lib.rs @@ -18,6 +18,8 @@ pub enum SigningKeyEnum { Ed25519(ed25519_dalek::SigningKey), /// secp256k1 ECDSA; public key is sent as SEC1 compressed 33 bytes; signature is raw 64-byte (r||s). EcdsaSecp256k1(k256::ecdsa::SigningKey), + /// RSA for Windows Hello (KeyCredentialManager); public key is DER SPKI; signature is PSS+SHA-256. + Rsa(rsa::RsaPrivateKey), } impl SigningKeyEnum { @@ -29,6 +31,13 @@ impl SigningKeyEnum { SigningKeyEnum::EcdsaSecp256k1(k) => { k.verifying_key().to_encoded_point(true).as_bytes().to_vec() } + SigningKeyEnum::Rsa(k) => { + use rsa::pkcs8::EncodePublicKey as _; + k.to_public_key() + .to_public_key_der() + .expect("rsa SPKI encoding is infallible") + .to_vec() + } } } @@ -37,6 +46,7 @@ impl SigningKeyEnum { match self { SigningKeyEnum::Ed25519(_) => ProtoKeyType::Ed25519, SigningKeyEnum::EcdsaSecp256k1(_) => ProtoKeyType::EcdsaSecp256k1, + SigningKeyEnum::Rsa(_) => ProtoKeyType::Rsa, } } @@ -52,6 +62,15 @@ impl SigningKeyEnum { let sig: k256::ecdsa::Signature = k.sign(msg); sig.to_bytes().to_vec() } + SigningKeyEnum::Rsa(k) => { + use rsa::signature::RandomizedSigner as _; + let signing_key = rsa::pss::BlindedSigningKey::::new(k.clone()); + // Use rand_core OsRng from the rsa crate's re-exported rand_core (0.6.x), + // which is the version rsa's signature API expects. + let sig = signing_key.sign_with_rng(&mut rsa::rand_core::OsRng, msg); + use rsa::signature::SignatureEncoding as _; + sig.to_vec() + } } } } From c1c5d1413321f9f415bb14b01343270e47ce78e1 Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 14 Mar 2026 14:13:15 +0100 Subject: [PATCH 6/7] fix(rustc): config toolchaing mismatch --- .gitignore | 3 ++- server/.cargo/config.toml | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 server/.cargo/config.toml diff --git a/.gitignore b/.gitignore index 3fa4465..ae3beec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target/ scripts/__pycache__/ -.DS_Store \ No newline at end of file +.DS_Store +.cargo/config.toml \ No newline at end of file diff --git a/server/.cargo/config.toml b/server/.cargo/config.toml deleted file mode 100644 index 47f62d0..0000000 --- a/server/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[profile.dev] -codegen-backend = "llvm" From 17ac195c5d0bf7ac2ba33a46aed8fb4752b32b3f Mon Sep 17 00:00:00 2001 From: CleverWild Date: Sat, 14 Mar 2026 14:30:23 +0100 Subject: [PATCH 7/7] clippy: fix --- server/clippy.toml | 4 +- .../arbiter-server/src/actors/client/auth.rs | 41 +++++++++++-------- .../src/actors/client/session.rs | 14 +++---- .../arbiter-server/src/actors/evm/mod.rs | 2 +- .../arbiter-server/src/actors/router/mod.rs | 5 +-- server/crates/arbiter-server/src/evm/mod.rs | 9 ++-- .../src/evm/policies/ether_transfer/mod.rs | 19 ++++----- .../src/evm/policies/token_transfers/mod.rs | 20 ++++----- .../arbiter-server/src/evm/safe_signer.rs | 14 +++---- .../crates/arbiter-server/tests/common/mod.rs | 6 +-- 10 files changed, 64 insertions(+), 70 deletions(-) diff --git a/server/clippy.toml b/server/clippy.toml index 09149fd..4ab45d6 100644 --- a/server/clippy.toml +++ b/server/clippy.toml @@ -3,6 +3,6 @@ disallowed-methods = [ # We only use RSA for Windows Hello (KeyCredentialManager) public-key verification — decryption # is never required and must not be introduced. { path = "rsa::RsaPrivateKey::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." }, - { path = "rsa::pkcs1v15::DecryptingKey::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." }, - { path = "rsa::oaep::DecryptingKey::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted." }, + { path = "rsa::pkcs1v15::DecryptingKey::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted.", allow-invalid = true }, + { path = "rsa::oaep::DecryptingKey::decrypt", reason = "RSA decryption is forbidden (RUSTSEC-2023-0071 Marvin Attack). Only PSS signing/verification is permitted.", allow-invalid = true }, ] diff --git a/server/crates/arbiter-server/src/actors/client/auth.rs b/server/crates/arbiter-server/src/actors/client/auth.rs index 3f9c7a8..cb11d9a 100644 --- a/server/crates/arbiter-server/src/actors/client/auth.rs +++ b/server/crates/arbiter-server/src/actors/client/auth.rs @@ -17,7 +17,10 @@ use kameo::error::SendError; use tracing::error; use crate::{ - actors::{client::ClientConnection, router::{self, RequestClientApproval}}, + actors::{ + client::ClientConnection, + router::{self, RequestClientApproval}, + }, db::{self, schema::program_client}, }; @@ -100,7 +103,9 @@ async fn approve_new_client( ) -> Result<(), Error> { let result = actors .router - .ask(RequestClientApproval { client_pubkey: pubkey }) + .ask(RequestClientApproval { + client_pubkey: pubkey, + }) .await; match result { @@ -166,18 +171,18 @@ async fn challenge_client( Error::Transport })?; - let AuthChallengeSolution { signature } = expect_message( - &mut *props.transport, - |req: ClientRequest| match req.payload? { - ClientRequestPayload::AuthChallengeSolution(s) => Some(s), - _ => None, - }, - ) - .await - .map_err(|e| { - error!(error = ?e, "Failed to receive challenge solution"); - Error::Transport - })?; + let AuthChallengeSolution { signature } = + expect_message(&mut *props.transport, |req: ClientRequest| { + match req.payload? { + ClientRequestPayload::AuthChallengeSolution(s) => Some(s), + _ => None, + } + }) + .await + .map_err(|e| { + error!(error = ?e, "Failed to receive challenge solution"); + Error::Transport + })?; let formatted = format_challenge(nonce, &challenge.pubkey); let sig = signature.as_slice().try_into().map_err(|_| { @@ -196,9 +201,9 @@ async fn challenge_client( fn connect_error_code(err: &Error) -> ConnectErrorCode { match err { Error::ApproveError(ApproveError::Denied) => ConnectErrorCode::ApprovalDenied, - Error::ApproveError(ApproveError::Upstream(router::ApprovalError::NoUserAgentsConnected)) => { - ConnectErrorCode::NoUserAgentsOnline - } + Error::ApproveError(ApproveError::Upstream( + router::ApprovalError::NoUserAgentsConnected, + )) => ConnectErrorCode::NoUserAgentsOnline, _ => ConnectErrorCode::Unknown, } } @@ -234,7 +239,7 @@ async fn authenticate(props: &mut ClientConnection) -> Result Result { match authenticate(&mut props).await { - Ok(pubkey) => Ok(ClientSession::new(props, pubkey)), + Ok(_pubkey) => Ok(ClientSession::new(props)), Err(err) => { let code = connect_error_code(&err); let _ = props diff --git a/server/crates/arbiter-server/src/actors/client/session.rs b/server/crates/arbiter-server/src/actors/client/session.rs index a0d21ca..a2ae4a4 100644 --- a/server/crates/arbiter-server/src/actors/client/session.rs +++ b/server/crates/arbiter-server/src/actors/client/session.rs @@ -1,5 +1,4 @@ use arbiter_proto::proto::client::{ClientRequest, ClientResponse}; -use ed25519_dalek::VerifyingKey; use kameo::Actor; use tokio::select; use tracing::{error, info}; @@ -10,12 +9,11 @@ use crate::{actors::{ pub struct ClientSession { props: ClientConnection, - key: VerifyingKey, } impl ClientSession { - pub(crate) fn new(props: ClientConnection, key: VerifyingKey) -> Self { - Self { props, key } + pub(crate) fn new(props: ClientConnection) -> Self { + Self { props } } pub async fn process_transport_inbound(&mut self, req: ClientRequest) -> Output { @@ -24,9 +22,8 @@ impl ClientSession { ClientError::MissingRequestPayload })?; - match msg { - _ => Err(ClientError::UnexpectedRequestPayload), - } + let _ = msg; + Err(ClientError::UnexpectedRequestPayload) } } @@ -92,7 +89,6 @@ impl ClientSession { use arbiter_proto::transport::DummyTransport; let transport: super::Transport = Box::new(DummyTransport::new()); let props = ClientConnection::new(db, transport, actors); - let key = VerifyingKey::from_bytes(&[0u8; 32]).unwrap(); - Self { props, key } + Self { props } } } diff --git a/server/crates/arbiter-server/src/actors/evm/mod.rs b/server/crates/arbiter-server/src/actors/evm/mod.rs index 0b7e97a..012b41c 100644 --- a/server/crates/arbiter-server/src/actors/evm/mod.rs +++ b/server/crates/arbiter-server/src/actors/evm/mod.rs @@ -1,4 +1,4 @@ -use alloy::{consensus::TxEip1559, network::TxSigner, primitives::Address, signers::Signature}; +use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; use diesel::{ExpressionMethods, OptionalExtension as _, QueryDsl, SelectableHelper as _, dsl::insert_into}; use diesel_async::RunQueryDsl; use kameo::{Actor, actor::ActorRef, messages}; diff --git a/server/crates/arbiter-server/src/actors/router/mod.rs b/server/crates/arbiter-server/src/actors/router/mod.rs index ac1d720..a0a75b8 100644 --- a/server/crates/arbiter-server/src/actors/router/mod.rs +++ b/server/crates/arbiter-server/src/actors/router/mod.rs @@ -67,7 +67,7 @@ async fn request_client_approval( client_pubkey: VerifyingKey, ) -> Result { if user_agents.is_empty() { - return Err(ApprovalError::NoUserAgentsConnected).into(); + return Err(ApprovalError::NoUserAgentsConnected); } let mut pool = JoinSet::new(); @@ -76,7 +76,6 @@ async fn request_client_approval( for weak_ref in user_agents { match weak_ref.upgrade() { Some(agent) => { - let client_pubkey = client_pubkey.clone(); let cancel_rx = cancel_rx.clone(); pool.spawn(async move { agent @@ -167,7 +166,7 @@ impl MessageRouter { // handle in subtask to not to lock the actor tokio::task::spawn(async move { let result = request_client_approval(&weak_refs, client_pubkey).await; - let _ = reply_sender.send(result); + reply_sender.send(result); }); reply diff --git a/server/crates/arbiter-server/src/evm/mod.rs b/server/crates/arbiter-server/src/evm/mod.rs index 9e00fc0..f295dc8 100644 --- a/server/crates/arbiter-server/src/evm/mod.rs +++ b/server/crates/arbiter-server/src/evm/mod.rs @@ -117,7 +117,8 @@ async fn check_shared_constraints( let now = Utc::now(); // Validity window - if shared.valid_from.map_or(false, |t| now < t) || shared.valid_until.map_or(false, |t| now > t) + if shared.valid_from.is_some_and(|t| now < t) + || shared.valid_until.is_some_and(|t| now > t) { violations.push(EvalViolation::InvalidTime); } @@ -125,8 +126,8 @@ async fn check_shared_constraints( // Gas fee caps let fee_exceeded = shared .max_gas_fee_per_gas - .map_or(false, |cap| U256::from(context.max_fee_per_gas) > cap); - let priority_exceeded = shared.max_priority_fee_per_gas.map_or(false, |cap| { + .is_some_and(|cap| U256::from(context.max_fee_per_gas) > cap); + let priority_exceeded = shared.max_priority_fee_per_gas.is_some_and(|cap| { U256::from(context.max_priority_fee_per_gas) > cap }); if fee_exceeded || priority_exceeded { @@ -228,7 +229,7 @@ impl Engine { .values(&NewEvmBasicGrant { wallet_id: full_grant.basic.wallet_id, chain_id: full_grant.basic.chain as i32, - client_id: client_id, + client_id, valid_from: full_grant.basic.valid_from.map(SqliteTimestamp), valid_until: full_grant.basic.valid_until.map(SqliteTimestamp), max_gas_fee_per_gas: full_grant diff --git a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs index dfea8cb..e1f01c5 100644 --- a/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/ether_transfer/mod.rs @@ -41,17 +41,12 @@ pub struct Meaning { } impl Display for Meaning { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Ether transfer of {} to {}", - self.value, - self.to.to_string() - ) + write!(f, "Ether transfer of {} to {}", self.value, self.to) } } -impl Into for Meaning { - fn into(self) -> SpecificMeaning { - SpecificMeaning::EtherTransfer(self) +impl From for SpecificMeaning { + fn from(val: Meaning) -> SpecificMeaning { + SpecificMeaning::EtherTransfer(val) } } @@ -61,9 +56,9 @@ pub struct Settings { limit: VolumeRateLimit, } -impl Into for Settings { - fn into(self) -> SpecificGrant { - SpecificGrant::EtherTransfer(self) +impl From for SpecificGrant { + fn from(val: Settings) -> SpecificGrant { + SpecificGrant::EtherTransfer(val) } } diff --git a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs index 53d8679..856370f 100644 --- a/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs +++ b/server/crates/arbiter-server/src/evm/policies/token_transfers/mod.rs @@ -51,9 +51,9 @@ impl std::fmt::Display for Meaning { ) } } -impl Into for Meaning { - fn into(self) -> SpecificMeaning { - SpecificMeaning::TokenTransfer(self) +impl From for SpecificMeaning { + fn from(val: Meaning) -> SpecificMeaning { + SpecificMeaning::TokenTransfer(val) } } @@ -63,9 +63,9 @@ pub struct Settings { target: Option
, volume_limits: Vec, } -impl Into for Settings { - fn into(self) -> SpecificGrant { - SpecificGrant::TokenTransfer(self) +impl From for SpecificGrant { + fn from(val: Settings) -> SpecificGrant { + SpecificGrant::TokenTransfer(val) } } @@ -156,10 +156,10 @@ impl Policy for TokenTransfer { return Ok(violations); } - if let Some(allowed) = grant.settings.target { - if allowed != meaning.to { - violations.push(EvalViolation::InvalidTarget { target: meaning.to }); - } + if let Some(allowed) = grant.settings.target + && allowed != meaning.to + { + violations.push(EvalViolation::InvalidTarget { target: meaning.to }); } let rate_violations = check_volume_rate_limits(grant, db).await?; diff --git a/server/crates/arbiter-server/src/evm/safe_signer.rs b/server/crates/arbiter-server/src/evm/safe_signer.rs index 5a2fdad..1e10031 100644 --- a/server/crates/arbiter-server/src/evm/safe_signer.rs +++ b/server/crates/arbiter-server/src/evm/safe_signer.rs @@ -94,13 +94,13 @@ impl SafeSigner { &self, tx: &mut dyn SignableTransaction, ) -> Result { - if let Some(chain_id) = self.chain_id { - if !tx.set_chain_id_checked(chain_id) { - return Err(Error::TransactionChainIdMismatch { - signer: chain_id, - tx: tx.chain_id().unwrap(), - }); - } + if let Some(chain_id) = self.chain_id + && !tx.set_chain_id_checked(chain_id) + { + return Err(Error::TransactionChainIdMismatch { + signer: chain_id, + tx: tx.chain_id().unwrap(), + }); } self.sign_hash_inner(&tx.signature_hash()).map_err(Error::other) } diff --git a/server/crates/arbiter-server/tests/common/mod.rs b/server/crates/arbiter-server/tests/common/mod.rs index e23360f..7fb5bac 100644 --- a/server/crates/arbiter-server/tests/common/mod.rs +++ b/server/crates/arbiter-server/tests/common/mod.rs @@ -9,7 +9,6 @@ use diesel_async::RunQueryDsl; use memsafe::MemSafe; use tokio::sync::mpsc; - #[allow(dead_code)] pub async fn bootstrapped_keyholder(db: &db::DatabasePool) -> KeyHolder { let mut actor = KeyHolder::new(db.clone()).await.unwrap(); @@ -31,13 +30,14 @@ pub async fn root_key_history_id(db: &db::DatabasePool) -> i32 { id.expect("root_key_id should be set after bootstrap") } - +#[allow(dead_code)] pub struct ChannelTransport { receiver: mpsc::Receiver, sender: mpsc::Sender, } impl ChannelTransport { + #[allow(dead_code)] pub fn new() -> (Self, ChannelTransport) { let (tx1, rx1) = mpsc::channel(10); let (tx2, rx2) = mpsc::channel(10); @@ -54,8 +54,6 @@ impl ChannelTransport { } } - - #[async_trait] impl Bi for ChannelTransport where