mirror of
https://github.com/signalapp/libsignal.git
synced 2026-04-25 17:25:18 +02:00
keytrans: Check that values in search response match expected ones
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2793,6 +2793,7 @@ dependencies = [
|
||||
"serde_with",
|
||||
"static_assertions",
|
||||
"strum",
|
||||
"subtle",
|
||||
"test-case",
|
||||
"test-log",
|
||||
"thiserror 2.0.17",
|
||||
|
||||
@@ -41,6 +41,7 @@ serde_json = { workspace = true }
|
||||
serde_with = { workspace = true, features = ["base64"] }
|
||||
static_assertions = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
subtle = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt", "time", "macros"] }
|
||||
tokio-stream = { workspace = true }
|
||||
|
||||
@@ -310,6 +310,7 @@ impl UnauthenticatedChatApi for KeyTransparencyClient<'_> {
|
||||
.inner
|
||||
.verify_chat_search_response(
|
||||
aci,
|
||||
aci_identity_key,
|
||||
e164.map(|(e164, _)| e164),
|
||||
username_hash,
|
||||
stored_account_data,
|
||||
@@ -803,6 +804,7 @@ mod test {
|
||||
|
||||
let result = kt.verify_chat_search_response(
|
||||
&aci,
|
||||
&test_account::aci_identity_key(),
|
||||
e164,
|
||||
username_hash,
|
||||
Some(account_data),
|
||||
@@ -844,6 +846,7 @@ mod test {
|
||||
|
||||
let result = kt.verify_chat_search_response(
|
||||
&aci,
|
||||
&test_account::aci_identity_key(),
|
||||
Some(e164),
|
||||
Some(username_hash),
|
||||
Some(account_data),
|
||||
|
||||
@@ -5,19 +5,23 @@
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use libsignal_core::curve::PublicKey;
|
||||
use libsignal_core::{Aci, E164};
|
||||
use libsignal_keytrans::{
|
||||
AccountData, CondensedTreeSearchResponse, FullSearchResponse, FullTreeHead, KeyTransparency,
|
||||
LastTreeHead, LocalStateUpdate, MonitoringData, SearchContext, SlimSearchRequest,
|
||||
VerifiedSearchResult,
|
||||
};
|
||||
use subtle::ConstantTimeEq as _;
|
||||
|
||||
use super::{AccountDataField, Error, MaybePartial, SearchKey, TypedSearchResponse, UsernameHash};
|
||||
|
||||
pub(super) trait KeyTransparencyVerifyExt {
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn verify_single_search_response(
|
||||
&self,
|
||||
search_key: Vec<u8>,
|
||||
expected_value: &[u8],
|
||||
response: CondensedTreeSearchResponse,
|
||||
monitoring_data: Option<MonitoringData>,
|
||||
full_tree_head: &FullTreeHead,
|
||||
@@ -26,9 +30,11 @@ pub(super) trait KeyTransparencyVerifyExt {
|
||||
now: SystemTime,
|
||||
) -> Result<VerifiedSearchResult, Error>;
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn verify_chat_search_response(
|
||||
&self,
|
||||
aci: &Aci,
|
||||
aci_identity_key: &PublicKey,
|
||||
e164: Option<E164>,
|
||||
username_hash: Option<UsernameHash>,
|
||||
stored_account_data: Option<AccountData>,
|
||||
@@ -42,6 +48,7 @@ impl KeyTransparencyVerifyExt for KeyTransparency {
|
||||
fn verify_single_search_response(
|
||||
&self,
|
||||
search_key: Vec<u8>,
|
||||
expected_value: &[u8],
|
||||
response: CondensedTreeSearchResponse,
|
||||
monitoring_data: Option<MonitoringData>,
|
||||
full_tree_head: &FullTreeHead,
|
||||
@@ -63,12 +70,14 @@ impl KeyTransparencyVerifyExt for KeyTransparency {
|
||||
true,
|
||||
now,
|
||||
)?;
|
||||
SearchValue(&result.value).check_equal(expected_value)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn verify_chat_search_response(
|
||||
&self,
|
||||
aci: &Aci,
|
||||
aci_identity_key: &PublicKey,
|
||||
e164: Option<E164>,
|
||||
username_hash: Option<UsernameHash>,
|
||||
stored_account_data: Option<AccountData>,
|
||||
@@ -103,6 +112,7 @@ impl KeyTransparencyVerifyExt for KeyTransparency {
|
||||
|
||||
let aci_result = self.verify_single_search_response(
|
||||
aci.as_search_key(),
|
||||
aci_identity_key.serialize().as_ref(),
|
||||
aci_search_response,
|
||||
aci_monitoring_data,
|
||||
&full_tree_head,
|
||||
@@ -118,6 +128,7 @@ impl KeyTransparencyVerifyExt for KeyTransparency {
|
||||
.map(|(e164, e164_search_response)| {
|
||||
self.verify_single_search_response(
|
||||
e164.as_search_key(),
|
||||
aci.service_id_binary().as_slice(),
|
||||
e164_search_response,
|
||||
e164_monitoring_data,
|
||||
&full_tree_head,
|
||||
@@ -140,6 +151,7 @@ impl KeyTransparencyVerifyExt for KeyTransparency {
|
||||
.map(|(username_hash, username_hash_response)| {
|
||||
self.verify_single_search_response(
|
||||
username_hash.as_search_key(),
|
||||
aci.service_id_binary().as_slice(),
|
||||
username_hash_response,
|
||||
username_hash_monitoring_data,
|
||||
&full_tree_head,
|
||||
@@ -213,3 +225,27 @@ fn match_optional_fields<T, U>(
|
||||
(Some(_), None) => Ok(MaybePartial::new(None, vec![field])),
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchValue<T>(T);
|
||||
|
||||
impl<T: AsRef<[u8]>> SearchValue<T> {
|
||||
const VERSION: u8 = 0;
|
||||
|
||||
fn as_bytes(&self) -> Option<&[u8]> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.split_first()
|
||||
.filter(|(version, _)| **version == Self::VERSION)
|
||||
.map(|(_, value)| value)
|
||||
}
|
||||
|
||||
pub fn check_equal(self, expected: &[u8]) -> Result<(), Error> {
|
||||
self.as_bytes()
|
||||
.filter(|returned| bool::from(returned.ct_eq(expected)))
|
||||
.map(|_| ())
|
||||
.ok_or_else(|| {
|
||||
libsignal_keytrans::Error::VerificationFailed("unexpected search value".to_string())
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user