refactor(server::{useragent::auth, client::auth}): use random based + timestamp nonce instead of monotonic counter in database
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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
3
server/Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)?
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user