4 Commits

Author SHA1 Message Date
hdbg
a9f9fc2a9d housekeeping(server): fixed clippy warns
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-04-07 16:28:47 +02:00
hdbg
d22ab49e3d refactor(server): moved shared module crypto into arbiter-crypto 2026-04-07 16:24:51 +02:00
hdbg
a845181ef6 docs: ml-dsa scheme everywhere
Some checks failed
ci/woodpecker/pr/server-audit Pipeline was successful
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
2026-04-07 15:02:32 +02:00
hdbg
0d424f3afc refactor(server): migrated auth to ml-dsa 2026-04-07 14:55:31 +02:00
43 changed files with 639 additions and 506 deletions

View File

@@ -67,18 +67,14 @@ The `program_client.nonce` column stores the **next usable nonce** — i.e. it i
## Cryptography
### Authentication
- **Client protocol:** ed25519
- **Client protocol:** ML-DSA
### User-Agent Authentication
User-agent authentication supports multiple signature schemes because platform-provided "hardware-bound" keys do not expose a uniform algorithm across operating systems and hardware.
- **Supported schemes:** RSA, Ed25519, ECDSA (secp256k1)
- **Why:** the user agent authenticates with keys backed by platform facilities, and those facilities differ by platform
- **Apple Silicon Secure Enclave / Secure Element:** ECDSA-only in practice
- **Windows Hello / TPM 2.0:** currently RSA-backed in our integration
This is why the user-agent auth protocol carries an explicit `KeyType`, while the SDK client protocol remains fixed to ed25519.
- **Supported schemes:** ML-DSA
- **Why:** Secure Enclave (MacOS) support them natively, on other platforms we could emulate while they roll-out
### Encryption at Rest
- **Scheme:** Symmetric AEAD — currently **XChaCha20-Poly1305**

230
server/Cargo.lock generated
View File

@@ -347,7 +347,7 @@ dependencies = [
"ruint",
"rustc-hash",
"serde",
"sha3",
"sha3 0.10.8",
]
[[package]]
@@ -548,7 +548,7 @@ dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"sha3",
"sha3 0.10.8",
"syn 2.0.117",
"syn-solidity",
]
@@ -680,9 +680,9 @@ name = "arbiter-client"
version = "0.1.0"
dependencies = [
"alloy",
"arbiter-crypto",
"arbiter-proto",
"async-trait",
"ed25519-dalek",
"http",
"rand 0.10.0",
"rustls-webpki",
@@ -692,6 +692,16 @@ dependencies = [
"tonic",
]
[[package]]
name = "arbiter-crypto"
version = "0.1.0"
dependencies = [
"base64",
"memsafe",
"ml-dsa",
"rand 0.10.0",
]
[[package]]
name = "arbiter-proto"
version = "0.1.0"
@@ -725,6 +735,7 @@ version = "0.1.0"
dependencies = [
"alloy",
"anyhow",
"arbiter-crypto",
"arbiter-proto",
"arbiter-tokens-registry",
"argon2",
@@ -742,7 +753,7 @@ dependencies = [
"insta",
"k256",
"kameo",
"memsafe",
"ml-dsa",
"mutants",
"pem",
"proptest",
@@ -751,14 +762,13 @@ dependencies = [
"rand 0.10.0",
"rcgen",
"restructed",
"rsa",
"rstest",
"rustls",
"secrecy",
"serde_with",
"sha2 0.10.9",
"smlang",
"spki",
"spki 0.7.3",
"strum 0.28.0",
"subtle",
"test-log",
@@ -1449,6 +1459,12 @@ dependencies = [
"cc",
]
[[package]]
name = "cmov"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
[[package]]
name = "console"
version = "0.15.11"
@@ -1479,6 +1495,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "const_format"
version = "0.2.35"
@@ -1600,6 +1622,15 @@ dependencies = [
"hybrid-array",
]
[[package]]
name = "ctutils"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
dependencies = [
"cmov",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
@@ -1738,8 +1769,17 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"pem-rfc7468",
"const-oid 0.9.6",
"zeroize",
]
[[package]]
name = "der"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b"
dependencies = [
"const-oid 0.10.2",
"zeroize",
]
@@ -1882,7 +1922,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"const-oid",
"const-oid 0.9.6",
"crypto-common 0.1.7",
"subtle",
]
@@ -1946,13 +1986,13 @@ version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"der 0.7.10",
"digest 0.10.7",
"elliptic-curve",
"rfc6979",
"serdect",
"signature 2.2.0",
"spki",
"spki 0.7.3",
]
[[package]]
@@ -1961,7 +2001,6 @@ version = "3.0.0-rc.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890"
dependencies = [
"serde",
"signature 3.0.0-rc.10",
]
@@ -1974,7 +2013,6 @@ dependencies = [
"curve25519-dalek 5.0.0-pre.6",
"ed25519",
"rand_core 0.10.0",
"serde",
"sha2 0.11.0-rc.5",
"subtle",
"zeroize",
@@ -2013,7 +2051,7 @@ dependencies = [
"ff",
"generic-array",
"group",
"pkcs8",
"pkcs8 0.10.2",
"rand_core 0.6.4",
"sec1",
"serdect",
@@ -2560,6 +2598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1"
dependencies = [
"typenum",
"zeroize",
]
[[package]]
@@ -2957,6 +2996,16 @@ dependencies = [
"cpufeatures 0.2.17",
]
[[package]]
name = "keccak"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
]
[[package]]
name = "keccak-asm"
version = "0.1.5"
@@ -2972,9 +3021,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"
@@ -3173,6 +3219,34 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "ml-dsa"
version = "0.1.0-rc.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5b2bb0ad6fa2b40396775bd56f51345171490fef993f46f91a876ecdbdaea55"
dependencies = [
"const-oid 0.10.2",
"ctutils",
"hybrid-array",
"module-lattice",
"pkcs8 0.11.0-rc.11",
"rand_core 0.10.0",
"sha3 0.11.0",
"signature 3.0.0-rc.10",
"zeroize",
]
[[package]]
name = "module-lattice"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "164eb3faeaecbd14b0b2a917c1b4d0c035097a9c559b0bed85c2cdd032bc8faa"
dependencies = [
"hybrid-array",
"num-traits",
"zeroize",
]
[[package]]
name = "multimap"
version = "0.10.1"
@@ -3214,23 +3288,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",
"serde",
"smallvec",
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.2.0"
@@ -3246,17 +3303,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"
@@ -3426,15 +3472,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"
@@ -3494,25 +3531,24 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
"der 0.7.10",
"spki 0.7.3",
]
[[package]]
name = "pkcs8"
version = "0.11.0-rc.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577"
dependencies = [
"der 0.8.0",
"spki 0.8.0",
]
[[package]]
@@ -4152,28 +4188,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",
"serde",
"sha2 0.10.9",
"signature 2.2.0",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rsqlite-vfs"
version = "0.1.0"
@@ -4413,9 +4427,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"der 0.7.10",
"generic-array",
"pkcs8",
"pkcs8 0.10.2",
"serdect",
"subtle",
"zeroize",
@@ -4609,7 +4623,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest 0.10.7",
"keccak",
"keccak 0.1.6",
]
[[package]]
name = "sha3"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1"
dependencies = [
"digest 0.11.2",
"keccak 0.2.0",
]
[[package]]
@@ -4662,6 +4686,10 @@ name = "signature"
version = "3.0.0-rc.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3"
dependencies = [
"digest 0.11.2",
"rand_core 0.10.0",
]
[[package]]
name = "simd-adler32"
@@ -4721,12 +4749,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"
@@ -4734,7 +4756,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
"der 0.7.10",
]
[[package]]
name = "spki"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f"
dependencies = [
"base64ct",
"der 0.8.0",
]
[[package]]

View File

@@ -20,7 +20,7 @@ tokio = { version = "1.50.0", features = ["full"] }
ed25519-dalek = { version = "3.0.0-pre.6", features = ["rand_core"] }
chrono = { version = "0.4.44", features = ["serde"] }
rand = "0.10.0"
rustls = { version = "0.23.37", features = ["aws-lc-rs"] }
rustls = { version = "0.23.37", features = ["aws-lc-rs", "logging", "prefer-post-quantum", "std"], default-features = false }
smlang = "0.8.0"
thiserror = "2.0.18"
async-trait = "0.1.89"
@@ -45,3 +45,5 @@ spki = "0.7"
prost = "0.14.3"
miette = { version = "7.6.0", features = ["fancy", "serde"] }
mutants = "0.0.4"
ml-dsa = { version = "0.1.0-rc.8", features = ["zeroize"] }
base64 = "0.22.1"

View File

@@ -13,12 +13,12 @@ evm = ["dep:alloy"]
[dependencies]
arbiter-proto.path = "../arbiter-proto"
arbiter-crypto.path = "../arbiter-crypto"
alloy = { workspace = true, optional = true }
tonic.workspace = true
tonic.features = ["tls-aws-lc"]
tokio.workspace = true
tokio-stream.workspace = true
ed25519-dalek.workspace = true
thiserror.workspace = true
http = "1.4.0"
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }

View File

@@ -1,5 +1,6 @@
use arbiter_crypto::authn::{CLIENT_CONTEXT, SigningKey, format_challenge};
use arbiter_proto::{
ClientMetadata, format_challenge,
ClientMetadata,
proto::{
client::{
ClientRequest,
@@ -14,7 +15,6 @@ use arbiter_proto::{
shared::ClientInfo as ProtoClientInfo,
},
};
use ed25519_dalek::Signer as _;
use crate::{
storage::StorageError,
@@ -54,14 +54,14 @@ fn map_auth_result(code: i32) -> AuthError {
async fn send_auth_challenge_request(
transport: &mut ClientTransport,
metadata: ClientMetadata,
key: &ed25519_dalek::SigningKey,
key: &SigningKey,
) -> std::result::Result<(), AuthError> {
transport
.send(ClientRequest {
request_id: next_request_id(),
payload: Some(ClientRequestPayload::Auth(proto_auth::Request {
payload: Some(AuthRequestPayload::ChallengeRequest(AuthChallengeRequest {
pubkey: key.verifying_key().to_bytes().to_vec(),
pubkey: key.public_key().to_bytes(),
client_info: Some(ProtoClientInfo {
name: metadata.name,
description: metadata.description,
@@ -95,11 +95,14 @@ async fn receive_auth_challenge(
async fn send_auth_challenge_solution(
transport: &mut ClientTransport,
key: &ed25519_dalek::SigningKey,
key: &SigningKey,
challenge: AuthChallenge,
) -> std::result::Result<(), AuthError> {
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey);
let signature = key.sign(&challenge_payload).to_bytes().to_vec();
let signature = key
.sign_message(&challenge_payload, CLIENT_CONTEXT)
.map_err(|_| AuthError::UnexpectedAuthResponse)?
.to_bytes();
transport
.send(ClientRequest {
@@ -140,7 +143,7 @@ async fn receive_auth_confirmation(
pub(crate) async fn authenticate(
transport: &mut ClientTransport,
metadata: ClientMetadata,
key: &ed25519_dalek::SigningKey,
key: &SigningKey,
) -> std::result::Result<(), AuthError> {
send_auth_challenge_request(transport, metadata, key).await?;
let challenge = receive_auth_challenge(transport).await?;

View File

@@ -1,3 +1,4 @@
use arbiter_crypto::authn::SigningKey;
use arbiter_proto::{
ClientMetadata, proto::arbiter_service_client::ArbiterServiceClient, url::ArbiterUrl,
};
@@ -60,7 +61,7 @@ impl ArbiterClient {
pub async fn connect_with_key(
url: ArbiterUrl,
metadata: ClientMetadata,
key: ed25519_dalek::SigningKey,
key: SigningKey,
) -> Result<Self, Error> {
let anchor = webpki::anchor_from_trusted_cert(&url.ca_cert)?.to_owned();
let tls = ClientTlsConfig::new().trust_anchor(anchor);

View File

@@ -1,3 +1,4 @@
use arbiter_crypto::authn::SigningKey;
use arbiter_proto::home_path;
use std::path::{Path, PathBuf};
@@ -11,7 +12,7 @@ pub enum StorageError {
}
pub trait SigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError>;
fn load_or_create(&self) -> std::result::Result<SigningKey, StorageError>;
}
#[derive(Debug, Clone)]
@@ -20,7 +21,7 @@ pub struct FileSigningKeyStorage {
}
impl FileSigningKeyStorage {
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ed25519.key";
pub const DEFAULT_FILE_NAME: &str = "sdk_client_ml_dsa.key";
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
@@ -30,7 +31,7 @@ impl FileSigningKeyStorage {
Ok(Self::new(home_path()?.join(Self::DEFAULT_FILE_NAME)))
}
fn read_key(path: &Path) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
fn read_key(path: &Path) -> std::result::Result<SigningKey, StorageError> {
let bytes = std::fs::read(path)?;
let raw: [u8; 32] =
bytes
@@ -39,12 +40,12 @@ impl FileSigningKeyStorage {
expected: 32,
actual: v.len(),
})?;
Ok(ed25519_dalek::SigningKey::from_bytes(&raw))
Ok(SigningKey::from_seed(raw))
}
}
impl SigningKeyStorage for FileSigningKeyStorage {
fn load_or_create(&self) -> std::result::Result<ed25519_dalek::SigningKey, StorageError> {
fn load_or_create(&self) -> std::result::Result<SigningKey, StorageError> {
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)?;
}
@@ -53,8 +54,8 @@ impl SigningKeyStorage for FileSigningKeyStorage {
return Self::read_key(&self.path);
}
let key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let raw_key = key.to_bytes();
let key = SigningKey::generate();
let raw_key = key.to_seed();
// Use create_new to prevent accidental overwrite if another process creates the key first.
match std::fs::OpenOptions::new()
@@ -103,7 +104,7 @@ mod tests {
.load_or_create()
.expect("second load_or_create should read same key");
assert_eq!(key_a.to_bytes(), key_b.to_bytes());
assert_eq!(key_a.to_seed(), key_b.to_seed());
assert!(path.exists());
std::fs::remove_file(path).expect("temp key file should be removable");

View File

@@ -0,0 +1 @@
/target

View File

@@ -0,0 +1,18 @@
[package]
name = "arbiter-crypto"
version = "0.1.0"
edition = "2024"
[dependencies]
ml-dsa = {workspace = true, optional = true }
rand = {workspace = true, optional = true}
base64 = {workspace = true, optional = true }
memsafe = {version = "0.4.0", optional = true}
[lints]
workspace = true
[features]
default = ["authn", "safecell"]
authn = ["dep:ml-dsa", "dep:rand", "dep:base64"]
safecell = ["dep:memsafe"]

View File

@@ -0,0 +1,2 @@
pub mod v1;
pub use v1::*;

View File

@@ -0,0 +1,186 @@
use base64::{Engine as _, prelude::BASE64_STANDARD};
use ml_dsa::{
EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature,
SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _,
};
pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client";
pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent";
pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec<u8> {
let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey));
concat_form.into_bytes()
}
pub type KeyParams = MlDsa87;
#[derive(Clone, Debug, PartialEq)]
pub struct PublicKey(Box<MlDsaVerifyingKey<KeyParams>>);
#[derive(Clone, Debug, PartialEq)]
pub struct Signature(Box<MlDsaSignature<KeyParams>>);
#[derive(Debug)]
pub struct SigningKey(Box<MlDsaSigningKey<KeyParams>>);
impl PublicKey {
pub fn to_bytes(&self) -> Vec<u8> {
self.0.encode().to_vec()
}
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool {
self.0.verify_with_context(
&format_challenge(nonce, &self.to_bytes()),
context,
&signature.0,
)
}
}
impl Signature {
pub fn to_bytes(&self) -> Vec<u8> {
self.0.encode().to_vec()
}
}
impl SigningKey {
pub fn generate() -> Self {
Self(Box::new(KeyParams::key_gen(&mut rand::rng())))
}
pub fn from_seed(seed: [u8; 32]) -> Self {
Self(Box::new(KeyParams::from_seed(&Seed::from(seed))))
}
pub fn to_seed(&self) -> [u8; 32] {
self.0.to_seed().into()
}
pub fn public_key(&self) -> PublicKey {
self.0.verifying_key().into()
}
pub fn sign_message(&self, message: &[u8], context: &[u8]) -> Result<Signature, Error> {
self.0
.signing_key()
.sign_deterministic(message, context)
.map(Into::into)
}
pub fn sign_challenge(&self, nonce: i32, context: &[u8]) -> Result<Signature, Error> {
self.sign_message(
&format_challenge(nonce, &self.public_key().to_bytes()),
context,
)
}
}
impl From<MlDsaVerifyingKey<KeyParams>> for PublicKey {
fn from(value: MlDsaVerifyingKey<KeyParams>) -> Self {
Self(Box::new(value))
}
}
impl From<MlDsaSignature<KeyParams>> for Signature {
fn from(value: MlDsaSignature<KeyParams>) -> Self {
Self(Box::new(value))
}
}
impl From<MlDsaSigningKey<KeyParams>> for SigningKey {
fn from(value: MlDsaSigningKey<KeyParams>) -> Self {
Self(Box::new(value))
}
}
impl TryFrom<Vec<u8>> for PublicKey {
type Error = ();
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(value.as_slice())
}
}
impl TryFrom<&'_ [u8]> for PublicKey {
type Error = ();
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
let encoded = EncodedVerifyingKey::<KeyParams>::try_from(value).map_err(|_| ())?;
Ok(Self(Box::new(MlDsaVerifyingKey::decode(&encoded))))
}
}
impl TryFrom<Vec<u8>> for Signature {
type Error = ();
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from(value.as_slice())
}
}
impl TryFrom<&'_ [u8]> for Signature {
type Error = ();
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
MlDsaSignature::try_from(value)
.map(|sig| Self(Box::new(sig)))
.map_err(|_| ())
}
}
#[cfg(test)]
mod tests {
use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _};
use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT};
#[test]
fn public_key_round_trip_decodes() {
let key = MlDsa87::key_gen(&mut rand::rng());
let encoded = PublicKey::from(key.verifying_key()).to_bytes();
let decoded = PublicKey::try_from(encoded.as_slice()).expect("public key should decode");
assert_eq!(decoded, PublicKey::from(key.verifying_key()));
}
#[test]
fn signature_round_trip_decodes() {
let key = SigningKey::generate();
let signature = key
.sign_message(b"challenge", CLIENT_CONTEXT)
.expect("signature should be created");
let decoded =
Signature::try_from(signature.to_bytes().as_slice()).expect("signature should decode");
assert_eq!(decoded, signature);
}
#[test]
fn challenge_verification_uses_context_and_canonical_key_bytes() {
let key = SigningKey::generate();
let public_key = key.public_key();
let nonce = 17;
let signature = key
.sign_challenge(nonce, CLIENT_CONTEXT)
.expect("signature should be created");
assert!(public_key.verify(nonce, CLIENT_CONTEXT, &signature));
assert!(!public_key.verify(nonce, USERAGENT_CONTEXT, &signature));
}
#[test]
fn signing_key_round_trip_seed_preserves_public_key_and_signing() {
let original = SigningKey::generate();
let restored = SigningKey::from_seed(original.to_seed());
assert_eq!(restored.public_key(), original.public_key());
let signature = restored
.sign_challenge(9, CLIENT_CONTEXT)
.expect("signature should be created");
assert!(restored.public_key().verify(9, CLIENT_CONTEXT, &signature));
}
}

View File

@@ -0,0 +1,5 @@
#[cfg(feature = "authn")]
pub mod authn;
#[cfg(feature = "safecell")]
pub mod safecell;

View File

@@ -105,6 +105,11 @@ impl<T> SafeCellHandle<T> for MemSafeCell<T> {
fn abort_memory_breach(action: &str, err: &memsafe::error::MemoryError) -> ! {
eprintln!("fatal {action}: {err}");
// SAFETY: Intentionally cause a segmentation fault to prevent further execution in a compromised state.
unsafe {
let unsafe_pointer = std::ptr::null_mut::<u8>();
std::ptr::write_volatile(unsafe_pointer, 0);
}
std::process::abort();
}

View File

@@ -17,7 +17,7 @@ url = "2.5.8"
miette.workspace = true
thiserror.workspace = true
rustls-pki-types.workspace = true
base64 = "0.22.1"
base64.workspace = true
prost-types.workspace = true
tracing.workspace = true
async-trait.workspace = true

View File

@@ -1,8 +1,6 @@
pub mod transport;
pub mod url;
use base64::{Engine, prelude::BASE64_STANDARD};
pub mod proto {
tonic::include_proto!("arbiter");
@@ -84,8 +82,3 @@ pub fn home_path() -> Result<std::path::PathBuf, std::io::Error> {
Ok(arbiter_home)
}
pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec<u8> {
let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey));
concat_form.into_bytes()
}

View File

@@ -16,9 +16,8 @@ diesel-async = { version = "0.8.0", features = [
"sqlite",
"tokio",
] }
ed25519-dalek.workspace = true
ed25519-dalek.features = ["serde"]
arbiter-proto.path = "../arbiter-proto"
arbiter-crypto.path = "../arbiter-crypto"
tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tonic.workspace = true
@@ -37,19 +36,13 @@ dashmap = "6.1.0"
rand.workspace = true
rcgen.workspace = true
chrono.workspace = true
memsafe = "0.4.0"
zeroize = { version = "1.8.2", features = ["std", "simd"] }
kameo.workspace = true
x25519-dalek.workspace = true
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
argon2 = { version = "0.5.3", features = ["zeroize"] }
restructed = "0.2.2"
strum = { version = "0.28.0", features = ["derive"] }
pem = "3.0.6"
k256.workspace = true
k256.features = ["serde"]
rsa.workspace = true
rsa.features = ["serde"]
sha2.workspace = true
hmac = "0.12"
spki.workspace = true
@@ -61,6 +54,10 @@ anyhow = "1.0.102"
serde_with = "3.18.0"
mutants.workspace = true
subtle = "2.6.1"
ml-dsa.workspace = true
ed25519-dalek.workspace = true
x25519-dalek.workspace = true
k256.workspace = true
[dev-dependencies]
insta = "1.46.3"

View File

@@ -47,7 +47,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)
key_type integer not null default(1),
created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now'))
) STRICT;

View File

@@ -1,5 +1,6 @@
use arbiter_crypto::authn::{self, CLIENT_CONTEXT};
use arbiter_proto::{
ClientMetadata, format_challenge,
ClientMetadata,
transport::{Bi, expect_message},
};
use chrono::Utc;
@@ -8,7 +9,6 @@ use diesel::{
dsl::insert_into, update,
};
use diesel_async::RunQueryDsl as _;
use ed25519_dalek::{Signature, VerifyingKey};
use kameo::{actor::ActorRef, error::SendError};
use tracing::error;
@@ -62,17 +62,20 @@ pub enum ApproveError {
#[derive(Debug, Clone)]
pub enum Inbound {
AuthChallengeRequest {
pubkey: VerifyingKey,
pubkey: authn::PublicKey,
metadata: ClientMetadata,
},
AuthChallengeSolution {
signature: Signature,
signature: authn::Signature,
},
}
#[derive(Debug, Clone)]
pub enum Outbound {
AuthChallenge { pubkey: VerifyingKey, nonce: i32 },
AuthChallenge {
pubkey: authn::PublicKey,
nonce: i32,
},
AuthSuccess,
}
@@ -80,9 +83,9 @@ pub enum Outbound {
/// Returns `None` if the pubkey is not registered.
async fn get_current_nonce_and_id(
db: &db::DatabasePool,
pubkey: &VerifyingKey,
pubkey: &authn::PublicKey,
) -> Result<Option<(i32, i32)>, Error> {
let pubkey_bytes = pubkey.as_bytes().to_vec();
let pubkey_bytes = pubkey.to_bytes();
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable
@@ -102,19 +105,17 @@ async fn get_current_nonce_and_id(
async fn verify_integrity(
db: &db::DatabasePool,
keyholder: &ActorRef<KeyHolder>,
pubkey: &VerifyingKey,
pubkey: &authn::PublicKey,
) -> Result<(), Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable
})?;
let (id, nonce) = get_current_nonce_and_id(db, pubkey)
.await?
.ok_or_else(|| {
error!("Client not found during integrity verification");
Error::DatabaseOperationFailed
})?;
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?.ok_or_else(|| {
error!("Client not found during integrity verification");
Error::DatabaseOperationFailed
})?;
let attestation = integrity::verify_entity(
&mut db_conn,
@@ -144,9 +145,9 @@ async fn verify_integrity(
async fn create_nonce(
db: &db::DatabasePool,
keyholder: &ActorRef<KeyHolder>,
pubkey: &VerifyingKey,
pubkey: &authn::PublicKey,
) -> Result<i32, Error> {
let pubkey_bytes = pubkey.as_bytes().to_vec();
let pubkey_bytes = pubkey.to_bytes();
let pubkey = pubkey.clone();
let mut conn = db.get().await.map_err(|e| {
@@ -212,7 +213,7 @@ async fn approve_new_client(
async fn insert_client(
db: &db::DatabasePool,
keyholder: &ActorRef<KeyHolder>,
pubkey: &VerifyingKey,
pubkey: &authn::PublicKey,
metadata: &ClientMetadata,
) -> Result<i32, Error> {
use crate::db::schema::{client_metadata, program_client};
@@ -242,7 +243,7 @@ async fn insert_client(
let client_id = insert_into(program_client::table)
.values((
program_client::public_key.eq(pubkey.as_bytes().to_vec()),
program_client::public_key.eq(pubkey.to_bytes()),
program_client::metadata_id.eq(metadata_id),
program_client::nonce.eq(NONCE_START),
))
@@ -345,14 +346,17 @@ async fn sync_client_metadata(
async fn challenge_client<T>(
transport: &mut T,
pubkey: VerifyingKey,
pubkey: authn::PublicKey,
nonce: i32,
) -> Result<(), Error>
where
T: Bi<Inbound, Result<Outbound, Error>> + ?Sized,
{
transport
.send(Ok(Outbound::AuthChallenge { pubkey, nonce }))
.send(Ok(Outbound::AuthChallenge {
pubkey: pubkey.clone(),
nonce,
}))
.await
.map_err(|e| {
error!(error = ?e, "Failed to send auth challenge");
@@ -369,12 +373,10 @@ where
Error::Transport
})?;
let formatted = format_challenge(nonce, pubkey.as_bytes());
pubkey.verify_strict(&formatted, &signature).map_err(|_| {
if !pubkey.verify(nonce, CLIENT_CONTEXT, &signature) {
error!("Challenge solution verification failed");
Error::InvalidChallengeSolution
})?;
return Err(Error::InvalidChallengeSolution);
}
Ok(())
}
@@ -396,7 +398,7 @@ where
approve_new_client(
&props.actors,
ClientProfile {
pubkey,
pubkey: pubkey.clone(),
metadata: metadata.clone(),
},
)

View File

@@ -1,3 +1,4 @@
use arbiter_crypto::authn;
use arbiter_proto::{ClientMetadata, transport::Bi};
use kameo::actor::Spawn;
use tracing::{error, info};
@@ -10,12 +11,12 @@ use crate::{
#[derive(Debug, Clone)]
pub struct ClientProfile {
pub pubkey: ed25519_dalek::VerifyingKey,
pub pubkey: authn::PublicKey,
pub metadata: ClientMetadata,
}
pub struct ClientCredentials {
pub pubkey: ed25519_dalek::VerifyingKey,
pub pubkey: authn::PublicKey,
pub nonce: i32,
}
@@ -25,7 +26,7 @@ impl Integrable for ClientCredentials {
impl Hashable for ClientCredentials {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
hasher.update(self.pubkey.as_bytes());
hasher.update(self.pubkey.to_bytes());
self.nonce.hash(hasher);
}
}
@@ -48,7 +49,9 @@ pub async fn connect_client<T>(mut props: ClientConnection, transport: &mut T)
where
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send + ?Sized,
{
match auth::authenticate(&mut props, transport).await {
let fut = auth::authenticate(&mut props, transport);
println!("authenticate future size: {}", std::mem::size_of_val(&fut));
match fut.await {
Ok(client_id) => {
ClientSession::spawn(ClientSession::new(props, client_id));
info!("Client authenticated, session started");

View File

@@ -7,11 +7,11 @@ use kameo::{Actor, actor::ActorRef, messages};
use rand::{SeedableRng, rng, rngs::StdRng};
use crate::{
actors::keyholder::{CreateNew, Decrypt, GetState, KeyHolder, KeyHolderState},
actors::keyholder::{CreateNew, Decrypt, KeyHolder},
crypto::integrity,
db::{
DatabaseError, DatabasePool,
models::{self, SqliteTimestamp},
models::{self},
schema,
},
evm::{
@@ -21,8 +21,8 @@ use crate::{
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
},
},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
pub use crate::evm::safe_signer;
@@ -158,7 +158,7 @@ impl EvmActor {
}
#[message]
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
pub async fn useragent_delete_grant(&mut self, _grant_id: i32) -> Result<(), Error> {
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
// let keyholder = self.keyholder.clone();

View File

@@ -9,22 +9,17 @@ use kameo::{Actor, Reply, messages};
use strum::{EnumDiscriminants, IntoDiscriminant};
use tracing::{error, info};
use crate::{
crypto::{
KeyCell, derive_key,
encryption::v1::{self, Nonce},
integrity::v1::HmacSha256,
},
safe_cell::SafeCell,
use crate::crypto::{
KeyCell, derive_key,
encryption::v1::{self, Nonce},
integrity::v1::HmacSha256,
};
use crate::{
db::{
self,
models::{self, RootKeyHistory},
schema::{self},
},
safe_cell::SafeCellHandle as _,
use crate::db::{
self,
models::{self, RootKeyHistory},
schema::{self},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
#[derive(Default, EnumDiscriminants)]
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
@@ -400,10 +395,8 @@ mod tests {
use diesel_async::RunQueryDsl;
use crate::{
db::{self},
safe_cell::SafeCell,
};
use crate::db::{self};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use super::*;

View File

@@ -1,18 +1,18 @@
use arbiter_crypto::authn;
use arbiter_proto::transport::Bi;
use tracing::error;
use crate::actors::user_agent::{
AuthPublicKey, UserAgentConnection,
UserAgentConnection,
auth::state::{AuthContext, AuthStateMachine},
};
mod state;
use state::*;
#[derive(Debug, Clone)]
pub enum Inbound {
AuthChallengeRequest {
pubkey: AuthPublicKey,
pubkey: authn::PublicKey,
bootstrap_token: Option<String>,
},
AuthChallengeSolution {
@@ -71,7 +71,7 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
pub async fn authenticate<T>(
props: &mut UserAgentConnection,
transport: T,
) -> Result<AuthPublicKey, Error>
) -> Result<authn::PublicKey, Error>
where
T: Bi<Inbound, Result<Outbound, Error>> + Send,
{

View File

@@ -1,7 +1,8 @@
use arbiter_crypto::authn::{self, USERAGENT_CONTEXT};
use arbiter_proto::transport::Bi;
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::{actor::ActorRef, error::SendError};
use kameo::actor::ActorRef;
use tracing::error;
use super::Error;
@@ -9,24 +10,24 @@ use crate::{
actors::{
bootstrap::ConsumeToken,
keyholder::KeyHolder,
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound},
user_agent::{UserAgentConnection, UserAgentCredentials, auth::Outbound},
},
crypto::integrity::{self, AttestationStatus},
crypto::integrity,
db::{DatabasePool, schema::useragent_client},
};
pub struct ChallengeRequest {
pub pubkey: AuthPublicKey,
pub pubkey: authn::PublicKey,
}
pub struct BootstrapAuthRequest {
pub pubkey: AuthPublicKey,
pub pubkey: authn::PublicKey,
pub token: String,
}
pub struct ChallengeContext {
pub challenge_nonce: i32,
pub key: AuthPublicKey,
pub key: authn::PublicKey,
}
pub struct ChallengeSolution {
@@ -38,15 +39,15 @@ smlang::statemachine!(
custom_error: true,
transitions: {
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthPublicKey),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthPublicKey),
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(authn::PublicKey),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(authn::PublicKey),
}
);
/// Returns the current nonce, ready to use for the challenge nonce.
async fn get_current_nonce_and_id(
db: &DatabasePool,
key: &AuthPublicKey,
key: &authn::PublicKey,
) -> Result<(i32, i32), Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
@@ -56,8 +57,7 @@ async fn get_current_nonce_and_id(
.exclusive_transaction(|conn| {
Box::pin(async move {
useragent_client::table
.filter(useragent_client::public_key.eq(key.to_stored_bytes()))
.filter(useragent_client::key_type.eq(key.key_type()))
.filter(useragent_client::public_key.eq(key.to_bytes()))
.select((useragent_client::id, useragent_client::nonce))
.first::<(i32, i32)>(conn)
.await
@@ -78,7 +78,7 @@ async fn get_current_nonce_and_id(
async fn verify_integrity(
db: &DatabasePool,
keyholder: &ActorRef<KeyHolder>,
pubkey: &AuthPublicKey,
pubkey: &authn::PublicKey,
) -> Result<(), Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
@@ -87,7 +87,7 @@ async fn verify_integrity(
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
let result = integrity::verify_entity(
let _result = integrity::verify_entity(
&mut db_conn,
keyholder,
&UserAgentCredentials {
@@ -108,7 +108,7 @@ async fn verify_integrity(
async fn create_nonce(
db: &DatabasePool,
keyholder: &ActorRef<KeyHolder>,
pubkey: &AuthPublicKey,
pubkey: &authn::PublicKey,
) -> Result<i32, Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
@@ -118,8 +118,7 @@ async fn create_nonce(
.exclusive_transaction(|conn| {
Box::pin(async move {
let (id, new_nonce): (i32, i32) = update(useragent_client::table)
.filter(useragent_client::public_key.eq(pubkey.to_stored_bytes()))
.filter(useragent_client::key_type.eq(pubkey.key_type()))
.filter(useragent_client::public_key.eq(pubkey.to_bytes()))
.set(useragent_client::nonce.eq(useragent_client::nonce + 1))
.returning((useragent_client::id, useragent_client::nonce))
.get_result(conn)
@@ -154,10 +153,9 @@ async fn create_nonce(
async fn register_key(
db: &DatabasePool,
keyholder: &ActorRef<KeyHolder>,
pubkey: &AuthPublicKey,
pubkey: &authn::PublicKey,
) -> Result<(), Error> {
let pubkey_bytes = pubkey.to_stored_bytes();
let key_type = pubkey.key_type();
let pubkey_bytes = pubkey.to_bytes();
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::internal("Database unavailable")
@@ -171,7 +169,6 @@ async fn register_key(
.values((
useragent_client::public_key.eq(pubkey_bytes),
useragent_client::nonce.eq(NONCE_START),
useragent_client::key_type.eq(key_type),
))
.returning(useragent_client::id)
.get_result(conn)
@@ -186,7 +183,7 @@ async fn register_key(
nonce: NONCE_START,
};
integrity::sign_entity(conn, &keyholder, &entity, id)
integrity::sign_entity(conn, keyholder, &entity, id)
.await
.map_err(|e| {
error!(error = ?e, "Failed to sign integrity tag for new user-agent key");
@@ -245,7 +242,7 @@ where
async fn verify_bootstrap_token(
&mut self,
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest,
) -> Result<AuthPublicKey, Self::Error> {
) -> Result<authn::PublicKey, Self::Error> {
let token_ok: bool = self
.conn
.actors
@@ -293,35 +290,13 @@ where
key,
}: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution,
) -> Result<AuthPublicKey, Self::Error> {
let formatted = arbiter_proto::format_challenge(*challenge_nonce, &key.to_stored_bytes());
) -> Result<authn::PublicKey, Self::Error> {
let signature = authn::Signature::try_from(solution.as_slice()).map_err(|_| {
error!("Failed to decode signature in challenge solution");
Error::InvalidChallengeSolution
})?;
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::<sha2::Sha256>::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()
}
};
let valid = key.verify(*challenge_nonce, USERAGENT_CONTEXT, &signature);
match valid {
true => {

View File

@@ -1,22 +1,13 @@
use crate::{
actors::{GlobalActors, client::ClientProfile},
crypto::integrity::Integrable,
db::{self, models::KeyType},
db,
};
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
#[derive(Clone, Debug)]
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),
}
use arbiter_crypto::authn;
#[derive(Debug)]
pub struct UserAgentCredentials {
pub pubkey: AuthPublicKey,
pub pubkey: authn::PublicKey,
pub nonce: i32,
}
@@ -24,67 +15,11 @@ impl Integrable for UserAgentCredentials {
const KIND: &'static str = "useragent_credentials";
}
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<u8> {
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 _;
#[allow(clippy::expect_used)]
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,
}
}
}
impl TryFrom<(KeyType, Vec<u8>)> for AuthPublicKey {
type Error = &'static str;
fn try_from(value: (KeyType, Vec<u8>)) -> Result<Self, Self::Error> {
let (key_type, bytes) = value;
match key_type {
KeyType::Ed25519 => {
let bytes: [u8; 32] = bytes.try_into().map_err(|_| "invalid Ed25519 key length")?;
let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes)
.map_err(|_e| "invalid Ed25519 key")?;
Ok(AuthPublicKey::Ed25519(key))
}
KeyType::EcdsaSecp256k1 => {
let point =
k256::EncodedPoint::from_bytes(&bytes).map_err(|_e| "invalid ECDSA key")?;
let key = k256::ecdsa::VerifyingKey::from_encoded_point(&point)
.map_err(|_e| "invalid ECDSA key")?;
Ok(AuthPublicKey::EcdsaSecp256k1(key))
}
KeyType::Rsa => {
use rsa::pkcs8::DecodePublicKey as _;
let key = rsa::RsaPublicKey::from_public_key_der(&bytes)
.map_err(|_e| "invalid RSA key")?;
Ok(AuthPublicKey::Rsa(key))
}
}
}
}
// Messages, sent by user agent to connection client without having a request
#[derive(Debug)]
pub enum OutOfBand {
ClientConnectionRequest { profile: ClientProfile },
ClientConnectionCancel { pubkey: ed25519_dalek::VerifyingKey },
ClientConnectionCancel { pubkey: authn::PublicKey },
}
pub struct UserAgentConnection {
@@ -106,9 +41,9 @@ pub use session::UserAgentSession;
use crate::crypto::integrity::hashing::Hashable;
impl Hashable for AuthPublicKey {
impl Hashable for authn::PublicKey {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
hasher.update(&self.to_stored_bytes());
hasher.update(self.to_bytes());
}
}

View File

@@ -1,8 +1,9 @@
use arbiter_crypto::authn;
use std::{borrow::Cow, collections::HashMap};
use arbiter_proto::transport::Sender;
use async_trait::async_trait;
use ed25519_dalek::VerifyingKey;
use kameo::{Actor, actor::ActorRef, messages};
use thiserror::Error;
use tracing::error;
@@ -12,7 +13,6 @@ use crate::actors::{
flow_coordinator::{RegisterUserAgent, client_connect_approval::ClientApprovalController},
user_agent::{OutOfBand, UserAgentConnection},
};
mod state;
use state::{DummyContext, UserAgentEvents, UserAgentStateMachine};
@@ -47,6 +47,7 @@ impl Error {
}
pub struct PendingClientApproval {
pubkey: authn::PublicKey,
controller: ActorRef<ClientApprovalController>,
}
@@ -55,7 +56,7 @@ pub struct UserAgentSession {
state: UserAgentStateMachine<DummyContext>,
sender: Box<dyn Sender<OutOfBand>>,
pending_client_approvals: HashMap<VerifyingKey, PendingClientApproval>,
pending_client_approvals: HashMap<Vec<u8>, PendingClientApproval>,
}
pub mod connection;
@@ -118,8 +119,13 @@ impl UserAgentSession {
return;
}
self.pending_client_approvals
.insert(client.pubkey, PendingClientApproval { controller });
self.pending_client_approvals.insert(
client.pubkey.to_bytes(),
PendingClientApproval {
pubkey: client.pubkey,
controller,
},
);
}
}
@@ -158,14 +164,18 @@ impl Actor for UserAgentSession {
let cancelled_pubkey = self
.pending_client_approvals
.iter()
.find_map(|(k, v)| (v.controller.id() == id).then_some(*k));
.find_map(|(k, v)| (v.controller.id() == id).then_some(k.clone()));
if let Some(pubkey) = cancelled_pubkey {
self.pending_client_approvals.remove(&pubkey);
if let Some(pubkey_bytes) = cancelled_pubkey {
let Some(approval) = self.pending_client_approvals.remove(&pubkey_bytes) else {
return Ok(std::ops::ControlFlow::Continue(()));
};
if let Err(e) = self
.sender
.send(OutOfBand::ClientConnectionCancel { pubkey })
.send(OutOfBand::ClientConnectionCancel {
pubkey: approval.pubkey,
})
.await
{
error!(

View File

@@ -1,6 +1,10 @@
use std::sync::Mutex;
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use arbiter_crypto::{
authn,
safecell::{SafeCell, SafeCellHandle as _},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
use diesel_async::{AsyncConnection, RunQueryDsl};
@@ -13,25 +17,21 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
use crate::actors::keyholder::KeyHolderState;
use crate::actors::user_agent::session::Error;
use crate::actors::{
evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
UseragentCreateGrant, UseragentListGrants,
},
keyholder::{self, Bootstrap, TryUnseal},
user_agent::session::{
UserAgentSession,
state::{UnsealContext, UserAgentEvents, UserAgentStates},
},
};
use crate::db::models::{
EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,
};
use crate::evm::policies::{Grant, SpecificGrant};
use crate::safe_cell::SafeCell;
use crate::{
actors::{
evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
UseragentCreateGrant, UseragentDeleteGrant, UseragentListGrants,
},
keyholder::{self, Bootstrap, TryUnseal},
user_agent::session::{
UserAgentSession,
state::{UnsealContext, UserAgentEvents, UserAgentStates},
},
},
safe_cell::SafeCellHandle as _,
};
impl UserAgentSession {
fn take_unseal_secret(&mut self) -> Result<(EphemeralSecret, PublicKey), Error> {
@@ -361,19 +361,21 @@ impl UserAgentSession {
&mut self,
grant_id: i32,
) -> Result<(), GrantMutationError> {
match self
.props
.actors
.evm
.ask(UseragentDeleteGrant { grant_id })
.await
{
Ok(()) => Ok(()),
Err(err) => {
error!(?err, "EVM grant delete failed");
Err(GrantMutationError::Internal)
}
}
// match self
// .props
// .actors
// .evm
// .ask(UseragentDeleteGrant { grant_id })
// .await
// {
// Ok(()) => Ok(()),
// Err(err) => {
// error!(?err, "EVM grant delete failed");
// Err(GrantMutationError::Internal)
// }
// }
let _ = grant_id;
todo!()
}
#[message]
@@ -473,10 +475,10 @@ impl UserAgentSession {
pub(crate) async fn handle_new_client_approve(
&mut self,
approved: bool,
pubkey: ed25519_dalek::VerifyingKey,
pubkey: authn::PublicKey,
ctx: &mut Context<Self, Result<(), Error>>,
) -> Result<(), Error> {
let pending_approval = match self.pending_client_approvals.remove(&pubkey) {
let pending_approval = match self.pending_client_approvals.remove(&pubkey.to_bytes()) {
Some(approval) => approval,
None => {
error!("Received client connection response for unknown client");

View File

@@ -59,10 +59,8 @@ mod tests {
use std::ops::Deref as _;
use super::*;
use crate::{
crypto::derive_key,
safe_cell::{SafeCell, SafeCellHandle as _},
};
use crate::crypto::derive_key;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
#[test]
pub fn derive_seal_key_deterministic() {

View File

@@ -1,6 +1,5 @@
use crate::{
actors::keyholder, crypto::integrity::hashing::Hashable, safe_cell::SafeCellHandle as _,
};
use crate::{actors::keyholder, crypto::integrity::hashing::Hashable};
use arbiter_crypto::safecell::SafeCellHandle as _;
use hmac::{Hmac, Mac as _};
use sha2::Sha256;
@@ -127,7 +126,7 @@ pub async fn sign_entity<E: Integrable>(
insert_into(integrity_envelope::table)
.values(NewIntegrityEnvelope {
entity_kind: E::KIND.to_owned(),
entity_id: entity_id,
entity_id,
payload_version: E::VERSION,
key_version,
mac: mac.to_vec(),
@@ -204,19 +203,19 @@ mod tests {
use diesel::{ExpressionMethods as _, QueryDsl};
use diesel_async::RunQueryDsl;
use kameo::{actor::ActorRef, prelude::Spawn};
use rand::seq::SliceRandom;
use sha2::Digest;
use proptest::prelude::*;
use crate::{
actors::keyholder::{Bootstrap, KeyHolder},
db::{self, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use super::{Error, Integrable, sign_entity, verify_entity};
use super::{hashing::Hashable, payload_hash};
use super::hashing::Hashable;
#[derive(Clone)]
struct DummyEntity {

View File

@@ -62,10 +62,10 @@ impl<T: Hashable> Hashable for Option<T> {
fn hash<H: Digest>(&self, hasher: &mut H) {
match self {
Some(value) => {
hasher.update(&[1]);
hasher.update([1]);
value.hash(hasher);
}
None => hasher.update(&[0]),
None => hasher.update([0]),
}
}
}
@@ -96,12 +96,12 @@ impl Hashable for alloy::primitives::U256 {
impl Hashable for chrono::Duration {
fn hash<H: Digest>(&self, hasher: &mut H) {
hasher.update(&self.num_seconds().to_be_bytes());
hasher.update(self.num_seconds().to_be_bytes());
}
}
impl Hashable for chrono::DateTime<chrono::Utc> {
fn hash<H: Digest>(&self, hasher: &mut H) {
hasher.update(&self.timestamp_millis().to_be_bytes());
hasher.update(self.timestamp_millis().to_be_bytes());
}
}

View File

@@ -10,7 +10,7 @@ use rand::{
rngs::{StdRng, SysRng},
};
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
pub mod encryption;
pub mod integrity;
@@ -141,7 +141,7 @@ mod tests {
derive_key,
encryption::v1::{Nonce, generate_salt},
};
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
#[test]
pub fn encrypt_decrypt() {

View File

@@ -72,40 +72,6 @@ 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, strum::FromRepr)]
#[diesel(sql_type = Integer)]
#[repr(i32)]
pub enum KeyType {
Ed25519 = 1,
EcdsaSecp256k1 = 2,
Rsa = 3,
}
impl ToSql<Integer, Sqlite> 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<Integer, Sqlite> for KeyType {
fn from_sql(
mut bytes: <Sqlite as diesel::backend::Backend>::RawValue<'_>,
) -> diesel::deserialize::Result<Self> {
let Some(SqliteType::Long) = bytes.value_type() else {
return Err("Expected Integer for KeyType".into());
};
let discriminant = bytes.read_long();
KeyType::from_repr(discriminant as i32)
.ok_or_else(|| format!("Unknown KeyType discriminant: {discriminant}").into())
}
}
}
pub use types::*;
@@ -244,7 +210,6 @@ pub struct UseragentClient {
pub public_key: Vec<u8>,
pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp,
pub key_type: KeyType,
}
#[derive(Models, Queryable, Debug, Insertable, Selectable)]

View File

@@ -1,12 +1,12 @@
use std::sync::Mutex;
use crate::safe_cell::{SafeCell, SafeCellHandle as _};
use alloy::{
consensus::SignableTransaction,
network::{TxSigner, TxSignerSync},
primitives::{Address, B256, ChainId, Signature},
signers::{Error, Result, Signer, SignerSync, utils::secret_key_to_address},
};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use async_trait::async_trait;
use k256::ecdsa::{self, RecoveryId, SigningKey, signature::hazmat::PrehashSigner};

View File

@@ -1,3 +1,4 @@
use arbiter_crypto::authn;
use arbiter_proto::{
ClientMetadata,
proto::{
@@ -45,7 +46,7 @@ impl<'a> AuthTransportAdapter<'a> {
match response {
auth::Outbound::AuthChallenge { pubkey, nonce } => {
AuthResponsePayload::Challenge(ProtoAuthChallenge {
pubkey: pubkey.to_bytes().to_vec(),
pubkey: pubkey.to_bytes(),
nonce,
})
}
@@ -160,11 +161,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
.await;
return None;
};
let Ok(pubkey) = <[u8; 32]>::try_from(pubkey) else {
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
return None;
};
let Ok(pubkey) = ed25519_dalek::VerifyingKey::from_bytes(&pubkey) else {
let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else {
let _ = self.send_auth_result(ProtoAuthResult::InvalidKey).await;
return None;
};
@@ -174,7 +171,7 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
})
}
AuthRequestPayload::ChallengeSolution(ProtoAuthChallengeSolution { signature }) => {
let Ok(signature) = ed25519_dalek::Signature::try_from(signature.as_slice()) else {
let Ok(signature) = authn::Signature::try_from(signature.as_slice()) else {
let _ = self
.send_auth_result(ProtoAuthResult::InvalidSignature)
.await;

View File

@@ -1,3 +1,4 @@
use arbiter_crypto::authn;
use arbiter_proto::{
proto::user_agent::{
UserAgentRequest, UserAgentResponse,
@@ -5,8 +6,7 @@ use arbiter_proto::{
self as proto_auth, AuthChallenge as ProtoAuthChallenge,
AuthChallengeRequest as ProtoAuthChallengeRequest,
AuthChallengeSolution as ProtoAuthChallengeSolution, AuthResult as ProtoAuthResult,
KeyType as ProtoKeyType, request::Payload as AuthRequestPayload,
response::Payload as AuthResponsePayload,
request::Payload as AuthRequestPayload, response::Payload as AuthResponsePayload,
},
user_agent_request::Payload as UserAgentRequestPayload,
user_agent_response::Payload as UserAgentResponsePayload,
@@ -18,8 +18,7 @@ use tonic::Status;
use tracing::warn;
use crate::{
actors::user_agent::{AuthPublicKey, UserAgentConnection, auth},
db::models::KeyType,
actors::user_agent::{UserAgentConnection, auth},
grpc::request_tracker::RequestTracker,
};
@@ -141,28 +140,9 @@ impl Receiver<auth::Inbound> for AuthTransportAdapter<'_> {
AuthRequestPayload::ChallengeRequest(ProtoAuthChallengeRequest {
pubkey,
bootstrap_token,
key_type,
key_type: _,
}) => {
let Ok(key_type) = ProtoKeyType::try_from(key_type) else {
warn!(
event = "received request with invalid key type",
"grpc.useragent.auth_adapter"
);
return None;
};
let key_type = match key_type {
ProtoKeyType::Ed25519 => KeyType::Ed25519,
ProtoKeyType::EcdsaSecp256k1 => KeyType::EcdsaSecp256k1,
ProtoKeyType::Rsa => KeyType::Rsa,
ProtoKeyType::Unspecified => {
warn!(
event = "received request with unspecified key type",
"grpc.useragent.auth_adapter"
);
return None;
}
};
let Ok(pubkey) = AuthPublicKey::try_from((key_type, pubkey)) else {
let Ok(pubkey) = authn::PublicKey::try_from(pubkey.as_slice()) else {
warn!(
event = "received request with invalid public key",
"grpc.useragent.auth_adapter"
@@ -188,7 +168,7 @@ pub async fn start(
conn: &mut UserAgentConnection,
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &mut RequestTracker,
) -> Result<AuthPublicKey, auth::Error> {
) -> Result<authn::PublicKey, auth::Error> {
let transport = AuthTransportAdapter::new(bi, request_tracker);
auth::authenticate(conn, transport).await
}

View File

@@ -1,3 +1,4 @@
use arbiter_crypto::authn;
use arbiter_proto::proto::{
shared::ClientInfo as ProtoClientMetadata,
user_agent::{
@@ -41,7 +42,7 @@ pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload {
match oob {
OutOfBand::ClientConnectionRequest { profile } => wrap_sdk_client_response(
SdkClientResponsePayload::ConnectionRequest(ProtoSdkClientConnectionRequest {
pubkey: profile.pubkey.to_bytes().to_vec(),
pubkey: profile.pubkey.to_bytes(),
info: Some(ProtoClientMetadata {
name: profile.metadata.name,
description: profile.metadata.description,
@@ -51,7 +52,7 @@ pub(super) fn out_of_band_payload(oob: OutOfBand) -> UserAgentResponsePayload {
),
OutOfBand::ClientConnectionCancel { pubkey } => wrap_sdk_client_response(
SdkClientResponsePayload::ConnectionCancel(ProtoSdkClientConnectionCancel {
pubkey: pubkey.to_bytes().to_vec(),
pubkey: pubkey.to_bytes(),
}),
),
}
@@ -89,10 +90,8 @@ async fn handle_connection_response(
actor: &ActorRef<UserAgentSession>,
resp: ProtoSdkClientConnectionResponse,
) -> Result<Option<UserAgentResponsePayload>, Status> {
let pubkey_bytes = <[u8; 32]>::try_from(resp.pubkey)
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key length"))?;
let pubkey = ed25519_dalek::VerifyingKey::from_bytes(&pubkey_bytes)
.map_err(|_| Status::invalid_argument("Invalid Ed25519 public key"))?;
let pubkey = authn::PublicKey::try_from(resp.pubkey.as_slice())
.map_err(|_| Status::invalid_argument("Invalid ML-DSA public key"))?;
actor
.ask(HandleNewClientApprove {
@@ -117,7 +116,7 @@ async fn handle_list(
.into_iter()
.map(|(client, metadata)| ProtoSdkClientEntry {
id: client.id,
pubkey: client.public_key,
pubkey: client.public_key.to_vec(),
info: Some(ProtoClientMetadata {
name: metadata.name,
description: metadata.description,

View File

@@ -7,7 +7,6 @@ pub mod crypto;
pub mod db;
pub mod evm;
pub mod grpc;
pub mod safe_cell;
pub mod utils;
pub struct Server {

View File

@@ -1,3 +1,7 @@
use arbiter_crypto::{
authn::{self, CLIENT_CONTEXT, format_challenge},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::ClientMetadata;
use arbiter_proto::transport::{Receiver, Sender};
use arbiter_server::{
@@ -8,11 +12,10 @@ use arbiter_server::{
},
crypto::integrity,
db::{self, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use diesel::{ExpressionMethods as _, NullableExpressionMethods as _, QueryDsl as _, insert_into};
use diesel_async::RunQueryDsl;
use ed25519_dalek::Signer as _;
use ml_dsa::{KeyGen, MlDsa87, SigningKey, VerifyingKey, signature::Keypair as _};
use super::common::ChannelTransport;
@@ -27,7 +30,7 @@ fn metadata(name: &str, description: Option<&str>, version: Option<&str>) -> Cli
async fn insert_registered_client(
db: &db::DatabasePool,
actors: &GlobalActors,
pubkey: ed25519_dalek::VerifyingKey,
pubkey: VerifyingKey<MlDsa87>,
metadata: &ClientMetadata,
) {
use arbiter_server::db::schema::{client_metadata, program_client};
@@ -45,7 +48,7 @@ async fn insert_registered_client(
.unwrap();
let client_id: i32 = insert_into(program_client::table)
.values((
program_client::public_key.eq(pubkey.to_bytes().to_vec()),
program_client::public_key.eq(pubkey.encode().to_vec()),
program_client::metadata_id.eq(metadata_id),
))
.returning(program_client::id)
@@ -56,18 +59,33 @@ async fn insert_registered_client(
integrity::sign_entity(
&mut conn,
&actors.key_holder,
&ClientCredentials { pubkey, nonce: 1 },
&ClientCredentials {
pubkey: pubkey.into(),
nonce: 1,
},
client_id,
)
.await
.unwrap();
}
fn sign_client_challenge(
key: &SigningKey<MlDsa87>,
nonce: i32,
pubkey: &authn::PublicKey,
) -> authn::Signature {
let challenge = format_challenge(nonce, &pubkey.to_bytes());
key.signing_key()
.sign_deterministic(&challenge, CLIENT_CONTEXT)
.unwrap()
.into()
}
async fn insert_bootstrap_sentinel_useragent(db: &db::DatabasePool) {
let mut conn = db.get().await.unwrap();
let sentinel_key = ed25519_dalek::SigningKey::generate(&mut rand::rng())
let sentinel_key = MlDsa87::key_gen(&mut rand::rng())
.verifying_key()
.to_bytes()
.encode()
.to_vec();
insert_into(schema::useragent_client::table)
@@ -107,11 +125,11 @@ pub async fn test_unregistered_pubkey_rejected() {
connect_client(props, &mut server_transport).await;
});
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let new_key = MlDsa87::key_gen(&mut rand::rng());
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: new_key.verifying_key(),
pubkey: new_key.verifying_key().into(),
metadata: metadata("client", Some("desc"), Some("1.0.0")),
})
.await
@@ -127,7 +145,7 @@ pub async fn test_challenge_auth() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let new_key = MlDsa87::key_gen(&mut rand::rng());
insert_registered_client(
&db,
@@ -147,7 +165,7 @@ pub async fn test_challenge_auth() {
// Send challenge request
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: new_key.verifying_key(),
pubkey: new_key.verifying_key().into(),
metadata: metadata("client", Some("desc"), Some("1.0.0")),
})
.await
@@ -167,8 +185,7 @@ pub async fn test_challenge_auth() {
};
// Sign the challenge and send solution
let formatted_challenge = arbiter_proto::format_challenge(challenge.1, challenge.0.as_bytes());
let signature = new_key.sign(&formatted_challenge);
let signature = sign_client_challenge(&new_key, challenge.1, &challenge.0);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
@@ -194,7 +211,7 @@ pub async fn test_challenge_auth() {
pub async fn test_metadata_unchanged_does_not_append_history() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let new_key = MlDsa87::key_gen(&mut rand::rng());
let requested = metadata("client", Some("desc"), Some("1.0.0"));
insert_registered_client(&db, &actors, new_key.verifying_key(), &requested).await;
@@ -209,7 +226,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: new_key.verifying_key(),
pubkey: new_key.verifying_key().into(),
metadata: requested,
})
.await
@@ -220,7 +237,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
other => panic!("Expected AuthChallenge, got {other:?}"),
};
let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes()));
let signature = sign_client_challenge(&new_key, nonce, &pubkey);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
@@ -251,7 +268,7 @@ pub async fn test_metadata_unchanged_does_not_append_history() {
pub async fn test_metadata_change_appends_history_and_repoints_binding() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let new_key = MlDsa87::key_gen(&mut rand::rng());
insert_registered_client(
&db,
@@ -271,7 +288,7 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() {
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: new_key.verifying_key(),
pubkey: new_key.verifying_key().into(),
metadata: metadata("client", Some("new"), Some("2.0.0")),
})
.await
@@ -282,7 +299,7 @@ pub async fn test_metadata_change_appends_history_and_repoints_binding() {
auth::Outbound::AuthChallenge { pubkey, nonce } => (pubkey, nonce),
other => panic!("Expected AuthChallenge, got {other:?}"),
};
let signature = new_key.sign(&arbiter_proto::format_challenge(nonce, pubkey.as_bytes()));
let signature = sign_client_challenge(&new_key, nonce, &pubkey);
test_transport
.send(auth::Inbound::AuthChallengeSolution { signature })
.await
@@ -339,7 +356,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
let db = db::create_test_pool().await;
let actors = spawn_test_actors(&db).await;
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let new_key = MlDsa87::key_gen(&mut rand::rng());
let requested = metadata("client", Some("desc"), Some("1.0.0"));
{
@@ -357,7 +374,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
.unwrap();
insert_into(program_client::table)
.values((
program_client::public_key.eq(new_key.verifying_key().to_bytes().to_vec()),
program_client::public_key.eq(new_key.verifying_key().encode().to_vec()),
program_client::metadata_id.eq(metadata_id),
))
.execute(&mut conn)
@@ -374,7 +391,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch() {
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: new_key.verifying_key(),
pubkey: new_key.verifying_key().into(),
metadata: requested,
})
.await

View File

@@ -1,9 +1,10 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_proto::transport::{Bi, Error, Receiver, Sender};
use arbiter_server::{
actors::keyholder::KeyHolder,
db::{self, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use async_trait::async_trait;
use diesel::QueryDsl;
use diesel_async::RunQueryDsl;

View File

@@ -1,10 +1,11 @@
use std::collections::{HashMap, HashSet};
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::keyholder::{CreateNew, Error, KeyHolder},
db::{self, models, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::sql_query};
use diesel_async::RunQueryDsl;
use kameo::actor::{ActorRef, Spawn as _};

View File

@@ -1,9 +1,10 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::keyholder::{Error, KeyHolder},
crypto::encryption::v1::{Nonce, ROOT_KEY_TAG},
db::{self, models, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use diesel::{QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;

View File

@@ -1,11 +1,12 @@
use std::collections::HashSet;
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::keyholder::Error,
crypto::encryption::v1::Nonce,
db::{self, models, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use diesel::{ExpressionMethods as _, QueryDsl, SelectableHelper, dsl::update};
use diesel_async::RunQueryDsl;

View File

@@ -1,21 +1,37 @@
use arbiter_crypto::{
authn::{self, USERAGENT_CONTEXT, format_challenge},
safecell::{SafeCell, SafeCellHandle as _},
};
use arbiter_proto::transport::{Receiver, Sender};
use arbiter_server::{
actors::{
GlobalActors,
bootstrap::GetToken,
keyholder::Bootstrap,
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth},
user_agent::{UserAgentConnection, UserAgentCredentials, auth},
},
crypto::integrity,
db::{self, schema},
safe_cell::{SafeCell, SafeCellHandle as _},
};
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
use diesel_async::RunQueryDsl;
use ed25519_dalek::Signer as _;
use ml_dsa::{KeyGen, MlDsa87, SigningKey, signature::Keypair as _};
use super::common::ChannelTransport;
fn sign_useragent_challenge(
key: &SigningKey<MlDsa87>,
nonce: i32,
pubkey_bytes: &[u8],
) -> authn::Signature {
let challenge = format_challenge(nonce, pubkey_bytes);
key.signing_key()
.sign_deterministic(&challenge, USERAGENT_CONTEXT)
.unwrap()
.into()
}
#[tokio::test]
#[test_log::test]
pub async fn test_bootstrap_token_auth() {
@@ -37,10 +53,10 @@ pub async fn test_bootstrap_token_auth() {
auth::authenticate(&mut props, server_transport).await
});
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let new_key = MlDsa87::key_gen(&mut rand::rng());
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
pubkey: new_key.verifying_key().into(),
bootstrap_token: Some(token),
})
.await
@@ -63,7 +79,7 @@ pub async fn test_bootstrap_token_auth() {
.first::<Vec<u8>>(&mut conn)
.await
.unwrap();
assert_eq!(stored_pubkey, new_key.verifying_key().to_bytes().to_vec());
assert_eq!(stored_pubkey, new_key.verifying_key().encode().to_vec());
}
#[tokio::test]
@@ -79,10 +95,10 @@ pub async fn test_bootstrap_invalid_token_auth() {
auth::authenticate(&mut props, server_transport).await
});
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let new_key = MlDsa87::key_gen(&mut rand::rng());
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
pubkey: new_key.verifying_key().into(),
bootstrap_token: Some("invalid_token".to_string()),
})
.await
@@ -115,8 +131,8 @@ pub async fn test_challenge_auth() {
.await
.unwrap();
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
let new_key = MlDsa87::key_gen(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().encode().to_vec();
{
let mut conn = db.get().await.unwrap();
@@ -133,7 +149,7 @@ pub async fn test_challenge_auth() {
&mut conn,
&actors.key_holder,
&UserAgentCredentials {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
pubkey: new_key.verifying_key().into(),
nonce: 1,
},
id,
@@ -151,7 +167,7 @@ pub async fn test_challenge_auth() {
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
pubkey: new_key.verifying_key().into(),
bootstrap_token: None,
})
.await
@@ -169,12 +185,11 @@ pub async fn test_challenge_auth() {
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
};
let formatted_challenge = arbiter_proto::format_challenge(challenge, &pubkey_bytes);
let signature = new_key.sign(&formatted_challenge);
let signature = sign_useragent_challenge(&new_key, challenge, &pubkey_bytes);
test_transport
.send(auth::Inbound::AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
signature: signature.to_bytes(),
})
.await
.unwrap();
@@ -205,8 +220,8 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed()
.await
.unwrap();
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
let new_key = MlDsa87::key_gen(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().encode().to_vec();
{
let mut conn = db.get().await.unwrap();
@@ -229,7 +244,7 @@ pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed()
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
pubkey: new_key.verifying_key().into(),
bootstrap_token: None,
})
.await
@@ -254,8 +269,8 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
.await
.unwrap();
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
let new_key = MlDsa87::key_gen(&mut rand::rng());
let pubkey_bytes = new_key.verifying_key().encode().to_vec();
{
let mut conn = db.get().await.unwrap();
@@ -272,7 +287,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
&mut conn,
&actors.key_holder,
&UserAgentCredentials {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
pubkey: new_key.verifying_key().into(),
nonce: 1,
},
id,
@@ -290,7 +305,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
test_transport
.send(auth::Inbound::AuthChallengeRequest {
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
pubkey: new_key.verifying_key().into(),
bootstrap_token: None,
})
.await
@@ -308,12 +323,11 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
};
let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes);
let signature = new_key.sign(&wrong_challenge);
let signature = sign_useragent_challenge(&new_key, challenge + 1, &pubkey_bytes);
test_transport
.send(auth::Inbound::AuthChallengeSolution {
signature: signature.to_bytes().to_vec(),
signature: signature.to_bytes(),
})
.await
.unwrap();

View File

@@ -1,3 +1,4 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_server::{
actors::{
GlobalActors,
@@ -8,11 +9,9 @@ use arbiter_server::{
},
},
db,
safe_cell::{SafeCell, SafeCellHandle as _},
};
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into};
use diesel_async::RunQueryDsl;
use kameo::actor::Spawn as _;
use x25519_dalek::{EphemeralSecret, PublicKey};