feat(auth): add seal-key-derived pubkey integrity tags with auth enforcement and unseal backfill
This commit is contained in:
@@ -3,9 +3,11 @@ use arbiter_server::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
bootstrap::GetToken,
|
||||
keyholder::Bootstrap,
|
||||
user_agent::{AuthPublicKey, UserAgentConnection, auth},
|
||||
},
|
||||
db::{self, schema},
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
use diesel::{ExpressionMethods as _, QueryDsl, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
@@ -165,3 +167,120 @@ pub async fn test_challenge_auth() {
|
||||
|
||||
task.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_challenge_auth_rejects_integrity_tag_mismatch_when_unsealed() {
|
||||
let db = db::create_test_pool().await;
|
||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||
|
||||
actors
|
||||
.key_holder
|
||||
.ask(Bootstrap {
|
||||
seal_key_raw: SafeCell::new(b"test-seal-key".to_vec()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
insert_into(schema::useragent_client::table)
|
||||
.values((
|
||||
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
|
||||
schema::useragent_client::key_type.eq(1i32),
|
||||
schema::useragent_client::pubkey_integrity_tag.eq(Some(vec![0u8; 32])),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
let db_for_task = db.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let mut props = UserAgentConnection::new(db_for_task, actors);
|
||||
auth::authenticate(&mut props, server_transport).await
|
||||
});
|
||||
|
||||
test_transport
|
||||
.send(auth::Inbound::AuthChallengeRequest {
|
||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||
bootstrap_token: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
task.await.unwrap(),
|
||||
Err(auth::Error::InvalidChallengeSolution)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_challenge_auth_rejects_invalid_signature() {
|
||||
let db = db::create_test_pool().await;
|
||||
let actors = GlobalActors::spawn(db.clone()).await.unwrap();
|
||||
|
||||
let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng());
|
||||
let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec();
|
||||
|
||||
// Pre-register key with key_type
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
insert_into(schema::useragent_client::table)
|
||||
.values((
|
||||
schema::useragent_client::public_key.eq(pubkey_bytes.clone()),
|
||||
schema::useragent_client::key_type.eq(1i32),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let (server_transport, mut test_transport) = ChannelTransport::new();
|
||||
let db_for_task = db.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
let mut props = UserAgentConnection::new(db_for_task, actors);
|
||||
auth::authenticate(&mut props, server_transport).await
|
||||
});
|
||||
|
||||
test_transport
|
||||
.send(auth::Inbound::AuthChallengeRequest {
|
||||
pubkey: AuthPublicKey::Ed25519(new_key.verifying_key()),
|
||||
bootstrap_token: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let response = test_transport
|
||||
.recv()
|
||||
.await
|
||||
.expect("should receive challenge");
|
||||
let challenge = match response {
|
||||
Ok(resp) => match resp {
|
||||
auth::Outbound::AuthChallenge { nonce } => nonce,
|
||||
other => panic!("Expected AuthChallenge, got {other:?}"),
|
||||
},
|
||||
Err(err) => panic!("Expected Ok response, got Err({err:?})"),
|
||||
};
|
||||
|
||||
// Sign a different challenge value so signature format is valid but verification must fail.
|
||||
let wrong_challenge = arbiter_proto::format_challenge(challenge + 1, &pubkey_bytes);
|
||||
let signature = new_key.sign(&wrong_challenge);
|
||||
|
||||
test_transport
|
||||
.send(auth::Inbound::AuthChallengeSolution {
|
||||
signature: signature.to_bytes().to_vec(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
task.await.unwrap(),
|
||||
Err(auth::Error::InvalidChallengeSolution)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@ use arbiter_server::{
|
||||
actors::{
|
||||
GlobalActors,
|
||||
keyholder::{Bootstrap, Seal},
|
||||
user_agent::{UserAgentSession, session::connection::{
|
||||
HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError,
|
||||
}},
|
||||
user_agent::{
|
||||
UserAgentSession,
|
||||
session::connection::{HandleUnsealEncryptedKey, HandleUnsealRequest, UnsealError},
|
||||
},
|
||||
},
|
||||
db,
|
||||
safe_cell::{SafeCell, SafeCellHandle as _},
|
||||
};
|
||||
use chacha20poly1305::{AeadInPlace, XChaCha20Poly1305, XNonce, aead::KeyInit};
|
||||
use diesel::{ExpressionMethods as _, QueryDsl as _, insert_into};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use kameo::actor::Spawn as _;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
@@ -149,3 +152,42 @@ pub async fn test_unseal_retry_after_invalid_key() {
|
||||
assert!(matches!(response, Ok(())));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[test_log::test]
|
||||
pub async fn test_unseal_backfills_missing_pubkey_integrity_tags() {
|
||||
let seal_key = b"test-seal-key";
|
||||
let (db, user_agent) = setup_sealed_user_agent(seal_key).await;
|
||||
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
insert_into(arbiter_server::db::schema::useragent_client::table)
|
||||
.values((
|
||||
arbiter_server::db::schema::useragent_client::public_key
|
||||
.eq(vec![1u8, 2u8, 3u8, 4u8]),
|
||||
arbiter_server::db::schema::useragent_client::key_type.eq(1i32),
|
||||
arbiter_server::db::schema::useragent_client::pubkey_integrity_tag
|
||||
.eq(Option::<Vec<u8>>::None),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let encrypted_key = client_dh_encrypt(&user_agent, seal_key).await;
|
||||
let response = user_agent.ask(encrypted_key).await;
|
||||
assert!(matches!(response, Ok(())));
|
||||
|
||||
{
|
||||
let mut conn = db.get().await.unwrap();
|
||||
let tags: Vec<Option<Vec<u8>>> = arbiter_server::db::schema::useragent_client::table
|
||||
.select(arbiter_server::db::schema::useragent_client::pubkey_integrity_tag)
|
||||
.load(&mut conn)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
tags.iter()
|
||||
.all(|tag| matches!(tag, Some(v) if v.len() == 32))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user