fix: Signature verification

This commit is contained in:
timedout
2026-01-30 00:31:44 +00:00
parent c4c1481d78
commit 2e04ae947f
3 changed files with 82 additions and 49 deletions
Generated
+1
View File
@@ -1090,6 +1090,7 @@ dependencies = [
"core_affinity", "core_affinity",
"ctor", "ctor",
"cyborgtime", "cyborgtime",
"ed25519-dalek",
"either", "either",
"figment", "figment",
"futures", "futures",
+14 -13
View File
@@ -10,31 +10,31 @@ version.workspace = true
[lib] [lib]
path = "mod.rs" path = "mod.rs"
crate-type = [ crate-type = [
"rlib", "rlib",
# "dylib", # "dylib",
] ]
[features] [features]
brotli_compression = [ brotli_compression = [
"reqwest/brotli", "reqwest/brotli",
] ]
conduwuit_mods = [ conduwuit_mods = [
"dep:libloading" "dep:libloading"
] ]
gzip_compression = [ gzip_compression = [
"reqwest/gzip", "reqwest/gzip",
] ]
hardened_malloc = [ hardened_malloc = [
"dep:hardened_malloc-rs" "dep:hardened_malloc-rs"
] ]
jemalloc = [ jemalloc = [
"dep:tikv-jemalloc-sys", "dep:tikv-jemalloc-sys",
"dep:tikv-jemalloc-ctl", "dep:tikv-jemalloc-ctl",
"dep:tikv-jemallocator", "dep:tikv-jemallocator",
] ]
jemalloc_conf = [] jemalloc_conf = []
jemalloc_prof = [ jemalloc_prof = [
"tikv-jemalloc-sys/profiling", "tikv-jemalloc-sys/profiling",
] ]
jemalloc_stats = [ jemalloc_stats = [
"tikv-jemalloc-sys/stats", "tikv-jemalloc-sys/stats",
@@ -43,10 +43,10 @@ jemalloc_stats = [
] ]
perf_measurements = [] perf_measurements = []
release_max_log_level = [ release_max_log_level = [
"tracing/max_level_trace", "tracing/max_level_trace",
"tracing/release_max_level_info", "tracing/release_max_level_info",
"log/max_level_trace", "log/max_level_trace",
"log/release_max_level_info", "log/release_max_level_info",
] ]
sentry_telemetry = [] sentry_telemetry = []
zstd_compression = [ zstd_compression = [
@@ -110,6 +110,7 @@ tracing.workspace = true
url.workspace = true url.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
lock_api.workspace = true lock_api.workspace = true
ed25519-dalek = "~2"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
nix.workspace = true nix.workspace = true
+67 -36
View File
@@ -1,5 +1,6 @@
use std::{borrow::Borrow, collections::BTreeSet}; use std::{borrow::Borrow, collections::BTreeSet};
use ed25519_dalek::{Verifier, VerifyingKey};
use futures::{ use futures::{
Future, Future,
future::{OptionFuture, join, join3}, future::{OptionFuture, join, join3},
@@ -11,17 +12,20 @@ use ruma::{
join_rules::{JoinRule, RoomJoinRulesEventContent}, join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, ThirdPartyInvite}, member::{MembershipState, ThirdPartyInvite},
power_levels::RoomPowerLevelsEventContent, power_levels::RoomPowerLevelsEventContent,
third_party_invite::RoomThirdPartyInviteEventContent, third_party_invite::{PublicKey, RoomThirdPartyInviteEventContent},
}, },
int, int,
serde::{Base64, Raw}, serde::{Base64, Raw, base64::Standard},
signatures::{PublicKeyMap, PublicKeySet, verify_json}, signatures::{ParseError, VerificationError},
}; };
use serde::{ use serde::{
Deserialize, Deserialize,
de::{Error as _, IgnoredAny}, 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::{ use super::{
Error, Event, Result, StateEventType, StateKey, TimelineEventType, Error, Event, Result, StateEventType, StateKey, TimelineEventType,
@@ -31,7 +35,7 @@ use super::{
}, },
room_version::RoomVersion, 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` // FIXME: field extracting could be bundled for `content`
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -1497,6 +1501,17 @@ fn get_send_level(
.unwrap_or_else(|| if state_key.is_some() { int!(50) } else { int!(0) }) .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. /// Checks a third-party invite is valid.
async fn verify_third_party_invite<F, Fut, E>( async fn verify_third_party_invite<F, Fut, E>(
target_current_membership: MembershipState, target_current_membership: MembershipState,
@@ -1554,45 +1569,61 @@ where
// content of m.room.third_party_invite as: // content of m.room.third_party_invite as:
// 1. A single public key in the public_key property. // 1. A single public key in the public_key property.
// 2. A list of public keys in the public_keys property. // 2. A list of public keys in the public_keys property.
if third_party_invite_event let Ok(tpi_content) =
.get_content::<RoomThirdPartyInviteEventContent>() third_party_invite_event.get_content::<RoomThirdPartyInviteEventContent>()
.is_err() else {
{
warn!("m.room.third_party_invite event has invalid content"); warn!("m.room.third_party_invite event has invalid content");
return false; return false;
} };
let Some(signatures) = signed.get("signatures").and_then(|v| v.as_object()) else { let Some(signatures) = signed.get("signatures").and_then(|v| v.as_object()) else {
warn!("invite event third_party_invite signed missing/invalid signatures"); warn!("invite event third_party_invite signed missing/invalid signatures");
return false; return false;
}; };
let mut public_key_map = PublicKeyMap::new(); let mut public_keys = tpi_content.public_keys.unwrap_or_default();
for (server_name, sig_map) in signatures { public_keys.push(PublicKey {
let mut pk_set = PublicKeySet::new(); public_key: tpi_content.public_key,
if let Some(sig_map) = sig_map.as_object() { key_validity_url: Some(tpi_content.key_validity_url),
for (key_id, sig) in sig_map { });
let Some(sig_str) = sig.as_str() else {
warn!( for pk in public_keys {
"invite event third_party_invite signature is not a string or is missing" // signatures -> { server_name: { ed25519:N: signature } }
); for (server_name, server_sigs) in signatures {
return false; if let Some(server_sigs) = server_sigs.as_object() {
}; for (key_id, signature_value) in server_sigs {
let Ok(sig_b64) = Base64::parse(sig_str) else { if let Some(signature_str) = signature_value.as_str() {
warn!("invite event third_party_invite signature is not valid Base64"); if let Ok(signature) = Base64::<Standard>::parse(signature_str) {
return false; debug!(
}; ?pk,
pk_set.insert(key_id.clone(), sig_b64); %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, warn!("no valid signature found for third-party invite");
to_canonical_object(signed).expect("signed was already validated"), false
) {
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
} }