fix: Signature verification

This commit is contained in:
timedout 2026-01-30 00:31:44 +00:00
parent c4c1481d78
commit 2e04ae947f
No known key found for this signature in database
GPG key ID: 0FA334385D0B689F
3 changed files with 82 additions and 49 deletions

1
Cargo.lock generated
View file

@ -1090,6 +1090,7 @@ dependencies = [
"core_affinity",
"ctor",
"cyborgtime",
"ed25519-dalek",
"either",
"figment",
"futures",

View file

@ -10,31 +10,31 @@ version.workspace = true
[lib]
path = "mod.rs"
crate-type = [
"rlib",
# "dylib",
"rlib",
# "dylib",
]
[features]
brotli_compression = [
"reqwest/brotli",
"reqwest/brotli",
]
conduwuit_mods = [
"dep:libloading"
]
gzip_compression = [
"reqwest/gzip",
"reqwest/gzip",
]
hardened_malloc = [
"dep:hardened_malloc-rs"
"dep:hardened_malloc-rs"
]
jemalloc = [
"dep:tikv-jemalloc-sys",
"dep:tikv-jemalloc-ctl",
"dep:tikv-jemallocator",
"dep:tikv-jemalloc-sys",
"dep:tikv-jemalloc-ctl",
"dep:tikv-jemallocator",
]
jemalloc_conf = []
jemalloc_prof = [
"tikv-jemalloc-sys/profiling",
"tikv-jemalloc-sys/profiling",
]
jemalloc_stats = [
"tikv-jemalloc-sys/stats",
@ -43,10 +43,10 @@ jemalloc_stats = [
]
perf_measurements = []
release_max_log_level = [
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
"tracing/max_level_trace",
"tracing/release_max_level_info",
"log/max_level_trace",
"log/release_max_level_info",
]
sentry_telemetry = []
zstd_compression = [
@ -110,6 +110,7 @@ tracing.workspace = true
url.workspace = true
parking_lot.workspace = true
lock_api.workspace = true
ed25519-dalek = "~2"
[target.'cfg(unix)'.dependencies]
nix.workspace = true

View file

@ -1,5 +1,6 @@
use std::{borrow::Borrow, collections::BTreeSet};
use ed25519_dalek::{Verifier, VerifyingKey};
use futures::{
Future,
future::{OptionFuture, join, join3},
@ -11,17 +12,20 @@ use ruma::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, ThirdPartyInvite},
power_levels::RoomPowerLevelsEventContent,
third_party_invite::RoomThirdPartyInviteEventContent,
third_party_invite::{PublicKey, RoomThirdPartyInviteEventContent},
},
int,
serde::{Base64, Raw},
signatures::{PublicKeyMap, PublicKeySet, verify_json},
serde::{Base64, Raw, base64::Standard},
signatures::{ParseError, VerificationError},
};
use serde::{
Deserialize,
de::{Error as _, IgnoredAny},
};
use serde_json::{from_str as from_json_str, value::RawValue as RawJsonValue};
use serde_json::{
from_str as from_json_str,
value::{RawValue as RawJsonValue, to_raw_value},
};
use super::{
Error, Event, Result, StateEventType, StateKey, TimelineEventType,
@ -31,7 +35,7 @@ use super::{
},
room_version::RoomVersion,
};
use crate::{debug, error, trace, utils::to_canonical_object, warn};
use crate::{debug, error, trace, warn};
// FIXME: field extracting could be bundled for `content`
#[derive(Deserialize)]
@ -1497,6 +1501,17 @@ fn get_send_level(
.unwrap_or_else(|| if state_key.is_some() { int!(50) } else { int!(0) })
}
fn verify_payload(pk: &[u8], sig: &[u8], c: &[u8]) -> Result<(), ruma::signatures::Error> {
VerifyingKey::from_bytes(
pk.try_into()
.map_err(|_| ParseError::PublicKey(ed25519_dalek::SignatureError::new()))?,
)
.map_err(ParseError::PublicKey)?
.verify(c, &sig.try_into().map_err(ParseError::Signature)?)
.map_err(VerificationError::Signature)
.map_err(ruma::signatures::Error::from)
}
/// Checks a third-party invite is valid.
async fn verify_third_party_invite<F, Fut, E>(
target_current_membership: MembershipState,
@ -1554,45 +1569,61 @@ where
// content of m.room.third_party_invite as:
// 1. A single public key in the public_key property.
// 2. A list of public keys in the public_keys property.
if third_party_invite_event
.get_content::<RoomThirdPartyInviteEventContent>()
.is_err()
{
let Ok(tpi_content) =
third_party_invite_event.get_content::<RoomThirdPartyInviteEventContent>()
else {
warn!("m.room.third_party_invite event has invalid content");
return false;
}
};
let Some(signatures) = signed.get("signatures").and_then(|v| v.as_object()) else {
warn!("invite event third_party_invite signed missing/invalid signatures");
return false;
};
let mut public_key_map = PublicKeyMap::new();
for (server_name, sig_map) in signatures {
let mut pk_set = PublicKeySet::new();
if let Some(sig_map) = sig_map.as_object() {
for (key_id, sig) in sig_map {
let Some(sig_str) = sig.as_str() else {
warn!(
"invite event third_party_invite signature is not a string or is missing"
);
return false;
};
let Ok(sig_b64) = Base64::parse(sig_str) else {
warn!("invite event third_party_invite signature is not valid Base64");
return false;
};
pk_set.insert(key_id.clone(), sig_b64);
let mut public_keys = tpi_content.public_keys.unwrap_or_default();
public_keys.push(PublicKey {
public_key: tpi_content.public_key,
key_validity_url: Some(tpi_content.key_validity_url),
});
for pk in public_keys {
// signatures -> { server_name: { ed25519:N: signature } }
for (server_name, server_sigs) in signatures {
if let Some(server_sigs) = server_sigs.as_object() {
for (key_id, signature_value) in server_sigs {
if let Some(signature_str) = signature_value.as_str() {
if let Ok(signature) = Base64::<Standard>::parse(signature_str) {
debug!(
?pk,
%server_name,
%key_id,
"verifying third-party invite signature",
);
match verify_payload(
pk.public_key.as_bytes(),
signature.as_bytes(),
to_raw_value(signed).unwrap().get().as_bytes(),
) {
| Ok(()) => {
debug!("valid third-party invite signature found");
return true;
},
| Err(e) => {
warn!(
?pk,
%server_name,
%key_id,
"invalid third-party invite signature: {e}",
);
},
}
}
}
}
}
}
public_key_map.insert(server_name.clone(), pk_set);
}
if let Err(e) = verify_json(
&public_key_map,
to_canonical_object(signed).expect("signed was already validated"),
) {
warn!("invite event third_party_invite signature verification failed: {e}");
return false;
}
// If there was no error, there was a valid signature, so allow.
true
warn!("no valid signature found for third-party invite");
false
}