From 31b778ee0be6db6cf72cc99208a8de7e14ab8cd9 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Sat, 25 Nov 2017 15:30:03 -0700 Subject: [PATCH] ircd::m: Improve server keys related functions. --- include/ircd/m/keys.h | 37 ++- ircd/m.cc | 601 +++++++++++++------------------------ ircd/m_io.cc | 264 ++++++++++++++++ modules/client/register.cc | 2 +- 4 files changed, 505 insertions(+), 399 deletions(-) diff --git a/include/ircd/m/keys.h b/include/ircd/m/keys.h index a5cf4c710..5905088e5 100644 --- a/include/ircd/m/keys.h +++ b/include/ircd/m/keys.h @@ -29,7 +29,7 @@ namespace ircd::m { - struct key; + struct keys; } namespace ircd::m::self @@ -53,6 +53,16 @@ namespace ircd::m::name constexpr const char *const valid_until_ts {"valid_until_ts"}; } +/// Contains the public keys and proof of identity for a remote server. +/// +/// A user who wishes to verify a signature from a remote server must have +/// the name of the server (origin) and the key_id. Calling the appropriate +/// static function of this class will attempt to fetch the key from the db +/// or make network requests, with valid response being saved to the db. Keys +/// are thus managed internally so the user doesn't supply a buffer or ever +/// construct this object; instead this object backed by internal db data is +/// presented in the supplied synchronous closure. +/// /// 2.2.1.1 Publishing Keys /// /// Key Type, Description @@ -63,7 +73,7 @@ namespace ircd::m::name /// tls_fingerprints Array of Objects, Hashes of X.509 TLS certificates used by this this server encoded as Unpadded Base64. /// valid_until_ts Integer, POSIX timestamp when the list of valid keys should be refreshed. /// -struct ircd::m::key +struct ircd::m::keys :json::tuple < json::property, @@ -74,21 +84,22 @@ struct ircd::m::key json::property > { - static room keys; + using key_closure = std::function; // remember to unquote()!!! + using keys_closure = std::function; - bool verify() const noexcept; + static m::room room; + + static bool get_local(const string_view &server_name, const keys_closure &); + static bool verify(const keys &) noexcept; + static void set(const keys &); + + public: + static void get(const string_view &server_name, const string_view &key_id, const string_view &query_server, const keys_closure &); + static void get(const string_view &server_name, const string_view &key_id, const key_closure &); + static void get(const string_view &server_name, const keys_closure &); using super_type::tuple; using super_type::operator=; }; -namespace ircd::m::keys -{ - using closure = std::function; - - void set(const key &); - bool get(const string_view &server_name, const string_view &key_id, const closure &); - bool get(const string_view &server_name, const closure &); -} - #pragma GCC diagnostic pop diff --git a/ircd/m.cc b/ircd/m.cc index 5420ab9bb..6464d63a9 100644 --- a/ircd/m.cc +++ b/ircd/m.cc @@ -251,276 +251,6 @@ ircd::m::dbs::init_modules() modules.emplace(name, name); } -/////////////////////////////////////////////////////////////////////////////// -// -// m/session.h -// - -ircd::m::io::session::session(const net::remote &remote) -:server{remote} -,destination{remote.hostname} -{ -} - -ircd::json::object -ircd::m::io::session::operator()(parse::buffer &pb, - request &request) -{ - request.destination = destination; - request(server); - return response - { - server, pb - }; -} - -// -// response -// - -ircd::m::io::response::response(server &server, - parse::buffer &pb) -{ - http::code status; - json::object &object - { - static_cast(*this) - }; - - parse::capstan pc - { - pb, read_closure(server) - }; - - http::response - { - pc, - nullptr, - [&pc, &status, &object](const http::response::head &head) - { - status = http::status(head.status); - object = http::response::content{pc, head}; - }, - [](const auto &header) - { - //std::cout << header.first << " :" << header.second << std::endl; - } - }; - - if(status < 200 || status >= 300) - throw m::error(status, object); -} - -// -// request -// - -namespace ircd::m::name -{ -// constexpr const char *const content {"content"}; - constexpr const char *const destination {"destination"}; - constexpr const char *const method {"method"}; -// constexpr const char *const origin {"origin"}; - constexpr const char *const uri {"uri"}; -} - -struct ircd::m::io::request::authorization -:json::tuple -< - json::property, - json::property, - json::property, - json::property, - json::property -> -{ - string_view generate(const mutable_buffer &out); - - using super_type::tuple; -}; - -void -ircd::m::io::request::operator()(const vector_view &addl_headers) -const -{ -} - -void -ircd::m::io::request::operator()(server &server, - const vector_view &addl_headers) -const -{ - const size_t addl_headers_size - { - std::min(addl_headers.size(), size_t(64UL)) - }; - - size_t headers{2 + addl_headers_size}; - http::line::header header[headers + 1] - { - { "User-Agent", BRANDING_NAME " (IRCd " BRANDING_VERSION ")" }, - { "Content-Type", "application/json" }, - }; - - for(size_t i(0); i < addl_headers_size; ++i) - header[headers++] = addl_headers.at(i); - - char x_matrix[1024]; - if(startswith(path, "_matrix/federation")) - header[headers++] = - { - "Authorization", generate_authorization(x_matrix) - }; - - http::request - { - destination, - method, - path, - query, - content, - write_closure(server), - { header, headers } - }; -} - -ircd::string_view -ircd::m::io::request::generate_authorization(const mutable_buffer &out) -const -{ - const fmt::bsprintf<2048> uri - { - "/%s%s%s", lstrip(path, '/'), query? "?" : "", query - }; - - request::authorization authorization - { - json::members - { - { "destination", destination }, - { "method", method }, - { "origin", my_host() }, - { "uri", uri }, - } - }; - - if(string_view{content}.size() > 2) - json::get<"content"_>(authorization) = content; - - return authorization.generate(out); -} - -ircd::string_view -ircd::m::io::request::authorization::generate(const mutable_buffer &out) -{ - // Any buffers here can be comfortably large if they're not on a stack and - // nothing in this procedure has a yield which risks decohering static - // buffers; the assertion is tripped if so. - ctx::critical_assertion ca; - - static fixed_buffer request_object_buf; - const auto request_object - { - json::stringify(request_object_buf, *this) - }; - - const ed25519::sig sig - { - self::secret_key.sign(request_object) - }; - - static fixed_buffer signature_buf; - const auto x_matrix_len - { - fmt::sprintf(out, "X-Matrix origin=%s,key=\"%s\",sig=\"%s\"", - unquote(string_view{at<"origin"_>(*this)}), - self::public_key_id, - b64encode_unpadded(signature_buf, sig)) - }; - - return - { - data(out), size_t(x_matrix_len) - }; -} - -bool -ircd::m::io::verify_x_matrix_authorization(const string_view &x_matrix, - const string_view &method, - const string_view &uri, - const string_view &content) -{ - string_view tokens[3], origin, key, sig; - if(ircd::tokens(split(x_matrix, ' ').second, ',', tokens) != 3) - return false; - - for(const auto &token : tokens) - { - const auto &key_value - { - split(token, '=') - }; - - switch(hash(key_value.first)) - { - case hash("origin"): origin = unquote(key_value.second); break; - case hash("key"): key = unquote(key_value.second); break; - case hash("sig"): sig = unquote(key_value.second); break; - } - } - - request::authorization authorization - { - json::members - { - { "destination", my_host() }, - { "method", method }, - { "origin", origin }, - { "uri", uri }, - } - }; - - if(content.size() > 2) - json::get<"content"_>(authorization) = content; - - //TODO: XXX - const json::strung request_object - { - authorization - }; - - const ed25519::sig _sig - { - [&sig](auto &buf) - { - b64decode(buf, sig); - } - }; - - const ed25519::pk pk - { - [&origin, &key](auto &buf) - { - m::keys::get(origin, key, [&key, &buf](const auto &keys) - { - const json::object vks - { - at<"verify_keys"_>(keys) - }; - - const json::object vkk - { - vks.at(key) - }; - - b64decode(buf, unquote(vkk.at("key"))); - }); - } - }; - - return pk.verify(const_raw_buffer{request_object}, _sig); -} - /////////////////////////////////////////////////////////////////////////////// // // m/keys.h @@ -532,8 +262,10 @@ keys_room_id "keys", ircd::my_host() }; +/// The keys room is where the public key data for each server is stored as +/// state indexed by the server name. ircd::m::room -ircd::m::key::keys +ircd::m::keys::room { keys_room_id }; @@ -636,7 +368,7 @@ ircd::m::init_keys(const json::object &options) static void ircd::m::bootstrap_keys() { - create(key::keys, me.user_id); + create(keys::room, me.user_id); const json::strung verify_keys { @@ -649,7 +381,7 @@ ircd::m::bootstrap_keys() }} }; - key my_key; + keys my_key; json::get<"verify_keys"_>(my_key) = verify_keys; json::get<"server_name"_>(my_key) = my_host(); json::get<"old_verify_keys"_>(my_key) = "{}"; @@ -695,25 +427,209 @@ ircd::m::bootstrap_keys() keys::set(my_key); } -bool -ircd::m::keys::get(const string_view &server_name, - const closure &closure) -{ - return get(server_name, string_view{}, closure); -} - -bool +void ircd::m::keys::get(const string_view &server_name, const string_view &key_id, - const closure &closure) + const key_closure &closure) +try +{ + get(server_name, [&key_id, &closure](const keys &keys) + { + const json::object vks + { + at<"verify_keys"_>(keys) + }; + + const json::object vkk + { + vks.at(key_id) + }; + + // The key is not unquote() because some types of keys may be + // more complex than just a string one day; think: RLWE. + const string_view &key + { + vkk.at("key") + }; + + closure(key); + }); +} +catch(const json::not_found &e) +{ + throw m::NOT_FOUND + { + "Failed to find key '%s' for '%s': %s", + key_id, + server_name, + e.what() + }; +} + +void +ircd::m::keys::get(const string_view &server_name, + const keys_closure &closure) { assert(!server_name.empty()); + if(get_local(server_name, closure)) + return; + + if(server_name == my_host()) + throw m::NOT_FOUND + { + "keys for '%s' (that's myself) not found", server_name + }; + + log.debug("Keys for %s not cached; querying network...", + server_name); + + char url[1024]; const auto url_len + { + fmt::snprintf(url, sizeof(url), "_matrix/key/v2/server/") + }; + + //TODO: XXX + const unique_buffer buffer + { + 8192 + }; + + ircd::parse::buffer pb{mutable_buffer{buffer}}; + m::request request{"GET", url, {}, {}}; + m::session session{server_name}; + const json::object response + { + session(pb, request) + }; + + const m::keys &keys + { + response + }; + + if(!verify(keys)) + throw m::error + { + http::UNAUTHORIZED, "M_INVALID_SIGNATURE", + "Failed to verify keys for '%s'", server_name + }; + + log.debug("Verified keys from '%s'", + server_name); + + set(keys); + closure(keys); +} + +void +ircd::m::keys::get(const string_view &server_name, + const string_view &key_id, + const string_view &query_server, + const keys_closure &closure) +try +{ + assert(!server_name.empty()); + assert(!query_server.empty()); + + char key_id_buf[1024]; + char server_name_buf[1024]; + char url[1024]; const auto url_len + { + fmt::snprintf(url, sizeof(url), "_matrix/key/v2/query/%s/%s/", + urlencode(server_name, server_name_buf), + urlencode(key_id, key_id_buf)) + }; + + //TODO: XXX + const unique_buffer buffer + { + 8192 + }; + + // Make request and receive response synchronously. + // This ircd::ctx will block here fetching. + ircd::parse::buffer pb{mutable_buffer{buffer}}; + m::request request{"GET", url, {}, {}}; + m::session session{server_name}; + const json::object response + { + session(pb, request) + }; + + const json::array &keys + { + response.at("server_keys") + }; + + log::debug("Fetched %zu candidate keys seeking '%s' for '%s' from '%s' (%s)", + keys.count(), + empty(key_id)? "*" : key_id, + server_name, + query_server, + string(net::remote(session.server))); + + bool ret{false}; + for(auto it(begin(keys)); it != end(keys); ++it) + { + const m::keys &keys{*it}; + const auto &_server_name + { + at<"server_name"_>(keys) + }; + + if(!verify(keys)) + throw m::error + { + http::UNAUTHORIZED, "M_INVALID_SIGNATURE", + "Failed to verify keys for '%s' from '%s'", + _server_name, + query_server + }; + + log.debug("Verified keys for '%s' from '%s'", + _server_name, + query_server); + + set(keys); + const json::object vks{json::get<"verify_keys"_>(keys)}; + if(_server_name == server_name) + { + closure(keys); + ret = true; + } + } + + if(!ret) + throw m::NOT_FOUND + { + "Failed to get any keys for '%s' from '%s' (got %zu total keys otherwise)", + server_name, + query_server, + keys.count() + }; +} +catch(const json::not_found &e) +{ + throw m::NOT_FOUND + { + "Failed to find key '%s' for '%s' when querying '%s': %s", + key_id, + server_name, + query_server, + e.what() + }; +} + +bool +ircd::m::keys::get_local(const string_view &server_name, + const keys_closure &closure) +{ const m::vm::query query { - { "room_id", key::keys.room_id }, - { "type", "ircd.key" }, - { "state_key", server_name }, + { "room_id", keys::room.room_id }, + { "type", "ircd.key" }, + { "state_key", server_name }, }; const auto have @@ -725,110 +641,25 @@ ircd::m::keys::get(const string_view &server_name, } }; - if(m::vm::test(query, have)) - return true; - - if(server_name == my_host()) - throw m::NOT_FOUND - { - "key '%s' for '%s' not found", key_id?: "", server_name - }; - - log.debug("Key %s for %s not cached; querying network...", - key_id?: "", - server_name); - - char key_id_buf[1024], server_name_buf[1024]; - char url[1024]; const auto url_len - { -/* - fmt::snprintf(url, sizeof(url), "_matrix/key/v2/query/%s/%s", - server_name, - key_id): -*/ - fmt::snprintf(url, sizeof(url), "_matrix/key/v2/server/%s", - urlencode(key_id, key_id_buf)) - }; - - //TODO: XXX - const unique_buffer buffer - { - 8192 - }; - - ircd::parse::buffer pb{mutable_buffer{buffer}}; - m::request request - { - "GET", url, {}, {} - }; - - m::session session - { - server_name - }; - - const string_view response - { - session(pb, request) - }; - -/* - const json::array &keys - { - response.at("server_keys") - }; - - log::debug("Fetched %zu candidate keys from '%s' (%s)", - keys.size(), - server_name, - string(remote(*session.client))); - - if(keys.empty()) - throw m::NOT_FOUND - { - "Failed to get key '%s' for '%s'", key_id, server_name - }; - - const m::key &key - { - keys[0] - }; -*/ - const m::key &key - { - response - }; - - if(!key.verify()) - throw m::error - { - http::UNAUTHORIZED, "M_INVALID_SIGNATURE", "Failed to verify key from '%s'", server_name - }; - - log.debug("Verified key from '%s'", - server_name); - - m::keys::set(key); - closure(key); - return true; + return m::vm::test(query, have); } void -ircd::m::keys::set(const key &key) +ircd::m::keys::set(const keys &keys) { const auto &state_key { - unquote(at<"server_name"_>(key)) + unquote(at<"server_name"_>(keys)) }; const m::user::id::buf sender { - "ircd", unquote(at<"server_name"_>(key)) + "ircd", unquote(at<"server_name"_>(keys)) }; const json::strung content { - key + keys }; json::iov event; @@ -840,17 +671,17 @@ ircd::m::keys::set(const key &key) { event, json::member { "content", content }} }; - key::keys.send(event); + keys::room.send(event); } /// Verify this key data (with itself). bool -ircd::m::key::verify() -const noexcept try +ircd::m::keys::verify(const keys &keys) +noexcept try { const auto &valid_until_ts { - at<"valid_until_ts"_>(*this) + at<"valid_until_ts"_>(keys) }; if(valid_until_ts < ircd::time()) @@ -858,7 +689,7 @@ const noexcept try const json::object &verify_keys { - at<"verify_keys"_>(*this) + at<"verify_keys"_>(keys) }; const string_view &key_id @@ -881,12 +712,12 @@ const noexcept try const json::object &signatures { - at<"signatures"_>(*this) + at<"signatures"_>(keys) }; const string_view &server_name { - unquote(at<"server_name"_>(*this)) + unquote(at<"server_name"_>(keys)) }; const json::object &server_signatures @@ -900,15 +731,15 @@ const noexcept try }}; ///TODO: XXX - m::key copy{*this}; + m::keys copy{keys}; at<"signatures"_>(copy) = string_view{}; - const std::string preimage{json::strung(copy)}; + const json::strung preimage{copy}; return pk.verify(const_raw_buffer{preimage}, sig); } catch(const std::exception &e) { log.error("key verification for '%s' failed: %s", - json::get<"server_name"_>(*this, ""_sv), + json::get<"server_name"_>(keys, ""_sv), e.what()); return false; diff --git a/ircd/m_io.cc b/ircd/m_io.cc index c00ce0320..9f86d1b8e 100644 --- a/ircd/m_io.cc +++ b/ircd/m_io.cc @@ -517,9 +517,273 @@ try }}; return m::vm::test(query, test); +*/ + return false; } catch(const std::exception &e) { tab.error = std::make_exception_ptr(e); return false; } + + +/////////////////////////////////////////////////////////////////////////////// +// +// m/session.h +// + +ircd::m::io::session::session(const net::remote &remote) +:server{remote} +,destination{remote.hostname} +{ +} + +ircd::json::object +ircd::m::io::session::operator()(parse::buffer &pb, + request &request) +{ + request.destination = destination; + request(server); + return response + { + server, pb + }; +} + +// +// response +// + +ircd::m::io::response::response(server &server, + parse::buffer &pb) +{ + http::code status; + json::object &object + { + static_cast(*this) + }; + + parse::capstan pc + { + pb, read_closure(server) + }; + + http::response + { + pc, + nullptr, + [&pc, &status, &object](const http::response::head &head) + { + status = http::status(head.status); + object = http::response::content{pc, head}; + }, + [](const auto &header) + { + //std::cout << header.first << " :" << header.second << std::endl; + } + }; + + if(status < 200 || status >= 300) + throw m::error(status, object); +} + +// +// request +// + +namespace ircd::m::name +{ +// constexpr const char *const content {"content"}; + constexpr const char *const destination {"destination"}; + constexpr const char *const method {"method"}; +// constexpr const char *const origin {"origin"}; + constexpr const char *const uri {"uri"}; +} + +struct ircd::m::io::request::authorization +:json::tuple +< + json::property, + json::property, + json::property, + json::property, + json::property +> +{ + string_view generate(const mutable_buffer &out); + + using super_type::tuple; +}; + +void +ircd::m::io::request::operator()(const vector_view &addl_headers) +const +{ +} + +void +ircd::m::io::request::operator()(server &server, + const vector_view &addl_headers) +const +{ + const size_t addl_headers_size + { + std::min(addl_headers.size(), size_t(64UL)) + }; + + size_t headers{2 + addl_headers_size}; + http::line::header header[headers + 1] + { + { "User-Agent", BRANDING_NAME " (IRCd " BRANDING_VERSION ")" }, + { "Content-Type", "application/json" }, + }; + + for(size_t i(0); i < addl_headers_size; ++i) + header[headers++] = addl_headers.at(i); + + char x_matrix[1024]; + if(startswith(path, "_matrix/federation")) + header[headers++] = + { + "Authorization", generate_authorization(x_matrix) + }; + + http::request + { + destination, + method, + path, + query, + content, + write_closure(server), + { header, headers } + }; +} + +ircd::string_view +ircd::m::io::request::generate_authorization(const mutable_buffer &out) +const +{ + const fmt::bsprintf<2048> uri + { + "/%s%s%s", lstrip(path, '/'), query? "?" : "", query + }; + + request::authorization authorization + { + json::members + { + { "destination", destination }, + { "method", method }, + { "origin", my_host() }, + { "uri", uri }, + } + }; + + if(string_view{content}.size() > 2) + json::get<"content"_>(authorization) = content; + + return authorization.generate(out); +} + +ircd::string_view +ircd::m::io::request::authorization::generate(const mutable_buffer &out) +{ + // Any buffers here can be comfortably large if they're not on a stack and + // nothing in this procedure has a yield which risks decohering static + // buffers; the assertion is tripped if so. + ctx::critical_assertion ca; + + static fixed_buffer request_object_buf; + const auto request_object + { + json::stringify(request_object_buf, *this) + }; + + const ed25519::sig sig + { + self::secret_key.sign(request_object) + }; + + static fixed_buffer signature_buf; + const auto x_matrix_len + { + fmt::sprintf(out, "X-Matrix origin=%s,key=\"%s\",sig=\"%s\"", + unquote(string_view{at<"origin"_>(*this)}), + self::public_key_id, + b64encode_unpadded(signature_buf, sig)) + }; + + return + { + data(out), size_t(x_matrix_len) + }; +} + +bool +ircd::m::io::verify_x_matrix_authorization(const string_view &x_matrix, + const string_view &method, + const string_view &uri, + const string_view &content) +{ + string_view tokens[3], origin, key, sig; + if(ircd::tokens(split(x_matrix, ' ').second, ',', tokens) != 3) + return false; + + for(const auto &token : tokens) + { + const auto &key_value + { + split(token, '=') + }; + + switch(hash(key_value.first)) + { + case hash("origin"): origin = unquote(key_value.second); break; + case hash("key"): key = unquote(key_value.second); break; + case hash("sig"): sig = unquote(key_value.second); break; + } + } + + request::authorization authorization + { + json::members + { + { "destination", my_host() }, + { "method", method }, + { "origin", origin }, + { "uri", uri }, + } + }; + + if(content.size() > 2) + json::get<"content"_>(authorization) = content; + + //TODO: XXX + const json::strung request_object + { + authorization + }; + + const ed25519::sig _sig + { + [&sig](auto &buf) + { + b64decode(buf, sig); + } + }; + + const ed25519::pk pk + { + [&origin, &key](auto &buf) + { + m::keys::get(origin, key, [&buf] + (const string_view &key) + { + b64decode(buf, unquote(key)); + }); + } + }; + + return pk.verify(const_raw_buffer{request_object}, _sig); +} diff --git a/modules/client/register.cc b/modules/client/register.cc index ce7ed0d9a..41701b52c 100644 --- a/modules/client/register.cc +++ b/modules/client/register.cc @@ -89,7 +89,7 @@ try // 3.3.1 Required. The desired password for the account. const auto &password { - at<"password"_>(request) + unquote(at<"password"_>(request)) }; // 3.3.1 If true, the server binds the email used for authentication to the