diff --git a/matrix/Makefile.am b/matrix/Makefile.am index 5a3041a78..96c5bed0a 100644 --- a/matrix/Makefile.am +++ b/matrix/Makefile.am @@ -163,6 +163,7 @@ libircd_matrix_la_SOURCES += fed.cc libircd_matrix_la_SOURCES += fed_well_known.cc libircd_matrix_la_SOURCES += feds.cc libircd_matrix_la_SOURCES += fetch.cc +libircd_matrix_la_SOURCES += fetch_check.cc libircd_matrix_la_SOURCES += gossip.cc libircd_matrix_la_SOURCES += groups.cc libircd_matrix_la_SOURCES += request.cc diff --git a/matrix/fetch.cc b/matrix/fetch.cc index 97a1e41d1..cb0c92d13 100644 --- a/matrix/fetch.cc +++ b/matrix/fetch.cc @@ -30,8 +30,7 @@ namespace ircd::m::fetch extern log::log log; static bool timedout(const request &, const system_point &now); - static void _check_event(const request &, const m::event &); - static void check_response(const request &, const json::object &); + extern void check_response(const request &, const json::object &); static bool proffer_remote(request &, const string_view &); static bool select_remote(request &, const string_view &); static bool select_random_remote(request &); @@ -788,281 +787,6 @@ ircd::m::fetch::finish(request &request) request.promise.set_value(std::move(res)); } -namespace ircd::m::fetch -{ - static void check_response__backfill(const request &, const json::object &); - static void check_response__event(const request &, const json::object &); - static void check_response__auth(const request &, const json::object &); - - extern conf::item check_event_id; - extern conf::item check_conforms; - extern conf::item check_signature; - extern conf::item check_hashes; - extern conf::item check_authoritative_redaction; -} - -decltype(ircd::m::fetch::check_event_id) -ircd::m::fetch::check_event_id -{ - { "name", "ircd.m.fetch.check.event_id" }, - { "default", true }, -}; - -decltype(ircd::m::fetch::check_conforms) -ircd::m::fetch::check_conforms -{ - { "name", "ircd.m.fetch.check.conforms" }, - { "default", true }, -}; - -decltype(ircd::m::fetch::check_hashes) -ircd::m::fetch::check_hashes -{ - { "name", "ircd.m.fetch.check.hashes" }, - { "default", true }, -}; - -decltype(ircd::m::fetch::check_authoritative_redaction) -ircd::m::fetch::check_authoritative_redaction -{ - { "name", "ircd.m.fetch.check.authoritative_redaction" }, - { "default", true }, -}; - -decltype(ircd::m::fetch::check_signature) -ircd::m::fetch::check_signature -{ - { "name", "ircd.m.fetch.check.signature" }, - { "default", true }, - { "description", - - R"( - false - Signatures of events will not be checked by the fetch unit (they - are still checked normally during evaluation; this conf item does not - disable event signature verification for the server). - - true - Signatures of events will be checked by the fetch unit such that - bogus responses allow the fetcher to try the next server. This check might - not occur in all cases. It will only occur if the server has the public - key already; fetch unit worker contexts cannot be blocked trying to obtain - unknown keys from remote hosts. - )"}, -}; - -void -ircd::m::fetch::check_response(const request &request, - const json::object &response) -{ - switch(request.opts.op) - { - case op::backfill: - return check_response__backfill(request, response); - - case op::event: - return check_response__event(request, response); - - case op::auth: - return check_response__auth(request, response); - - case op::noop: - return; - } - - ircd::panic - { - "Unchecked response; fetch op:%u", - uint(request.opts.op), - }; -} - -void -ircd::m::fetch::check_response__auth(const request &request, - const json::object &response) -{ - const json::array &auth_chain - { - response.at("auth_chain") - }; - - for(const json::object auth_event : auth_chain) - { - m::event::id::buf event_id; - const m::event event - { - event_id, auth_event - }; - - _check_event(request, event); - } -} - -void -ircd::m::fetch::check_response__event(const request &request, - const json::object &response) -{ - const json::array &pdus - { - response.at("pdus") - }; - - const m::event event - { - pdus.at(0), request.opts.event_id - }; - - _check_event(request, event); -} - -void -ircd::m::fetch::check_response__backfill(const request &request, - const json::object &response) -{ - const json::array &pdus - { - response.at("pdus") - }; - - for(const json::object event : pdus) - { - m::event::id::buf event_id; - const m::event _event - { - event_id, event - }; - - _check_event(request, _event); - } -} - -void -ircd::m::fetch::_check_event(const request &request, - const m::event &event) -{ - if(unlikely(!request.promise)) - throw ctx::broken_promise - { - "Fetch response check interrupted." - }; - - if(request.opts.check_event_id && check_event_id && !m::check_id(event)) - { - event::id::buf buf; - const m::event &claim - { - buf, event.source - }; - - throw ircd::error - { - "event::id claim:%s != sought:%s", - string_view{claim.event_id}, - string_view{request.opts.event_id}, - }; - } - - if(request.opts.check_conforms && check_conforms) - { - m::event::conforms conforms - { - event - }; - - const bool mismatch_hashes - { - check_hashes - && request.opts.check_hashes - && conforms.has(m::event::conforms::MISMATCH_HASHES) - }; - - const bool authoritative_redaction - { - check_authoritative_redaction - && request.opts.authoritative_redaction - && mismatch_hashes - && json::get<"origin"_>(event) == request.origin - }; - - if(mismatch_hashes && !authoritative_redaction) - { - const json::object _unsigned - { - event.source["unsigned"] - }; - - const json::string redacted_by - { - _unsigned["redacted_by"] - }; - - if(valid(id::EVENT, redacted_by)) - log::dwarning - { - log, "%s claims %s redacted by %s", - request.origin, - string_view{request.opts.event_id}, - redacted_by, - }; - - //TODO: XXX - } - - if(authoritative_redaction || !mismatch_hashes) - conforms.del(m::event::conforms::MISMATCH_HASHES); - - thread_local char buf[128]; - const string_view failures - { - conforms.string(buf) - }; - - assert(failures || conforms.clean()); - if(!conforms.clean()) - throw ircd::error - { - "Non-conforming event in response :%s", - failures, - }; - } - - // only check signature for v1 events - assert(request.promise); - if(request.opts.check_signature && check_signature && request.opts.event_id.version() == "1") - { - const string_view &server - { - !json::get<"origin"_>(event)? - user::id(at<"sender"_>(event)).host(): - string_view(json::get<"origin"_>(event)) - }; - - const json::object &signatures - { - at<"signatures"_>(event).at(server) - }; - - const json::string &key_id - { - !signatures.empty()? - signatures.begin()->first: - string_view{}, - }; - - if(!key_id) - throw ircd::error - { - "Cannot find any keys for '%s' in event.signatures", - server, - }; - - if(m::keys::cache::has(server, key_id)) - if(!verify(event, server)) - throw ircd::error - { - "Signature verification failed." - }; - } -} - bool ircd::m::fetch::timedout(const request &request, const system_point &now) diff --git a/matrix/fetch_check.cc b/matrix/fetch_check.cc new file mode 100644 index 000000000..956d153d6 --- /dev/null +++ b/matrix/fetch_check.cc @@ -0,0 +1,307 @@ +// Matrix Construct +// +// Copyright (C) Matrix Construct Developers, Authors & Contributors +// Copyright (C) 2016-2022 Jason Volk +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice is present in all copies. The +// full license for this software is available in the LICENSE file. + +namespace ircd::m::fetch +{ + static void check_event_signature(const request &, const m::event &); + static void check_event_conforms(const request &, const m::event &); + static void check_event_id(const request &, const m::event &); + static void check_event(const request &, const m::event &); + static void check_response__backfill(const request &, const json::object &); + static void check_response__event(const request &, const json::object &); + static void check_response__auth(const request &, const json::object &); + extern void check_response(const request &, const json::object &); + + extern conf::item enable_check_event_id; + extern conf::item enable_check_conforms; + extern conf::item enable_check_signature; + extern conf::item enable_check_hashes; + extern conf::item enable_check_authoritative_redaction; +} + +decltype(ircd::m::fetch::enable_check_event_id) +ircd::m::fetch::enable_check_event_id +{ + { "name", "ircd.m.fetch.check.event_id" }, + { "default", true }, +}; + +decltype(ircd::m::fetch::enable_check_conforms) +ircd::m::fetch::enable_check_conforms +{ + { "name", "ircd.m.fetch.check.conforms" }, + { "default", true }, +}; + +decltype(ircd::m::fetch::enable_check_hashes) +ircd::m::fetch::enable_check_hashes +{ + { "name", "ircd.m.fetch.check.hashes" }, + { "default", true }, +}; + +decltype(ircd::m::fetch::enable_check_authoritative_redaction) +ircd::m::fetch::enable_check_authoritative_redaction +{ + { "name", "ircd.m.fetch.check.authoritative_redaction" }, + { "default", true }, +}; + +decltype(ircd::m::fetch::enable_check_signature) +ircd::m::fetch::enable_check_signature +{ + { "name", "ircd.m.fetch.check.signature" }, + { "default", true }, + { "description", + + R"( + false - Signatures of events will not be checked by the fetch unit (they + are still checked normally during evaluation; this conf item does not + disable event signature verification for the server). + + true - Signatures of events will be checked by the fetch unit such that + bogus responses allow the fetcher to try the next server. This check might + not occur in all cases. It will only occur if the server has the public + key already; fetch unit worker contexts cannot be blocked trying to obtain + unknown keys from remote hosts. + )"}, +}; + +void +ircd::m::fetch::check_response(const request &request, + const json::object &response) +{ + switch(request.opts.op) + { + case op::backfill: + return check_response__backfill(request, response); + + case op::event: + return check_response__event(request, response); + + case op::auth: + return check_response__auth(request, response); + + case op::noop: + return; + } + + ircd::panic + { + "Unchecked response; fetch op:%u", + uint(request.opts.op), + }; +} + +void +ircd::m::fetch::check_response__auth(const request &request, + const json::object &response) +{ + const json::array &auth_chain + { + response.at("auth_chain") + }; + + for(const json::object auth_event : auth_chain) + { + m::event::id::buf event_id; + const m::event event + { + event_id, auth_event + }; + + check_event(request, event); + } +} + +void +ircd::m::fetch::check_response__event(const request &request, + const json::object &response) +{ + const json::array &pdus + { + response.at("pdus") + }; + + const m::event event + { + pdus.at(0), request.opts.event_id + }; + + check_event(request, event); +} + +void +ircd::m::fetch::check_response__backfill(const request &request, + const json::object &response) +{ + const json::array &pdus + { + response.at("pdus") + }; + + for(const json::object event : pdus) + { + m::event::id::buf event_id; + const m::event _event + { + event_id, event + }; + + check_event(request, _event); + } +} + +void +ircd::m::fetch::check_event(const request &request, + const m::event &event) +{ + if(unlikely(!request.promise)) + throw ctx::broken_promise + { + "Fetch response check interrupted." + }; + + if(request.opts.check_event_id && enable_check_event_id) + check_event_id(request, event); + + if(request.opts.check_conforms && enable_check_conforms) + check_event_conforms(request, event); + + // only check signature for v1 events + assert(request.promise); + if(request.opts.check_signature && enable_check_signature && request.opts.event_id.version() == "1") + check_event_signature(request, event); +} + +void +ircd::m::fetch::check_event_id(const request &request, + const m::event &event) +{ + if(likely(m::check_id(event))) + return; + + event::id::buf buf; + const m::event &claim + { + buf, event.source + }; + + throw ircd::error + { + "event::id claim:%s != sought:%s", + string_view{claim.event_id}, + string_view{request.opts.event_id}, + }; +} + +void +ircd::m::fetch::check_event_conforms(const request &request, + const m::event &event) +{ + m::event::conforms conforms + { + event + }; + + const bool mismatch_hashes + { + enable_check_hashes + && request.opts.check_hashes + && conforms.has(m::event::conforms::MISMATCH_HASHES) + }; + + const bool authoritative_redaction + { + enable_check_authoritative_redaction + && request.opts.authoritative_redaction + && mismatch_hashes + && json::get<"origin"_>(event) == request.origin + }; + + if(mismatch_hashes && !authoritative_redaction) + { + const json::object _unsigned + { + event.source["unsigned"] + }; + + const json::string redacted_by + { + _unsigned["redacted_by"] + }; + + if(valid(id::EVENT, redacted_by)) + log::dwarning + { + log, "%s claims %s redacted by %s", + request.origin, + string_view{request.opts.event_id}, + redacted_by, + }; + + //TODO: XXX + } + + if(authoritative_redaction || !mismatch_hashes) + conforms.del(m::event::conforms::MISMATCH_HASHES); + + thread_local char buf[128]; + const string_view failures + { + conforms.string(buf) + }; + + assert(failures || conforms.clean()); + if(!conforms.clean()) + throw ircd::error + { + "Non-conforming event in response :%s", + failures, + }; +} + +void +ircd::m::fetch::check_event_signature(const request &request, + const m::event &event) +{ + const string_view &server + { + !json::get<"origin"_>(event)? + user::id(at<"sender"_>(event)).host(): + string_view(json::get<"origin"_>(event)) + }; + + const json::object &signatures + { + at<"signatures"_>(event).at(server) + }; + + const json::string &key_id + { + !signatures.empty()? + signatures.begin()->first: + string_view{}, + }; + + if(!key_id) + throw ircd::error + { + "Cannot find any keys for '%s' in event.signatures", + server, + }; + + if(m::keys::cache::has(server, key_id)) + if(!verify(event, server)) + throw ircd::error + { + "Signature verification failed." + }; +}