refactor(server::{useragent::auth, client::auth}): use random based + timestamp nonce instead of monotonic counter in database

This commit is contained in:
Skipper
2026-04-17 16:14:45 +02:00
parent 51e6571d80
commit 0e09afda5d
24 changed files with 320 additions and 466 deletions

View File

@@ -10,8 +10,8 @@ message AuthChallengeRequest {
} }
message AuthChallenge { message AuthChallenge {
bytes pubkey = 1; uint64 timestamp_nanos = 1;
int32 nonce = 2; bytes random = 2;
} }
message AuthChallengeSolution { message AuthChallengeSolution {

View File

@@ -8,7 +8,8 @@ message AuthChallengeRequest {
} }
message AuthChallenge { message AuthChallenge {
int32 nonce = 1; uint64 timestamp_nanos = 1;
bytes random = 2;
} }
message AuthChallengeSolution { message AuthChallengeSolution {

3
server/Cargo.lock generated
View File

@@ -683,6 +683,7 @@ dependencies = [
"arbiter-crypto", "arbiter-crypto",
"arbiter-proto", "arbiter-proto",
"async-trait", "async-trait",
"chrono",
"http", "http",
"rand 0.10.1", "rand 0.10.1",
"rustls-webpki", "rustls-webpki",
@@ -696,7 +697,7 @@ dependencies = [
name = "arbiter-crypto" name = "arbiter-crypto"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"base64", "chrono",
"memsafe", "memsafe",
"ml-dsa", "ml-dsa",
"rand 0.10.1", "rand 0.10.1",

View File

@@ -24,3 +24,4 @@ http = "1.4.0"
rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] } rustls-webpki = { version = "0.103.10", features = ["aws-lc-rs"] }
async-trait.workspace = true async-trait.workspace = true
rand.workspace = true rand.workspace = true
chrono.workspace = true

View File

@@ -1,4 +1,4 @@
use arbiter_crypto::authn::{CLIENT_CONTEXT, SigningKey, format_challenge}; use arbiter_crypto::authn::{self, CLIENT_CONTEXT, SigningKey};
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, ClientMetadata,
proto::{ proto::{
@@ -15,6 +15,7 @@ use arbiter_proto::{
shared::ClientInfo as ProtoClientInfo, shared::ClientInfo as ProtoClientInfo,
}, },
}; };
use chrono::DateTime;
use crate::{ use crate::{
storage::StorageError, storage::StorageError,
@@ -23,6 +24,8 @@ use crate::{
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum AuthError { pub enum AuthError {
#[error("Server sent invalid auth challenge")]
InvalidChallenge,
#[error("Auth challenge was not returned by server")] #[error("Auth challenge was not returned by server")]
MissingAuthChallenge, MissingAuthChallenge,
@@ -98,7 +101,15 @@ async fn send_auth_challenge_solution(
key: &SigningKey, key: &SigningKey,
challenge: AuthChallenge, challenge: AuthChallenge,
) -> std::result::Result<(), AuthError> { ) -> std::result::Result<(), AuthError> {
let challenge_payload = format_challenge(challenge.nonce, &challenge.pubkey); let timestamp = DateTime::from_timestamp_nanos(challenge.timestamp_nanos as i64);
let challenge = authn::AuthChallenge {
nonce: *challenge
.random
.as_array()
.ok_or(AuthError::InvalidChallenge)?,
timestamp,
};
let challenge_payload: Vec<u8> = challenge.format();
let signature = key let signature = key
.sign_message(&challenge_payload, CLIENT_CONTEXT) .sign_message(&challenge_payload, CLIENT_CONTEXT)
.map_err(|_| AuthError::UnexpectedAuthResponse)? .map_err(|_| AuthError::UnexpectedAuthResponse)?

View File

@@ -6,14 +6,14 @@ edition = "2024"
[dependencies] [dependencies]
ml-dsa = {workspace = true, optional = true } ml-dsa = {workspace = true, optional = true }
rand = {workspace = true, optional = true} rand = {workspace = true, optional = true}
base64 = {workspace = true, optional = true }
memsafe = {version = "0.4.0", optional = true} memsafe = {version = "0.4.0", optional = true}
x-wing = { version = "0.1.0-rc.0", features = ["zeroize"] } x-wing = { version = "0.1.0-rc.0", features = ["zeroize"] }
chrono.workspace = true
[lints] [lints]
workspace = true workspace = true
[features] [features]
default = ["authn", "safecell"] default = ["authn", "safecell"]
authn = ["dep:ml-dsa", "dep:rand", "dep:base64"] authn = ["dep:ml-dsa", "dep:rand"]
safecell = ["dep:memsafe"] safecell = ["dep:memsafe"]

View File

@@ -1,17 +1,48 @@
use std::hash::Hash; use std::hash::Hash;
use base64::{Engine as _, prelude::BASE64_STANDARD}; use chrono::{DateTime, Utc};
use ml_dsa::{ use ml_dsa::{
EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature, EncodedVerifyingKey, Error, KeyGen, MlDsa87, Seed, Signature as MlDsaSignature,
SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _, SigningKey as MlDsaSigningKey, VerifyingKey as MlDsaVerifyingKey, signature::Keypair as _,
}; };
use rand::RngExt;
pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client"; pub static CLIENT_CONTEXT: &[u8] = b"arbiter_client";
pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent"; pub static USERAGENT_CONTEXT: &[u8] = b"arbiter_user_agent";
pub fn format_challenge(nonce: i32, pubkey: &[u8]) -> Vec<u8> { const NONCE_SIZE: usize = 32;
let concat_form = format!("{}:{}", nonce, BASE64_STANDARD.encode(pubkey));
concat_form.into_bytes() #[derive(Debug, Clone)]
pub struct AuthChallenge {
pub nonce: [u8; NONCE_SIZE],
pub timestamp: DateTime<Utc>,
}
impl AuthChallenge {
pub fn generate(rng: &mut impl rand::CryptoRng) -> Self {
let timestamp = Utc::now();
let nonce = {
let mut array = [0; NONCE_SIZE];
rng.fill(&mut array);
array
};
Self { nonce, timestamp }
}
pub fn format(&self) -> Vec<u8> {
{
let mut buffer = Vec::from(self.nonce);
let stamp = self
.timestamp
.timestamp_nanos_opt()
.expect("We would be long dead by the time this triggers :)");
buffer.extend_from_slice(stamp.to_be_bytes().as_slice());
buffer
}
}
} }
pub type KeyParams = MlDsa87; pub type KeyParams = MlDsa87;
@@ -36,12 +67,10 @@ impl PublicKey {
self.0.encode().to_vec() self.0.encode().to_vec()
} }
pub fn verify(&self, nonce: i32, context: &[u8], signature: &Signature) -> bool { pub fn verify(&self, challenge: &AuthChallenge, context: &[u8], signature: &Signature) -> bool {
self.0.verify_with_context( let challenge = challenge.format();
&format_challenge(nonce, &self.to_bytes()), self.0
context, .verify_with_context(&challenge, context, &signature.0)
&signature.0,
)
} }
} }
@@ -75,11 +104,14 @@ impl SigningKey {
.map(Into::into) .map(Into::into)
} }
pub fn sign_challenge(&self, nonce: i32, context: &[u8]) -> Result<Signature, Error> { pub fn sign_challenge(
self.sign_message( &self,
&format_challenge(nonce, &self.public_key().to_bytes()), challenge: &AuthChallenge,
context, context: &[u8],
) ) -> Result<Signature, Error> {
let challenge = challenge.format();
self.sign_message(&challenge, context)
} }
} }
@@ -140,6 +172,8 @@ impl TryFrom<&'_ [u8]> for Signature {
mod tests { mod tests {
use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _}; use ml_dsa::{KeyGen, MlDsa87, signature::Keypair as _};
use crate::authn::AuthChallenge;
use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT}; use super::{CLIENT_CONTEXT, PublicKey, Signature, SigningKey, USERAGENT_CONTEXT};
#[test] #[test]
@@ -169,13 +203,13 @@ mod tests {
fn challenge_verification_uses_context_and_canonical_key_bytes() { fn challenge_verification_uses_context_and_canonical_key_bytes() {
let key = SigningKey::generate(); let key = SigningKey::generate();
let public_key = key.public_key(); let public_key = key.public_key();
let nonce = 17; let challenge = AuthChallenge::generate(&mut rand::rng());
let signature = key let signature = key
.sign_challenge(nonce, CLIENT_CONTEXT) .sign_challenge(&challenge, CLIENT_CONTEXT)
.expect("signature should be created"); .expect("signature should be created");
assert!(public_key.verify(nonce, CLIENT_CONTEXT, &signature)); assert!(public_key.verify(&challenge, CLIENT_CONTEXT, &signature));
assert!(!public_key.verify(nonce, USERAGENT_CONTEXT, &signature)); assert!(!public_key.verify(&challenge, USERAGENT_CONTEXT, &signature));
} }
#[test] #[test]
@@ -185,10 +219,16 @@ mod tests {
assert_eq!(restored.public_key(), original.public_key()); assert_eq!(restored.public_key(), original.public_key());
let challenge = AuthChallenge::generate(&mut rand::rng());
let signature = restored let signature = restored
.sign_challenge(9, CLIENT_CONTEXT) .sign_challenge(&challenge, CLIENT_CONTEXT)
.expect("signature should be created"); .expect("signature should be created");
assert!(restored.public_key().verify(9, CLIENT_CONTEXT, &signature)); assert!(
restored
.public_key()
.verify(&challenge, CLIENT_CONTEXT, &signature)
);
} }
} }

View File

@@ -45,9 +45,7 @@ insert into arbiter_settings (id) values (1) on conflict do nothing;
create table if not exists useragent_client ( create table if not exists useragent_client (
id integer not null primary key, id integer not null primary key,
nonce integer not null default(1), -- used for auth challenge
public_key blob not null, public_key blob not null,
key_type integer not null default(1),
created_at integer not null default(unixepoch ('now')), created_at integer not null default(unixepoch ('now')),
updated_at integer not null default(unixepoch ('now')) updated_at integer not null default(unixepoch ('now'))
) STRICT; ) STRICT;

View File

@@ -195,7 +195,6 @@ pub struct ProgramClientMetadataHistory {
#[diesel(table_name = schema::program_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::program_client, check_for_backend(Sqlite))]
pub struct ProgramClient { pub struct ProgramClient {
pub id: i32, pub id: i32,
pub nonce: i32,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub metadata_id: i32, pub metadata_id: i32,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
@@ -206,7 +205,6 @@ pub struct ProgramClient {
#[diesel(table_name = schema::useragent_client, check_for_backend(Sqlite))] #[diesel(table_name = schema::useragent_client, check_for_backend(Sqlite))]
pub struct UseragentClient { pub struct UseragentClient {
pub id: i32, pub id: i32,
pub nonce: i32,
pub public_key: Vec<u8>, pub public_key: Vec<u8>,
pub created_at: SqliteTimestamp, pub created_at: SqliteTimestamp,
pub updated_at: SqliteTimestamp, pub updated_at: SqliteTimestamp,

View File

@@ -155,7 +155,6 @@ diesel::table! {
diesel::table! { diesel::table! {
program_client (id) { program_client (id) {
id -> Integer, id -> Integer,
nonce -> Integer,
public_key -> Binary, public_key -> Binary,
metadata_id -> Integer, metadata_id -> Integer,
created_at -> Integer, created_at -> Integer,
@@ -189,7 +188,6 @@ diesel::table! {
diesel::table! { diesel::table! {
useragent_client (id) { useragent_client (id) {
id -> Integer, id -> Integer,
nonce -> Integer,
public_key -> Binary, public_key -> Binary,
key_type -> Integer, key_type -> Integer,
created_at -> Integer, created_at -> Integer,

View File

@@ -44,10 +44,14 @@ impl<'a> AuthTransportAdapter<'a> {
fn response_to_proto(response: auth::Outbound) -> AuthResponsePayload { fn response_to_proto(response: auth::Outbound) -> AuthResponsePayload {
match response { match response {
auth::Outbound::AuthChallenge { pubkey, nonce } => { auth::Outbound::AuthChallenge { challenge } => {
AuthResponsePayload::Challenge(ProtoAuthChallenge { AuthResponsePayload::Challenge(ProtoAuthChallenge {
pubkey: pubkey.to_bytes(), timestamp_nanos: challenge
nonce, .timestamp
.timestamp_nanos_opt()
.expect("timestamp within range")
as u64,
random: challenge.nonce.to_vec(),
}) })
} }
auth::Outbound::AuthSuccess => { auth::Outbound::AuthSuccess => {

View File

@@ -19,7 +19,7 @@ use tracing::warn;
use crate::{ use crate::{
grpc::request_tracker::RequestTracker, grpc::request_tracker::RequestTracker,
peers::user_agent::{AuthCredentials, UserAgentConnection, auth}, peers::user_agent::{Credentials, UserAgentConnection, auth},
}; };
pub struct AuthTransportAdapter<'a> { pub struct AuthTransportAdapter<'a> {
@@ -77,8 +77,15 @@ impl Sender<Result<auth::Outbound, auth::Error>> for AuthTransportAdapter<'_> {
) -> Result<(), TransportError> { ) -> Result<(), TransportError> {
use auth::{Error, Outbound}; use auth::{Error, Outbound};
let payload = match item { let payload = match item {
Ok(Outbound::AuthChallenge { nonce }) => { Ok(Outbound::AuthChallenge { challenge }) => {
AuthResponsePayload::Challenge(ProtoAuthChallenge { nonce }) AuthResponsePayload::Challenge(ProtoAuthChallenge {
timestamp_nanos: challenge
.timestamp
.timestamp_nanos_opt()
.expect("timestamp within range")
as u64,
random: challenge.nonce.to_vec(),
})
} }
Ok(Outbound::AuthSuccess) => { Ok(Outbound::AuthSuccess) => {
AuthResponsePayload::Result(ProtoAuthResult::Success.into()) AuthResponsePayload::Result(ProtoAuthResult::Success.into())
@@ -183,7 +190,7 @@ pub async fn start(
conn: &mut UserAgentConnection, conn: &mut UserAgentConnection,
bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>, bi: &mut GrpcBi<UserAgentRequest, UserAgentResponse>,
request_tracker: &mut RequestTracker, request_tracker: &mut RequestTracker,
) -> Result<AuthCredentials, auth::Error> { ) -> Result<Credentials, auth::Error> {
let mut transport = AuthTransportAdapter::new(bi, request_tracker); let mut transport = AuthTransportAdapter::new(bi, request_tracker);
auth::authenticate(conn, &mut transport).await auth::authenticate(conn, &mut transport).await
} }

View File

@@ -1,4 +1,4 @@
use arbiter_crypto::authn::{self, CLIENT_CONTEXT}; use arbiter_crypto::authn::{self, AuthChallenge, CLIENT_CONTEXT};
use arbiter_proto::{ use arbiter_proto::{
ClientMetadata, ClientMetadata,
transport::{Bi, expect_message}, transport::{Bi, expect_message},
@@ -74,19 +74,14 @@ pub enum Inbound {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Outbound { pub enum Outbound {
AuthChallenge { AuthChallenge { challenge: AuthChallenge },
pubkey: authn::PublicKey,
nonce: i32,
},
AuthSuccess, AuthSuccess,
} }
/// Returns the current nonce and client ID for a registered client. async fn get_client_id(
/// Returns `None` if the pubkey is not registered.
async fn get_current_nonce_and_id(
db: &db::DatabasePool, db: &db::DatabasePool,
pubkey: &authn::PublicKey, pubkey: &authn::PublicKey,
) -> Result<Option<(i32, i32)>, Error> { ) -> Result<Option<i32>, Error> {
let pubkey_bytes = pubkey.to_bytes(); let pubkey_bytes = pubkey.to_bytes();
let mut conn = db.get().await.map_err(|e| { let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error"); error!(error = ?e, "Database pool error");
@@ -94,8 +89,8 @@ async fn get_current_nonce_and_id(
})?; })?;
program_client::table program_client::table
.filter(program_client::public_key.eq(&pubkey_bytes)) .filter(program_client::public_key.eq(&pubkey_bytes))
.select((program_client::id, program_client::nonce)) .select(program_client::id)
.first::<(i32, i32)>(&mut conn) .first::<i32>(&mut conn)
.await .await
.optional() .optional()
.map_err(|e| { .map_err(|e| {
@@ -114,7 +109,7 @@ async fn verify_integrity(
Error::DatabasePoolUnavailable Error::DatabasePoolUnavailable
})?; })?;
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?.ok_or_else(|| { let id = get_client_id(db, pubkey).await?.ok_or_else(|| {
error!("Client not found during integrity verification"); error!("Client not found during integrity verification");
Error::DatabaseOperationFailed Error::DatabaseOperationFailed
})?; })?;
@@ -124,7 +119,6 @@ async fn verify_integrity(
vault, vault,
&ClientCredentials { &ClientCredentials {
pubkey: pubkey.clone(), pubkey: pubkey.clone(),
nonce,
}, },
id, id,
) )
@@ -142,53 +136,6 @@ async fn verify_integrity(
Ok(()) Ok(())
} }
/// Atomically increments the nonce and re-signs the integrity envelope.
/// Returns the new nonce, which is used as the challenge nonce.
async fn create_nonce(
db: &db::DatabasePool,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<i32, Error> {
let pubkey_bytes = pubkey.to_bytes();
let pubkey = pubkey.clone();
let mut conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::DatabasePoolUnavailable
})?;
conn.exclusive_transaction(|conn| {
let vault = vault.clone();
let pubkey = pubkey.clone();
Box::pin(async move {
let (id, new_nonce): (i32, i32) = update(program_client::table)
.filter(program_client::public_key.eq(&pubkey_bytes))
.set(program_client::nonce.eq(program_client::nonce + 1))
.returning((program_client::id, program_client::nonce))
.get_result(conn)
.await?;
integrity::sign_entity(
conn,
&vault,
&ClientCredentials {
pubkey: pubkey.clone(),
nonce: new_nonce,
},
id,
)
.await
.map_err(|e| {
error!(?e, "Integrity sign failed after nonce update");
Error::DatabaseOperationFailed
})?;
Ok(new_nonce)
})
})
.await
}
async fn approve_new_client(actors: &GlobalActors, profile: ClientProfile) -> Result<(), Error> { async fn approve_new_client(actors: &GlobalActors, profile: ClientProfile) -> Result<(), Error> {
let result = actors let result = actors
.flow_coordinator .flow_coordinator
@@ -228,8 +175,6 @@ async fn insert_client(
let vault = vault.clone(); let vault = vault.clone();
let pubkey = pubkey.clone(); let pubkey = pubkey.clone();
Box::pin(async move { Box::pin(async move {
const NONCE_START: i32 = 1;
let metadata_id = insert_into(client_metadata::table) let metadata_id = insert_into(client_metadata::table)
.values(( .values((
client_metadata::name.eq(&metadata.name), client_metadata::name.eq(&metadata.name),
@@ -244,7 +189,6 @@ async fn insert_client(
.values(( .values((
program_client::public_key.eq(pubkey.to_bytes()), program_client::public_key.eq(pubkey.to_bytes()),
program_client::metadata_id.eq(metadata_id), program_client::metadata_id.eq(metadata_id),
program_client::nonce.eq(NONCE_START),
)) ))
.on_conflict_do_nothing() .on_conflict_do_nothing()
.returning(program_client::id) .returning(program_client::id)
@@ -256,7 +200,6 @@ async fn insert_client(
&vault, &vault,
&ClientCredentials { &ClientCredentials {
pubkey: pubkey.clone(), pubkey: pubkey.clone(),
nonce: NONCE_START,
}, },
client_id, client_id,
) )
@@ -346,15 +289,14 @@ async fn sync_client_metadata(
async fn challenge_client<T>( async fn challenge_client<T>(
transport: &mut T, transport: &mut T,
pubkey: authn::PublicKey, pubkey: authn::PublicKey,
nonce: i32, challenge: AuthChallenge,
) -> Result<(), Error> ) -> Result<(), Error>
where where
T: Bi<Inbound, Result<Outbound, Error>> + ?Sized, T: Bi<Inbound, Result<Outbound, Error>> + ?Sized,
{ {
transport transport
.send(Ok(Outbound::AuthChallenge { .send(Ok(Outbound::AuthChallenge {
pubkey: pubkey.clone(), challenge: challenge.clone(),
nonce,
})) }))
.await .await
.map_err(|e| { .map_err(|e| {
@@ -372,7 +314,7 @@ where
Error::Transport Error::Transport
})?; })?;
if !pubkey.verify(nonce, CLIENT_CONTEXT, &signature) { if !pubkey.verify(&challenge, CLIENT_CONTEXT, &signature) {
error!("Challenge solution verification failed"); error!("Challenge solution verification failed");
return Err(Error::InvalidChallengeSolution); return Err(Error::InvalidChallengeSolution);
} }
@@ -388,8 +330,8 @@ where
return Err(Error::Transport); return Err(Error::Transport);
}; };
let client_id = match get_current_nonce_and_id(&props.db, &pubkey).await? { let client_id = match get_client_id(&props.db, &pubkey).await? {
Some((id, _)) => { Some(id) => {
verify_integrity(&props.db, &props.actors.vault, &pubkey).await?; verify_integrity(&props.db, &props.actors.vault, &pubkey).await?;
id id
} }
@@ -407,8 +349,9 @@ where
}; };
sync_client_metadata(&props.db, client_id, &metadata).await?; sync_client_metadata(&props.db, client_id, &metadata).await?;
let challenge_nonce = create_nonce(&props.db, &props.actors.vault, &pubkey).await?;
challenge_client(transport, pubkey, challenge_nonce).await?; let challenge = AuthChallenge::generate(&mut rand::rng());
challenge_client(transport, pubkey, challenge).await?;
transport transport
.send(Ok(Outbound::AuthSuccess)) .send(Ok(Outbound::AuthSuccess))

View File

@@ -18,7 +18,6 @@ pub struct ClientProfile {
pub struct ClientCredentials { pub struct ClientCredentials {
pub pubkey: authn::PublicKey, pub pubkey: authn::PublicKey,
pub nonce: i32,
} }
impl Integrable for ClientCredentials { impl Integrable for ClientCredentials {
@@ -28,7 +27,6 @@ impl Integrable for ClientCredentials {
impl Hashable for ClientCredentials { impl Hashable for ClientCredentials {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) { fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
hasher.update(self.pubkey.to_bytes()); hasher.update(self.pubkey.to_bytes());
self.nonce.hash(hasher);
} }
} }

View File

@@ -1,11 +1,12 @@
use arbiter_crypto::authn; use arbiter_crypto::authn::{self, AuthChallenge};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
use tracing::error; use tracing::error;
mod state; mod state;
use state::*; use state::*;
use super::{AuthCredentials, UserAgentConnection}; use super::Credentials;
use super::UserAgentConnection;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Inbound { pub enum Inbound {
@@ -44,7 +45,7 @@ impl From<diesel::result::Error> for Error {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Outbound { pub enum Outbound {
AuthChallenge { nonce: i32 }, AuthChallenge { challenge: AuthChallenge },
AuthSuccess, AuthSuccess,
} }
@@ -52,12 +53,11 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
match payload { match payload {
Inbound::AuthChallengeRequest { Inbound::AuthChallengeRequest {
pubkey, pubkey,
bootstrap_token: None, bootstrap_token,
} => AuthEvents::AuthRequest(ChallengeRequest { pubkey }), } => AuthEvents::AuthRequest(ChallengeRequest {
Inbound::AuthChallengeRequest {
pubkey, pubkey,
bootstrap_token: Some(token), bootstrap_token,
} => AuthEvents::BootstrapAuthRequest(BootstrapAuthRequest { pubkey, token }), }),
Inbound::AuthChallengeSolution { signature } => { Inbound::AuthChallengeSolution { signature } => {
AuthEvents::ReceivedSolution(ChallengeSolution { AuthEvents::ReceivedSolution(ChallengeSolution {
solution: signature, solution: signature,
@@ -69,14 +69,13 @@ fn parse_auth_event(payload: Inbound) -> AuthEvents {
pub async fn authenticate<T>( pub async fn authenticate<T>(
props: &mut UserAgentConnection, props: &mut UserAgentConnection,
transport: &mut T, transport: &mut T,
) -> Result<AuthCredentials, Error> ) -> Result<Credentials, Error>
where where
T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized, T: Bi<Inbound, Result<Outbound, Error>> + Send + ?Sized,
{ {
let mut state = AuthStateMachine::new(AuthContext::new(props, transport)); let mut state = AuthStateMachine::new(AuthContext::new(props, transport));
loop { loop {
// `state` holds a mutable reference to `props` so we can't access it directly here
let Some(payload) = state.context_mut().transport.recv().await else { let Some(payload) = state.context_mut().transport.recv().await else {
return Err(Error::Transport); return Err(Error::Transport);
}; };

View File

@@ -1,32 +1,26 @@
use super::super::{AuthCredentials, Credentials, UserAgentConnection}; use super::super::{Credentials, UserAgentConnection};
use arbiter_crypto::authn::{self, USERAGENT_CONTEXT}; use arbiter_crypto::authn::{self, AuthChallenge, USERAGENT_CONTEXT};
use arbiter_proto::transport::Bi; use arbiter_proto::transport::Bi;
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, sqlite::Sqlite, update}; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl};
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::RunQueryDsl;
use kameo::actor::ActorRef;
use tracing::error; use tracing::error;
use super::Error; use super::Error;
use crate::peers::user_agent::auth::Outbound;
use crate::{ use crate::{
actors::{bootstrap::ConsumeToken, vault::Vault}, actors::bootstrap::ConsumeToken,
crypto::integrity,
db::{DatabasePool, schema::useragent_client}, db::{DatabasePool, schema::useragent_client},
peers::user_agent::auth::Outbound,
}; };
pub struct ChallengeRequest { pub struct ChallengeRequest {
pub pubkey: authn::PublicKey, pub pubkey: authn::PublicKey,
} pub bootstrap_token: Option<String>,
pub struct BootstrapAuthRequest {
pub pubkey: authn::PublicKey,
pub token: String,
} }
pub struct ChallengeContext { pub struct ChallengeContext {
pub id: i32, pub challenge: AuthChallenge,
pub challenge_nonce: i32, pub pubkey: authn::PublicKey,
pub key: authn::PublicKey, pub bootstrap_token: Option<String>,
} }
pub struct ChallengeSolution { pub struct ChallengeSolution {
@@ -38,116 +32,25 @@ smlang::statemachine!(
custom_error: true, custom_error: true,
transitions: { transitions: {
*Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext), *Init + AuthRequest(ChallengeRequest) / async prepare_challenge = SentChallenge(ChallengeContext),
Init + BootstrapAuthRequest(BootstrapAuthRequest) / async verify_bootstrap_token = AuthOk(AuthCredentials), SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(Credentials),
SentChallenge(ChallengeContext) + ReceivedSolution(ChallengeSolution) / async verify_solution = AuthOk(AuthCredentials),
} }
); );
const NONCE_START: i32 = 1; async fn get_client_id(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<Option<i32>, Error> {
let mut conn = db.get().await.map_err(|e| {
/// Returns the current nonce, ready to use for the challenge nonce.
async fn get_current_nonce_and_id(
db: &DatabasePool,
key: &authn::PublicKey,
) -> Result<(i32, i32), Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error"); error!(error = ?e, "Database pool error");
Error::internal("Database unavailable") Error::internal("Database unavailable")
})?; })?;
db_conn
.exclusive_transaction(|conn| {
Box::pin(async move {
useragent_client::table useragent_client::table
.filter(useragent_client::public_key.eq(key.to_bytes())) .filter(useragent_client::public_key.eq(pubkey.to_bytes()))
.select((useragent_client::id, useragent_client::nonce)) .select(useragent_client::id)
.first::<(i32, i32)>(conn) .first::<i32>(&mut conn)
.await
})
})
.await .await
.optional() .optional()
.map_err(|e| { .map_err(|e| {
error!(error = ?e, "Database error"); error!(error = ?e, "Database error");
Error::internal("Database operation failed") Error::internal("Database operation failed")
})?
.ok_or_else(|| {
error!(?key, "Public key not found in database");
Error::UnregisteredPublicKey
})
}
async fn verify_integrity(
db: &DatabasePool,
vault: &ActorRef<Vault>,
pubkey: &authn::PublicKey,
) -> Result<(), Error> {
let mut db_conn = db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::internal("Database unavailable")
})?;
let (id, nonce) = get_current_nonce_and_id(db, pubkey).await?;
let _result = integrity::verify_entity(
&mut db_conn,
vault,
&AuthCredentials {
creds: Credentials {
id,
pubkey: pubkey.clone(),
},
new_nonce: nonce,
},
id,
)
.await
.map_err(|e| {
error!(?e, "Integrity verification failed");
Error::internal("Integrity verification failed")
})?;
Ok(())
}
async fn compute_current_nonce(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
pubkey: &authn::PublicKey,
) -> Result<(i32, i32), Error> {
update(useragent_client::table)
.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)
.await
.map_err(|e| {
error!(error = ?e, "Database error incrementing nonce");
Error::internal("Database operation failed")
})
}
async fn resign_credentials(
conn: &mut impl AsyncConnection<Backend = Sqlite>,
vault: &ActorRef<Vault>,
id: i32,
pubkey: &authn::PublicKey,
new_nonce: i32,
) -> Result<(), Error> {
integrity::sign_entity(
conn,
vault,
&AuthCredentials {
creds: Credentials {
id,
pubkey: pubkey.clone(),
},
new_nonce,
},
id,
)
.await
.map_err(|e| {
error!(?e, "Integrity signature update failed");
Error::internal("Database error")
}) })
} }
@@ -159,10 +62,7 @@ async fn register_key(db: &DatabasePool, pubkey: &authn::PublicKey) -> Result<i3
})?; })?;
let id: i32 = diesel::insert_into(useragent_client::table) let id: i32 = diesel::insert_into(useragent_client::table)
.values(( .values((useragent_client::public_key.eq(pubkey_bytes),))
useragent_client::public_key.eq(pubkey_bytes),
useragent_client::nonce.eq(NONCE_START),
))
.returning(useragent_client::id) .returning(useragent_client::id)
.get_result(&mut conn) .get_result(&mut conn)
.await .await
@@ -193,38 +93,25 @@ where
async fn prepare_challenge( async fn prepare_challenge(
&mut self, &mut self,
ChallengeRequest { pubkey }: ChallengeRequest, ChallengeRequest {
pubkey,
bootstrap_token,
}: ChallengeRequest,
) -> Result<ChallengeContext, Self::Error> { ) -> Result<ChallengeContext, Self::Error> {
let is_signing = integrity::is_signing_available(&self.conn.actors.vault) // Verify pubkey is registered (unless bootstrapping)
.await if bootstrap_token.is_none() {
.unwrap_or(false); let id = get_client_id(&self.conn.db, &pubkey).await?;
if id.is_none() {
if is_signing { return Err(Error::UnregisteredPublicKey);
verify_integrity(&self.conn.db, &self.conn.actors.vault, &pubkey).await?; }
} }
let vault = self.conn.actors.vault.clone(); let challenge = AuthChallenge::generate(&mut rand::rng());
let mut conn = self.conn.db.get().await.map_err(|e| {
error!(error = ?e, "Database pool error");
Error::internal("Database unavailable")
})?;
let (id, nonce) = conn
.exclusive_transaction(|conn| {
let pubkey = pubkey.clone();
let vault = vault.clone();
Box::pin(async move {
let (id, new_nonce) = compute_current_nonce(conn, &pubkey).await?;
if is_signing {
resign_credentials(conn, &vault, id, &pubkey, new_nonce).await?;
}
Result::<_, Error>::Ok((id, new_nonce))
})
})
.await?;
self.transport self.transport
.send(Ok(Outbound::AuthChallenge { nonce })) .send(Ok(Outbound::AuthChallenge {
challenge: challenge.clone(),
}))
.await .await
.map_err(|e| { .map_err(|e| {
error!(?e, "Failed to send auth challenge"); error!(?e, "Failed to send auth challenge");
@@ -232,18 +119,41 @@ where
})?; })?;
Ok(ChallengeContext { Ok(ChallengeContext {
id, challenge,
challenge_nonce: nonce, pubkey,
key: pubkey, bootstrap_token,
}) })
} }
#[allow(missing_docs)] #[allow(missing_docs)]
#[allow(clippy::result_unit_err)] #[allow(clippy::unused_unit)]
async fn verify_bootstrap_token( async fn verify_solution(
&mut self, &mut self,
BootstrapAuthRequest { pubkey, token }: BootstrapAuthRequest, ChallengeContext {
) -> Result<AuthCredentials, Self::Error> { challenge,
pubkey,
bootstrap_token,
}: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution,
) -> Result<Credentials, 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 = pubkey.verify(challenge, USERAGENT_CONTEXT, &signature);
if !valid {
self.transport
.send(Err(Error::InvalidChallengeSolution))
.await
.map_err(|_| Error::Transport)?;
return Err(Error::InvalidChallengeSolution);
}
// Resolve client id: bootstrap (consume token + register) or lookup
let id = match bootstrap_token {
Some(token) => {
let token_ok: bool = self let token_ok: bool = self
.conn .conn
.actors .actors
@@ -258,72 +168,29 @@ where
})?; })?;
if !token_ok { if !token_ok {
error!("Invalid bootstrap token provided");
return Err(Error::InvalidBootstrapToken);
}
match token_ok {
true => {
let id = register_key(&self.conn.db, &pubkey).await?;
self.transport
.send(Ok(Outbound::AuthSuccess))
.await
.map_err(|_| Error::Transport)?;
Ok(AuthCredentials {
creds: Credentials { id, pubkey },
new_nonce: NONCE_START,
})
}
false => {
error!("Invalid bootstrap token provided"); error!("Invalid bootstrap token provided");
self.transport self.transport
.send(Err(Error::InvalidBootstrapToken)) .send(Err(Error::InvalidBootstrapToken))
.await .await
.map_err(|_| Error::Transport)?; .map_err(|_| Error::Transport)?;
Err(Error::InvalidBootstrapToken) return Err(Error::InvalidBootstrapToken);
}
}
} }
#[allow(missing_docs)] register_key(&self.conn.db, pubkey).await?
#[allow(clippy::unused_unit)] }
async fn verify_solution( None => get_client_id(&self.conn.db, pubkey)
&mut self, .await?
ChallengeContext { .ok_or(Error::UnregisteredPublicKey)?,
id, };
challenge_nonce,
key,
}: &ChallengeContext,
ChallengeSolution { solution }: ChallengeSolution,
) -> Result<AuthCredentials, 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 = key.verify(*challenge_nonce, USERAGENT_CONTEXT, &signature);
match valid {
true => {
self.transport self.transport
.send(Ok(Outbound::AuthSuccess)) .send(Ok(Outbound::AuthSuccess))
.await .await
.map_err(|_| Error::Transport)?; .map_err(|_| Error::Transport)?;
Ok(AuthCredentials {
creds: Credentials { Ok(Credentials {
id: *id, id,
pubkey: key.clone(), pubkey: pubkey.clone(),
},
new_nonce: *challenge_nonce,
}) })
} }
false => {
self.transport
.send(Err(Error::InvalidChallengeSolution))
.await
.map_err(|_| Error::Transport)?;
Err(Error::InvalidChallengeSolution)
}
}
}
} }

View File

@@ -1,7 +1,10 @@
use crate::{ use crate::{
actors::GlobalActors, actors::{
crypto::integrity::{self, Integrable}, GlobalActors,
db::{self, DatabaseError}, vault::{GetState, Vault},
},
crypto::integrity::{self, AttestationStatus, Integrable},
db::{self, DatabaseError, DatabasePool},
peers::client::ClientProfile, peers::client::ClientProfile,
}; };
use arbiter_crypto::authn; use arbiter_crypto::authn;
@@ -11,7 +14,7 @@ pub use auth::authenticate;
use kameo::actor::{ActorRef, Spawn as _}; use kameo::actor::{ActorRef, Spawn as _};
pub use session::UserAgentSession; pub use session::UserAgentSession;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tracing::warn; use tracing::{error, warn};
use vault_gate::VaultGate; use vault_gate::VaultGate;
use crate::crypto::integrity::hashing::Hashable; use crate::crypto::integrity::hashing::Hashable;
@@ -20,24 +23,11 @@ pub mod auth;
pub mod session; pub mod session;
pub mod vault_gate; pub mod vault_gate;
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone)]
pub struct Credentials { pub struct Credentials {
pub id: i32, pub id: i32,
pub pubkey: authn::PublicKey, pub pubkey: authn::PublicKey,
} }
impl Hashable for Credentials {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.id.hash(hasher);
self.pubkey.hash(hasher);
}
}
#[derive(Debug, Clone)]
pub struct AuthCredentials {
pub creds: Credentials,
// denotes new nonce, not current
pub new_nonce: i32,
}
impl Hashable for authn::PublicKey { impl Hashable for authn::PublicKey {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) { fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
@@ -45,14 +35,14 @@ impl Hashable for authn::PublicKey {
} }
} }
impl Hashable for AuthCredentials { impl Hashable for Credentials {
fn hash<H: sha2::Digest>(&self, hasher: &mut H) { fn hash<H: sha2::Digest>(&self, hasher: &mut H) {
self.creds.hash(hasher); self.id.hash(hasher);
self.new_nonce.hash(hasher); self.pubkey.hash(hasher);
} }
} }
impl Integrable for AuthCredentials { impl Integrable for Credentials {
const KIND: &'static str = "useragent_credentials"; const KIND: &'static str = "useragent_credentials";
} }
@@ -95,38 +85,44 @@ impl From<auth::Error> for Error {
} }
} }
pub async fn start<T>( async fn verify_integrity(
props: &mut UserAgentConnection, db: &DatabasePool,
mut transport: T, vault: &ActorRef<Vault>,
oob_sender: Box<dyn Sender<OutOfBand>>, credentials: &Credentials,
) -> Result<ActorRef<UserAgentSession>, Error> ) -> Result<(), Error> {
where let mut conn = db
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send, .get()
T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send,
{
let auth_creds = authenticate(props, &mut transport).await?;
let creds = match integrity::is_signing_available(&props.actors.vault)
.await .await
.map_err(|_| Error::Internal("Integrity verification failed".into()))? .map_err(|_| Error::Internal("DB unavailable".into()))?;
{ match integrity::verify_entity(&mut conn, &vault, credentials, credentials.id).await {
// credentials were checked by `auth` stage Ok(AttestationStatus::Attested) => Ok(()),
true => auth_creds.creds, Ok(AttestationStatus::Unavailable) => {
false => run_vault_gate(props, &mut transport, auth_creds).await?, Err(Error::Internal("Vault sealed during promotion".into()))
}; }
Err(e) => {
error!(?e, "Integrity verification failed during unseal promotion");
Err(Error::Internal("Integrity check failed".into()))
}
}
}
Ok(UserAgentSession::spawn(UserAgentSession::new( async fn should_run_gate(vault: &ActorRef<Vault>) -> Result<bool, Error> {
props.clone(), let vault_state = vault
creds, .ask(GetState {})
oob_sender, .await
))) .map_err(|_| Error::Internal("Failed to contact the vault".into()))?;
Ok(!matches!(
vault_state,
crate::actors::vault::VaultState::Unsealed
))
} }
async fn run_vault_gate<T>( async fn run_vault_gate<T>(
props: &UserAgentConnection, props: &UserAgentConnection,
transport: &mut T, transport: &mut T,
auth_creds: AuthCredentials, auth_creds: Credentials,
) -> Result<Credentials, Error> ) -> Result<(), Error>
where where
T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send + ?Sized, T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send + ?Sized,
{ {
@@ -175,3 +171,29 @@ where
gate.kill(); gate.kill();
result result
} }
pub async fn start<T>(
props: &mut UserAgentConnection,
mut transport: T,
oob_sender: Box<dyn Sender<OutOfBand>>,
) -> Result<ActorRef<UserAgentSession>, Error>
where
T: Bi<auth::Inbound, Result<auth::Outbound, auth::Error>> + Send,
T: Bi<vault_gate::Inbound, Result<vault_gate::Outbound, vault_gate::Error>> + Send,
{
let creds = authenticate(props, &mut transport).await?;
// should run vault gate only if sealed / unbootstrapped
if should_run_gate(&props.actors.vault).await? {
run_vault_gate(props, &mut transport, creds.clone()).await?;
}
// checking the integrity
verify_integrity(&props.db, &props.actors.vault, &creds).await?;
Ok(UserAgentSession::spawn(UserAgentSession::new(
props.clone(),
creds,
oob_sender,
)))
}

View File

@@ -1,26 +1,21 @@
use std::sync::Mutex;
use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature}; use alloy::{consensus::TxEip1559, primitives::Address, signers::Signature};
use arbiter_crypto::{ use arbiter_crypto::{
authn, authn,
safecell::{SafeCell, SafeCellHandle as _}, safecell::SafeCellHandle as _,
}; };
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit}; use chacha20poly1305::aead::KeyInit;
use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper}; use diesel::{ExpressionMethods as _, QueryDsl as _, SelectableHelper};
use diesel_async::{AsyncConnection, RunQueryDsl}; use diesel_async::{AsyncConnection, RunQueryDsl};
use kameo::error::SendError; use kameo::error::SendError;
use kameo::messages; use kameo::messages;
use kameo::prelude::Context; use kameo::prelude::Context;
use tracing::{error, info}; use tracing::error;
use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer; use crate::actors::flow_coordinator::client_connect_approval::ClientApprovalAnswer;
use crate::actors::{ use crate::actors::evm::{
evm::{
ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError, ClientSignTransaction, Generate, ListWallets, SignTransactionError as EvmSignError,
UseragentCreateGrant, UseragentListGrants, UseragentCreateGrant, UseragentListGrants,
},
vault::{self, Bootstrap, TryUnseal},
}; };
use crate::db::models::{ use crate::db::models::{
EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata, EvmWalletAccess, NewEvmWalletAccess, ProgramClient, ProgramClientMetadata,

View File

@@ -1,12 +1,9 @@
use arbiter_crypto::authn; use arbiter_crypto::authn;
use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::{RunQueryDsl};
use kameo_actors::message_bus::Register;
use std::{borrow::Cow, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
use arbiter_proto::transport::Sender; use arbiter_proto::transport::Sender;
use kameo::{Actor, actor::ActorRef, messages, prelude::Message}; use kameo::{Actor, actor::ActorRef, messages};
use thiserror::Error; use thiserror::Error;
use tracing::error; use tracing::error;
@@ -14,8 +11,8 @@ use crate::{
actors::{ actors::{
flow_coordinator::client_connect_approval::ClientApprovalController, flow_coordinator::client_connect_approval::ClientApprovalController,
useragent_registry::ConnectUseragent, useragent_registry::ConnectUseragent,
vault::events, },
}, crypto::integrity, db::schema::useragent_client, peers::{client::ClientProfile, user_agent::{AuthCredentials, Credentials}} peers::{client::ClientProfile, user_agent::Credentials},
}; };
use super::{OutOfBand, UserAgentConnection}; use super::{OutOfBand, UserAgentConnection};

View File

@@ -1,22 +1,24 @@
use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _}; use arbiter_crypto::safecell::{SafeCell, SafeCellHandle as _};
use arbiter_proto::transport::Bi;
use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce}; use chacha20poly1305::{AeadInPlace, KeyInit as _, XChaCha20Poly1305, XNonce};
use kameo::{Actor, error::SendError, messages, prelude::Message}; use kameo::{Actor, error::SendError, messages, prelude::Message};
use kameo_actors::message_bus::Register; use kameo_actors::message_bus::Register;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tracing::{error, info}; use tracing::{error, info, warn};
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret}; use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret};
pub mod state; pub mod state;
use state::*; use state::*;
use super::{AuthCredentials, Credentials}; use super::Credentials;
use crate::{ use crate::{
actors::{ actors::{
GlobalActors, GlobalActors,
vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events}, vault::{self, Bootstrap, GetState, TryUnseal, VaultState, events},
}, },
crypto::integrity::{self, AttestationStatus}, crypto::integrity::{self},
db::DatabasePool, db::DatabasePool,
peers::user_agent::UserAgentConnection,
}; };
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@@ -43,8 +45,8 @@ pub struct HandshakeResponse {
} }
pub struct VaultGate { pub struct VaultGate {
pub auth_creds: AuthCredentials, pub auth_creds: Credentials,
pub promotion_tx: Option<oneshot::Sender<Result<Credentials, Error>>>, pub promotion_tx: Option<oneshot::Sender<Result<(), Error>>>,
pub state: State, pub state: State,
pub actors: GlobalActors, pub actors: GlobalActors,
pub db: DatabasePool, pub db: DatabasePool,
@@ -52,10 +54,10 @@ pub struct VaultGate {
impl VaultGate { impl VaultGate {
pub fn new( pub fn new(
auth_creds: AuthCredentials, auth_creds: Credentials,
actors: GlobalActors, actors: GlobalActors,
db: DatabasePool, db: DatabasePool,
promotion_tx: oneshot::Sender<Result<Credentials, Error>>, promotion_tx: oneshot::Sender<Result<(), Error>>,
) -> Self { ) -> Self {
Self { Self {
auth_creds, auth_creds,
@@ -260,14 +262,14 @@ impl Message<events::Bootstrapped> for VaultGate {
&mut conn, &mut conn,
&self.actors.vault, &self.actors.vault,
&self.auth_creds, &self.auth_creds,
self.auth_creds.creds.id, self.auth_creds.id,
) )
.await .await
.map_err(|e| { .map_err(|e| {
error!(?e, "Failed to sign integrity envelope on bootstrap"); error!(?e, "Failed to sign integrity envelope on bootstrap");
Error::internal("Integrity sign failed") Error::internal("Integrity sign failed")
})?; })?;
Ok(self.auth_creds.creds.clone()) Ok(())
} }
.await; .await;
@@ -286,34 +288,8 @@ impl Message<events::Unsealed> for VaultGate {
_: events::Unsealed, _: events::Unsealed,
ctx: &mut kameo::prelude::Context<Self, Self::Reply>, ctx: &mut kameo::prelude::Context<Self, Self::Reply>,
) -> Self::Reply { ) -> Self::Reply {
let result = async {
let mut conn = self
.db
.get()
.await
.map_err(|_| Error::internal("DB unavailable"))?;
match integrity::verify_entity(
&mut conn,
&self.actors.vault,
&self.auth_creds,
self.auth_creds.creds.id,
)
.await
{
Ok(AttestationStatus::Attested) => Ok(self.auth_creds.creds.clone()),
Ok(AttestationStatus::Unavailable) => {
Err(Error::internal("Vault sealed during promotion"))
}
Err(e) => {
error!(?e, "Integrity verification failed during unseal promotion");
Err(Error::InvalidKey)
}
}
}
.await;
if let Some(tx) = self.promotion_tx.take() { if let Some(tx) = self.promotion_tx.take() {
let _ = tx.send(result); let _ = tx.send(Ok(()));
} }
ctx.stop(); ctx.stop();
} }

View File

@@ -1,6 +1,5 @@
use std::sync::Mutex;
use x25519_dalek::{EphemeralSecret, PublicKey, SharedSecret}; use x25519_dalek::{PublicKey, SharedSecret};
pub struct Handshake { pub struct Handshake {
client_pubkey: PublicKey, client_pubkey: PublicKey,

View File

@@ -1,5 +1,5 @@
use arbiter_crypto::{ use arbiter_crypto::{
authn::{self, CLIENT_CONTEXT, format_challenge}, authn::{self, AuthChallenge, CLIENT_CONTEXT},
safecell::{SafeCell, SafeCellHandle as _}, safecell::{SafeCell, SafeCellHandle as _},
}; };
use arbiter_proto::ClientMetadata; use arbiter_proto::ClientMetadata;
@@ -66,12 +66,8 @@ async fn insert_registered_client(
.unwrap(); .unwrap();
} }
fn sign_client_challenge( fn sign_client_challenge(key: &SigningKey<MlDsa87>, challenge: &AuthChallenge) -> authn::Signature {
key: &SigningKey<MlDsa87>, let challenge = challenge.format();
nonce: i32,
pubkey: &authn::PublicKey,
) -> authn::Signature {
let challenge = format_challenge(nonce, &pubkey.to_bytes());
key.signing_key() key.signing_key()
.sign_deterministic(&challenge, CLIENT_CONTEXT) .sign_deterministic(&challenge, CLIENT_CONTEXT)
.unwrap() .unwrap()

View File

@@ -8,7 +8,7 @@ use arbiter_server::{
actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap}, actors::{GlobalActors, bootstrap::GetToken, vault::Bootstrap},
crypto::integrity, crypto::integrity,
db::{self, schema}, db::{self, schema},
peers::user_agent::{AuthCredentials, Credentials, UserAgentConnection, auth}, peers::user_agent::{Credentials, Credentials, UserAgentConnection, auth},
}; };
use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
@@ -144,7 +144,7 @@ pub async fn test_challenge_auth() {
integrity::sign_entity( integrity::sign_entity(
&mut conn, &mut conn,
&actors.vault, &actors.vault,
&AuthCredentials { &Credentials {
creds: Credentials { creds: Credentials {
id, id,
pubkey: new_key.verifying_key().into(), pubkey: new_key.verifying_key().into(),
@@ -285,7 +285,7 @@ pub async fn test_challenge_auth_rejects_invalid_signature() {
integrity::sign_entity( integrity::sign_entity(
&mut conn, &mut conn,
&actors.vault, &actors.vault,
&AuthCredentials { &Credentials {
creds: Credentials { creds: Credentials {
id, id,
pubkey: new_key.verifying_key().into(), pubkey: new_key.verifying_key().into(),

View File

@@ -9,8 +9,10 @@ use arbiter_server::{
}, },
db, db,
peers::user_agent::{ peers::user_agent::{
AuthCredentials, Credentials, Credentials,
vault_gate::{Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate}, vault_gate::{
Error as VaultGateError, HandleHandshake, HandleUnsealEncryptedKey, VaultGate,
},
}, },
}; };
@@ -21,7 +23,11 @@ use x25519_dalek::{EphemeralSecret, PublicKey};
async fn setup_sealed_gate( async fn setup_sealed_gate(
seal_key: &[u8], seal_key: &[u8],
) -> (db::DatabasePool, kameo::actor::ActorRef<VaultGate>, oneshot::Receiver<Result<Credentials, VaultGateError>>) { ) -> (
db::DatabasePool,
kameo::actor::ActorRef<VaultGate>,
oneshot::Receiver<Result<Credentials, VaultGateError>>,
) {
let db = db::create_test_pool().await; let db = db::create_test_pool().await;
let actors = GlobalActors::spawn(db.clone()).await.unwrap(); let actors = GlobalActors::spawn(db.clone()).await.unwrap();
@@ -36,10 +42,7 @@ async fn setup_sealed_gate(
let (promotion_tx, promotion_rx) = oneshot::channel(); let (promotion_tx, promotion_rx) = oneshot::channel();
let pubkey = authn::SigningKey::generate().public_key(); let pubkey = authn::SigningKey::generate().public_key();
let auth_creds = AuthCredentials { let auth_creds = Credentials { id: 1, pubkey };
creds: Credentials { id: 1, pubkey },
new_nonce: 1,
};
let gate = VaultGate::spawn(VaultGate::new(auth_creds, actors, db.clone(), promotion_tx)); let gate = VaultGate::spawn(VaultGate::new(auth_creds, actors, db.clone(), promotion_tx));
(db, gate, promotion_rx) (db, gate, promotion_rx)