refactor(server): rework envelopes and integrity check
This commit is contained in:
@@ -8,19 +8,18 @@ use rand::{SeedableRng, rng, rngs::StdRng};
|
||||
|
||||
use crate::{
|
||||
actors::keyholder::{CreateNew, Decrypt, GetState, KeyHolder, KeyHolderState},
|
||||
crypto::integrity,
|
||||
db::{
|
||||
DatabaseError, DatabasePool,
|
||||
models::{self, SqliteTimestamp},
|
||||
schema,
|
||||
},
|
||||
evm::{
|
||||
self, RunKind,
|
||||
policies::{
|
||||
FullGrant, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||
self, ListError, RunKind, policies::{
|
||||
CombinedSettings, Grant, SharedGrantSettings, SpecificGrant, SpecificMeaning,
|
||||
ether_transfer::EtherTransfer, token_transfers::TokenTransfer,
|
||||
},
|
||||
}
|
||||
},
|
||||
integrity,
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
|
||||
@@ -58,8 +57,8 @@ pub enum Error {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] DatabaseError),
|
||||
|
||||
#[error("Vault is sealed")]
|
||||
VaultSealed,
|
||||
#[error("Integrity violation: {0}")]
|
||||
Integrity(#[from] integrity::Error),
|
||||
}
|
||||
|
||||
#[derive(Actor)]
|
||||
@@ -83,20 +82,6 @@ impl EvmActor {
|
||||
engine,
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_unsealed(&self) -> Result<(), Error> {
|
||||
let state = self
|
||||
.keyholder
|
||||
.ask(GetState)
|
||||
.await
|
||||
.map_err(|_| Error::KeyholderSend)?;
|
||||
|
||||
if state != KeyHolderState::Unsealed {
|
||||
return Err(Error::VaultSealed);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[messages]
|
||||
@@ -151,62 +136,58 @@ impl EvmActor {
|
||||
basic: SharedGrantSettings,
|
||||
grant: SpecificGrant,
|
||||
) -> Result<i32, Error> {
|
||||
self.ensure_unsealed().await?;
|
||||
|
||||
match grant {
|
||||
SpecificGrant::EtherTransfer(settings) => {
|
||||
self.engine
|
||||
.create_grant::<EtherTransfer>(FullGrant {
|
||||
basic,
|
||||
specific: settings,
|
||||
})
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
}
|
||||
SpecificGrant::TokenTransfer(settings) => {
|
||||
self.engine
|
||||
.create_grant::<TokenTransfer>(FullGrant {
|
||||
basic,
|
||||
specific: settings,
|
||||
})
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
}
|
||||
SpecificGrant::EtherTransfer(settings) => self
|
||||
.engine
|
||||
.create_grant::<EtherTransfer>(CombinedSettings {
|
||||
shared: basic,
|
||||
specific: settings,
|
||||
})
|
||||
.await
|
||||
.map_err(Error::from),
|
||||
SpecificGrant::TokenTransfer(settings) => self
|
||||
.engine
|
||||
.create_grant::<TokenTransfer>(CombinedSettings {
|
||||
shared: basic,
|
||||
specific: settings,
|
||||
})
|
||||
.await
|
||||
.map_err(Error::from),
|
||||
}
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn useragent_delete_grant(&mut self, grant_id: i32) -> Result<(), Error> {
|
||||
self.ensure_unsealed().await?;
|
||||
// let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||
// let keyholder = self.keyholder.clone();
|
||||
|
||||
let mut conn = self.db.get().await.map_err(DatabaseError::from)?;
|
||||
let keyholder = self.keyholder.clone();
|
||||
// diesel_async::AsyncConnection::transaction(&mut conn, |conn| {
|
||||
// Box::pin(async move {
|
||||
// diesel::update(schema::evm_basic_grant::table)
|
||||
// .filter(schema::evm_basic_grant::id.eq(grant_id))
|
||||
// .set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
|
||||
// .execute(conn)
|
||||
// .await?;
|
||||
|
||||
diesel_async::AsyncConnection::transaction(&mut conn, |conn| {
|
||||
Box::pin(async move {
|
||||
diesel::update(schema::evm_basic_grant::table)
|
||||
.filter(schema::evm_basic_grant::id.eq(grant_id))
|
||||
.set(schema::evm_basic_grant::revoked_at.eq(SqliteTimestamp::now()))
|
||||
.execute(conn)
|
||||
.await?;
|
||||
// let signed = integrity::evm::load_signed_grant_by_basic_id(conn, grant_id).await?;
|
||||
|
||||
let signed = integrity::evm::load_signed_grant_by_basic_id(conn, grant_id).await?;
|
||||
integrity::sign_entity(conn, &keyholder, &signed)
|
||||
.await
|
||||
.map_err(|_| diesel::result::Error::RollbackTransaction)?;
|
||||
// diesel::result::QueryResult::Ok(())
|
||||
// })
|
||||
// })
|
||||
// .await
|
||||
// .map_err(DatabaseError::from)?;
|
||||
|
||||
diesel::result::QueryResult::Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(DatabaseError::from)?;
|
||||
|
||||
Ok(())
|
||||
// Ok(())
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn useragent_list_grants(&mut self) -> Result<Vec<Grant<SpecificGrant>>, Error> {
|
||||
Ok(self.engine.list_all_grants().await?)
|
||||
match self.engine.list_all_grants().await {
|
||||
Ok(grants) => Ok(grants),
|
||||
Err(ListError::Database(db_err)) => Err(Error::Database(db_err)),
|
||||
Err(ListError::Integrity(integrity_err)) => Err(Error::Integrity(integrity_err)),
|
||||
}
|
||||
}
|
||||
|
||||
#[message]
|
||||
|
||||
@@ -4,9 +4,8 @@ use diesel::{
|
||||
dsl::{insert_into, update},
|
||||
};
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use hmac::{Hmac, Mac as _};
|
||||
use hmac::Mac as _;
|
||||
use kameo::{Actor, Reply, messages};
|
||||
use sha2::Sha256;
|
||||
use strum::{EnumDiscriminants, IntoDiscriminant};
|
||||
use tracing::{error, info};
|
||||
|
||||
@@ -14,7 +13,7 @@ use crate::{
|
||||
crypto::{
|
||||
KeyCell, derive_key,
|
||||
encryption::v1::{self, Nonce},
|
||||
integrity::v1::compute_integrity_tag,
|
||||
integrity::v1::HmacSha256,
|
||||
},
|
||||
safe_cell::SafeCell,
|
||||
};
|
||||
@@ -26,13 +25,6 @@ use crate::{
|
||||
},
|
||||
safe_cell::SafeCellHandle as _,
|
||||
};
|
||||
use encryption::v1::{self, KeyCell, Nonce};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
const INTEGRITY_SUBKEY_TAG: &[u8] = b"arbiter/db-integrity-key/v1";
|
||||
|
||||
pub mod encryption;
|
||||
|
||||
#[derive(Default, EnumDiscriminants)]
|
||||
#[strum_discriminants(derive(Reply), vis(pub), name(KeyHolderState))]
|
||||
@@ -142,19 +134,6 @@ impl KeyHolder {
|
||||
Ok(nonce)
|
||||
}
|
||||
|
||||
fn derive_integrity_key(root_key: &mut KeyCell) -> [u8; 32] {
|
||||
root_key.0.read_inline(|root_key_bytes| {
|
||||
let mut hmac = match HmacSha256::new_from_slice(root_key_bytes.as_slice()) {
|
||||
Ok(v) => v,
|
||||
Err(_) => unreachable!("HMAC accepts keys of any size"),
|
||||
};
|
||||
hmac.update(INTEGRITY_SUBKEY_TAG);
|
||||
let mut out = [0u8; 32];
|
||||
out.copy_from_slice(&hmac.finalize().into_bytes());
|
||||
out
|
||||
})
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn bootstrap(&mut self, seal_key_raw: SafeCell<Vec<u8>>) -> Result<(), Error> {
|
||||
if !matches!(self.state, State::Unbootstrapped) {
|
||||
@@ -272,22 +251,6 @@ impl KeyHolder {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Signs a generic integrity payload using the vault-derived integrity key
|
||||
#[message]
|
||||
pub fn sign_integrity_tag(
|
||||
&mut self,
|
||||
purpose_tag: Vec<u8>,
|
||||
data_parts: Vec<Vec<u8>>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let State::Unsealed { root_key, .. } = &mut self.state else {
|
||||
return Err(Error::NotBootstrapped);
|
||||
};
|
||||
|
||||
let tag =
|
||||
compute_integrity_tag(root_key, &purpose_tag, data_parts.iter().map(Vec::as_slice));
|
||||
Ok(tag.to_vec())
|
||||
}
|
||||
|
||||
#[message]
|
||||
pub async fn decrypt(&mut self, aead_id: i32) -> Result<SafeCell<Vec<u8>>, Error> {
|
||||
let State::Unsealed { root_key, .. } = &mut self.state else {
|
||||
@@ -371,12 +334,12 @@ impl KeyHolder {
|
||||
return Err(Error::NotBootstrapped);
|
||||
};
|
||||
|
||||
let integrity_key = Self::derive_integrity_key(root_key);
|
||||
|
||||
let mut hmac = match HmacSha256::new_from_slice(&integrity_key) {
|
||||
Ok(v) => v,
|
||||
Err(_) => unreachable!("HMAC accepts keys of any size"),
|
||||
};
|
||||
let mut hmac = root_key
|
||||
.0
|
||||
.read_inline(|k| match HmacSha256::new_from_slice(k) {
|
||||
Ok(v) => v,
|
||||
Err(_) => unreachable!("HMAC accepts keys of any size"),
|
||||
});
|
||||
hmac.update(&root_key_history_id.to_be_bytes());
|
||||
hmac.update(&mac_input);
|
||||
|
||||
@@ -403,11 +366,12 @@ impl KeyHolder {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let integrity_key = Self::derive_integrity_key(root_key);
|
||||
let mut hmac = match HmacSha256::new_from_slice(&integrity_key) {
|
||||
Ok(v) => v,
|
||||
Err(_) => unreachable!("HMAC accepts keys of any size"),
|
||||
};
|
||||
let mut hmac = root_key
|
||||
.0
|
||||
.read_inline(|k| match HmacSha256::new_from_slice(k) {
|
||||
Ok(v) => v,
|
||||
Err(_) => unreachable!("HMAC accepts keys of any size"),
|
||||
});
|
||||
hmac.update(&key_version.to_be_bytes());
|
||||
hmac.update(&mac_input);
|
||||
|
||||
|
||||
@@ -37,6 +37,13 @@ impl Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for Error {
|
||||
fn from(e: diesel::result::Error) -> Self {
|
||||
error!(?e, "Database error");
|
||||
Self::internal("Database error")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Outbound {
|
||||
AuthChallenge { nonce: i32 },
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
use arbiter_proto::transport::Bi;
|
||||
use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, update};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::error::SendError;
|
||||
use diesel_async::{AsyncConnection, RunQueryDsl};
|
||||
use kameo::{actor::ActorRef, error::SendError};
|
||||
use tracing::error;
|
||||
|
||||
use super::Error;
|
||||
use crate::{
|
||||
actors::{
|
||||
bootstrap::ConsumeToken,
|
||||
keyholder::{self, SignIntegrityTag},
|
||||
user_agent::{AuthPublicKey, UserAgentConnection, auth::Outbound},
|
||||
keyholder::KeyHolder,
|
||||
user_agent::{AuthPublicKey, UserAgentConnection, UserAgentCredentials, auth::Outbound},
|
||||
},
|
||||
crypto::integrity::v1::USERAGENT_INTEGRITY_TAG,
|
||||
db::schema,
|
||||
crypto::integrity::{self, AttestationStatus},
|
||||
db::{DatabasePool, schema::useragent_client},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AttestationStatus {
|
||||
Attested,
|
||||
NotAttested,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
pub struct ChallengeRequest {
|
||||
pub pubkey: AuthPublicKey,
|
||||
}
|
||||
@@ -50,11 +43,11 @@ smlang::statemachine!(
|
||||
}
|
||||
);
|
||||
|
||||
async fn create_nonce(
|
||||
db: &crate::db::DatabasePool,
|
||||
pubkey_bytes: &[u8],
|
||||
key_type: crate::db::models::KeyType,
|
||||
) -> Result<i32, Error> {
|
||||
/// Returns the current nonce, ready to use for the challenge nonce.
|
||||
async fn get_current_nonce_and_id(
|
||||
db: &DatabasePool,
|
||||
key: &AuthPublicKey,
|
||||
) -> Result<(i32, i32), Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
@@ -62,21 +55,12 @@ async fn create_nonce(
|
||||
db_conn
|
||||
.exclusive_transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
let current_nonce = schema::useragent_client::table
|
||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
||||
.filter(schema::useragent_client::key_type.eq(key_type))
|
||||
.select(schema::useragent_client::nonce)
|
||||
.first::<i32>(conn)
|
||||
.await?;
|
||||
|
||||
update(schema::useragent_client::table)
|
||||
.filter(schema::useragent_client::public_key.eq(pubkey_bytes.to_vec()))
|
||||
.filter(schema::useragent_client::key_type.eq(key_type))
|
||||
.set(schema::useragent_client::nonce.eq(current_nonce + 1))
|
||||
.execute(conn)
|
||||
.await?;
|
||||
|
||||
Result::<_, diesel::result::Error>::Ok(current_nonce)
|
||||
useragent_client::table
|
||||
.filter(useragent_client::public_key.eq(key.to_stored_bytes()))
|
||||
.filter(useragent_client::key_type.eq(key.key_type()))
|
||||
.select((useragent_client::id, useragent_client::nonce))
|
||||
.first::<(i32, i32)>(conn)
|
||||
.await
|
||||
})
|
||||
})
|
||||
.await
|
||||
@@ -86,15 +70,98 @@ async fn create_nonce(
|
||||
Error::internal("Database operation failed")
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
error!(?pubkey_bytes, "Public key not found in database");
|
||||
error!(?key, "Public key not found in database");
|
||||
Error::UnregisteredPublicKey
|
||||
})
|
||||
}
|
||||
|
||||
async fn register_key(
|
||||
db: &crate::db::DatabasePool,
|
||||
async fn verify_integrity(
|
||||
db: &DatabasePool,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> 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,
|
||||
keyholder,
|
||||
&UserAgentCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
nonce,
|
||||
},
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity verification failed");
|
||||
Error::internal("Integrity verification failed")
|
||||
})?;
|
||||
|
||||
match result {
|
||||
AttestationStatus::Attested | AttestationStatus::Unavailable => Ok(()),
|
||||
AttestationStatus::NotAttested => {
|
||||
error!(?pubkey, "Integrity verification failed: not attested");
|
||||
Err(Error::internal("Database tampering detected"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async fn create_nonce(
|
||||
db: &DatabasePool,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<i32, Error> {
|
||||
let mut db_conn = db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
let new_nonce = db_conn
|
||||
.exclusive_transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
let (id, new_nonce): (i32, i32) = update(useragent_client::table)
|
||||
.filter(useragent_client::public_key.eq(pubkey.to_stored_bytes()))
|
||||
.filter(useragent_client::key_type.eq(pubkey.key_type()))
|
||||
.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");
|
||||
Error::internal("Database operation failed")
|
||||
})?;
|
||||
|
||||
integrity::sign_entity(
|
||||
conn,
|
||||
keyholder,
|
||||
&UserAgentCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
nonce: new_nonce,
|
||||
},
|
||||
id,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(?e, "Integrity signature update failed");
|
||||
Error::internal("Database error")
|
||||
})?;
|
||||
|
||||
Result::<_, Error>::Ok(new_nonce)
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
Ok(new_nonce)
|
||||
}
|
||||
|
||||
async fn register_key(
|
||||
db: &DatabasePool,
|
||||
keyholder: &ActorRef<KeyHolder>,
|
||||
pubkey: &AuthPublicKey,
|
||||
integrity_tag: Option<Vec<u8>>,
|
||||
) -> Result<(), Error> {
|
||||
let pubkey_bytes = pubkey.to_stored_bytes();
|
||||
let key_type = pubkey.key_type();
|
||||
@@ -103,19 +170,40 @@ async fn register_key(
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
|
||||
diesel::insert_into(schema::useragent_client::table)
|
||||
.values((
|
||||
schema::useragent_client::public_key.eq(pubkey_bytes),
|
||||
schema::useragent_client::nonce.eq(1),
|
||||
schema::useragent_client::key_type.eq(key_type),
|
||||
schema::useragent_client::pubkey_integrity_tag.eq(integrity_tag),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?;
|
||||
conn.transaction(|conn| {
|
||||
Box::pin(async move {
|
||||
const NONCE_START: i32 = 1;
|
||||
|
||||
let id: i32 = diesel::insert_into(useragent_client::table)
|
||||
.values((
|
||||
useragent_client::public_key.eq(pubkey_bytes),
|
||||
useragent_client::nonce.eq(NONCE_START),
|
||||
useragent_client::key_type.eq(key_type),
|
||||
))
|
||||
.returning(useragent_client::id)
|
||||
.get_result(conn)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?;
|
||||
|
||||
let entity = UserAgentCredentials {
|
||||
pubkey: pubkey.clone(),
|
||||
nonce: NONCE_START,
|
||||
};
|
||||
|
||||
integrity::sign_entity(conn, &keyholder, &entity, id)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Failed to sign integrity tag for new user-agent key");
|
||||
Error::internal("Failed to register public key")
|
||||
})?;
|
||||
|
||||
Result::<_, Error>::Ok(())
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -141,15 +229,9 @@ where
|
||||
&mut self,
|
||||
ChallengeRequest { pubkey }: ChallengeRequest,
|
||||
) -> Result<ChallengeContext, Self::Error> {
|
||||
match self.verify_pubkey_attestation_status(&pubkey).await? {
|
||||
AttestationStatus::Attested | AttestationStatus::Unavailable => {}
|
||||
AttestationStatus::NotAttested => {
|
||||
return Err(Error::InvalidChallengeSolution);
|
||||
}
|
||||
}
|
||||
verify_integrity(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?;
|
||||
|
||||
let stored_bytes = pubkey.to_stored_bytes();
|
||||
let nonce = create_nonce(&self.conn.db, &stored_bytes, pubkey.key_type()).await?;
|
||||
let nonce = create_nonce(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?;
|
||||
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthChallenge { nonce }))
|
||||
@@ -189,22 +271,24 @@ where
|
||||
return Err(Error::InvalidBootstrapToken);
|
||||
}
|
||||
|
||||
let integrity_tag = self
|
||||
.try_sign_pubkey_integrity_tag(&pubkey)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(?err, "Failed to sign user-agent pubkey integrity tag");
|
||||
Error::internal("Failed to sign user-agent pubkey integrity tag")
|
||||
})?;
|
||||
|
||||
register_key(&self.conn.db, &pubkey, integrity_tag).await?;
|
||||
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
|
||||
Ok(pubkey)
|
||||
match token_ok {
|
||||
true => {
|
||||
register_key(&self.conn.db, &self.conn.actors.key_holder, &pubkey).await?;
|
||||
self.transport
|
||||
.send(Ok(Outbound::AuthSuccess))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
Ok(pubkey)
|
||||
}
|
||||
false => {
|
||||
error!("Invalid bootstrap token provided");
|
||||
self.transport
|
||||
.send(Err(Error::InvalidBootstrapToken))
|
||||
.await
|
||||
.map_err(|_| Error::Transport)?;
|
||||
Err(Error::InvalidBootstrapToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
@@ -264,93 +348,3 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AuthContext<'_, T>
|
||||
where
|
||||
T: Bi<super::Inbound, Result<super::Outbound, Error>> + Send,
|
||||
{
|
||||
async fn try_sign_pubkey_integrity_tag(
|
||||
&self,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<Option<Vec<u8>>, Error> {
|
||||
let signed = self
|
||||
.conn
|
||||
.actors
|
||||
.key_holder
|
||||
.ask(SignIntegrityTag {
|
||||
purpose_tag: USERAGENT_INTEGRITY_TAG.to_vec(),
|
||||
data_parts: vec![
|
||||
(pubkey.key_type() as i32).to_be_bytes().to_vec(),
|
||||
pubkey.to_stored_bytes(),
|
||||
],
|
||||
})
|
||||
.await;
|
||||
|
||||
match signed {
|
||||
Ok(tag) => Ok(Some(tag)),
|
||||
Err(SendError::HandlerError(keyholder::Error::NotBootstrapped)) => Ok(None),
|
||||
Err(SendError::HandlerError(err)) => {
|
||||
error!(
|
||||
?err,
|
||||
"Keyholder failed to sign user-agent pubkey integrity tag"
|
||||
);
|
||||
Err(Error::internal(
|
||||
"Keyholder failed to sign user-agent pubkey integrity tag",
|
||||
))
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
?err,
|
||||
"Failed to contact keyholder for user-agent pubkey integrity tag"
|
||||
);
|
||||
Err(Error::internal(
|
||||
"Failed to contact keyholder for user-agent pubkey integrity tag",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify_pubkey_attestation_status(
|
||||
&self,
|
||||
pubkey: &AuthPublicKey,
|
||||
) -> Result<AttestationStatus, Error> {
|
||||
let stored_tag: Option<Option<Vec<u8>>> = {
|
||||
let mut conn = self.conn.db.get().await.map_err(|e| {
|
||||
error!(error = ?e, "Database pool error");
|
||||
Error::internal("Database unavailable")
|
||||
})?;
|
||||
|
||||
schema::useragent_client::table
|
||||
.filter(schema::useragent_client::public_key.eq(pubkey.to_stored_bytes()))
|
||||
.filter(schema::useragent_client::key_type.eq(pubkey.key_type()))
|
||||
.select(schema::useragent_client::pubkey_integrity_tag)
|
||||
.first::<Option<Vec<u8>>>(&mut conn)
|
||||
.await
|
||||
.optional()
|
||||
.map_err(|e| {
|
||||
error!(error = ?e, "Database error");
|
||||
Error::internal("Database operation failed")
|
||||
})?
|
||||
};
|
||||
|
||||
let Some(stored_tag) = stored_tag else {
|
||||
return Err(Error::UnregisteredPublicKey);
|
||||
};
|
||||
|
||||
let Some(expected_tag) = self.try_sign_pubkey_integrity_tag(pubkey).await? else {
|
||||
return Ok(AttestationStatus::Unavailable);
|
||||
};
|
||||
|
||||
match stored_tag {
|
||||
Some(stored_tag) if stored_tag == expected_tag => Ok(AttestationStatus::Attested),
|
||||
Some(_) => {
|
||||
error!("User-agent pubkey integrity tag mismatch");
|
||||
Ok(AttestationStatus::NotAttested)
|
||||
}
|
||||
None => {
|
||||
error!("Missing pubkey integrity tag for registered key while vault is unsealed");
|
||||
Ok(AttestationStatus::NotAttested)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,65 @@
|
||||
use crate::{
|
||||
actors::{GlobalActors, client::ClientProfile},
|
||||
db::{self, models::KeyType},
|
||||
actors::{GlobalActors, client::ClientProfile}, crypto::integrity::Integrable, db::{self, models::KeyType}
|
||||
};
|
||||
|
||||
fn serialize_ecdsa<S>(key: &k256::ecdsa::VerifyingKey, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
// Serialize as hex string for easier debugging (33 bytes compressed SEC1 format)
|
||||
let key = key.to_encoded_point(true);
|
||||
let bytes = key.as_bytes();
|
||||
serializer.serialize_bytes(bytes)
|
||||
}
|
||||
|
||||
fn deserialize_ecdsa<'de, D>(deserializer: D) -> Result<k256::ecdsa::VerifyingKey, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct EcdsaVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for EcdsaVisitor {
|
||||
type Value = k256::ecdsa::VerifyingKey;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a compressed SEC1-encoded ECDSA public key")
|
||||
}
|
||||
|
||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let point = k256::EncodedPoint::from_bytes(v)
|
||||
.map_err(|_| E::custom("invalid compressed SEC1 format"))?;
|
||||
k256::ecdsa::VerifyingKey::from_encoded_point(&point)
|
||||
.map_err(|_| E::custom("invalid ECDSA public key"))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_bytes(EcdsaVisitor)
|
||||
}
|
||||
|
||||
/// Abstraction over Ed25519 / ECDSA-secp256k1 / RSA public keys used during the auth handshake.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub enum AuthPublicKey {
|
||||
Ed25519(ed25519_dalek::VerifyingKey),
|
||||
/// Compressed SEC1 public key; signature bytes are raw 64-byte (r||s).
|
||||
#[serde(serialize_with = "serialize_ecdsa", deserialize_with = "deserialize_ecdsa")]
|
||||
EcdsaSecp256k1(k256::ecdsa::VerifyingKey),
|
||||
/// RSA-2048+ public key (Windows Hello / KeyCredentialManager); signature bytes are PSS+SHA-256.
|
||||
Rsa(rsa::RsaPublicKey),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UserAgentCredentials {
|
||||
pub pubkey: AuthPublicKey,
|
||||
pub nonce: i32
|
||||
}
|
||||
|
||||
impl Integrable for UserAgentCredentials {
|
||||
const KIND: &'static str = "useragent_credentials";
|
||||
}
|
||||
|
||||
impl AuthPublicKey {
|
||||
/// Canonical bytes stored in DB and echoed back in the challenge.
|
||||
/// Ed25519: raw 32 bytes. ECDSA: SEC1 compressed 33 bytes. RSA: DER-encoded SPKI.
|
||||
@@ -91,4 +138,5 @@ pub mod auth;
|
||||
pub mod session;
|
||||
|
||||
pub use auth::authenticate;
|
||||
use serde::Serialize;
|
||||
pub use session::UserAgentSession;
|
||||
|
||||
@@ -349,9 +349,6 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(grant_id) => Ok(grant_id),
|
||||
Err(SendError::HandlerError(crate::actors::evm::Error::VaultSealed)) => {
|
||||
Err(GrantMutationError::VaultSealed)
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant create failed");
|
||||
Err(GrantMutationError::Internal)
|
||||
@@ -372,9 +369,6 @@ impl UserAgentSession {
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
Err(SendError::HandlerError(crate::actors::evm::Error::VaultSealed)) => {
|
||||
Err(GrantMutationError::VaultSealed)
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "EVM grant delete failed");
|
||||
Err(GrantMutationError::Internal)
|
||||
|
||||
Reference in New Issue
Block a user