0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-11-26 08:42:34 +01:00

ircd:Ⓜ️ Improve server keys related functions.

This commit is contained in:
Jason Volk 2017-11-25 15:30:03 -07:00
parent 8869b1577c
commit 31b778ee0b
4 changed files with 505 additions and 399 deletions

View file

@ -29,7 +29,7 @@
namespace ircd::m namespace ircd::m
{ {
struct key; struct keys;
} }
namespace ircd::m::self namespace ircd::m::self
@ -53,6 +53,16 @@ namespace ircd::m::name
constexpr const char *const valid_until_ts {"valid_until_ts"}; 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 /// 2.2.1.1 Publishing Keys
/// ///
/// Key Type, Description /// 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. /// 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. /// 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::tuple
< <
json::property<name::old_verify_keys, json::object>, json::property<name::old_verify_keys, json::object>,
@ -74,21 +84,22 @@ struct ircd::m::key
json::property<name::verify_keys, json::object> json::property<name::verify_keys, json::object>
> >
{ {
static room keys; using key_closure = std::function<void (const string_view &)>; // remember to unquote()!!!
using keys_closure = std::function<void (const keys &)>;
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::tuple;
using super_type::operator=; using super_type::operator=;
}; };
namespace ircd::m::keys
{
using closure = std::function<void (const key &)>;
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 #pragma GCC diagnostic pop

597
ircd/m.cc
View file

@ -251,276 +251,6 @@ ircd::m::dbs::init_modules()
modules.emplace(name, name); 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<json::object &>(*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<name::content, string_view>,
json::property<name::destination, string_view>,
json::property<name::method, string_view>,
json::property<name::origin, string_view>,
json::property<name::uri, string_view>
>
{
string_view generate(const mutable_buffer &out);
using super_type::tuple;
};
void
ircd::m::io::request::operator()(const vector_view<const http::header> &addl_headers)
const
{
}
void
ircd::m::io::request::operator()(server &server,
const vector_view<const http::header> &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<mutable_buffer, 131072> 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<mutable_buffer, 128> 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 // m/keys.h
@ -532,8 +262,10 @@ keys_room_id
"keys", ircd::my_host() "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::room
ircd::m::key::keys ircd::m::keys::room
{ {
keys_room_id keys_room_id
}; };
@ -636,7 +368,7 @@ ircd::m::init_keys(const json::object &options)
static void static void
ircd::m::bootstrap_keys() ircd::m::bootstrap_keys()
{ {
create(key::keys, me.user_id); create(keys::room, me.user_id);
const json::strung verify_keys 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<"verify_keys"_>(my_key) = verify_keys;
json::get<"server_name"_>(my_key) = my_host(); json::get<"server_name"_>(my_key) = my_host();
json::get<"old_verify_keys"_>(my_key) = "{}"; json::get<"old_verify_keys"_>(my_key) = "{}";
@ -695,23 +427,207 @@ ircd::m::bootstrap_keys()
keys::set(my_key); keys::set(my_key);
} }
bool void
ircd::m::keys::get(const string_view &server_name,
const closure &closure)
{
return get(server_name, string_view{}, closure);
}
bool
ircd::m::keys::get(const string_view &server_name, ircd::m::keys::get(const string_view &server_name,
const string_view &key_id, 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()); 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<mutable_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<mutable_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<m::vm::where::equal> query const m::vm::query<m::vm::where::equal> query
{ {
{ "room_id", key::keys.room_id }, { "room_id", keys::room.room_id },
{ "type", "ircd.key" }, { "type", "ircd.key" },
{ "state_key", server_name }, { "state_key", server_name },
}; };
@ -725,110 +641,25 @@ ircd::m::keys::get(const string_view &server_name,
} }
}; };
if(m::vm::test(query, have)) return m::vm::test(query, have);
return true;
if(server_name == my_host())
throw m::NOT_FOUND
{
"key '%s' for '%s' not found", key_id?: "<unspecified>", server_name
};
log.debug("Key %s for %s not cached; querying network...",
key_id?: "<unspecified>",
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<mutable_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;
} }
void void
ircd::m::keys::set(const key &key) ircd::m::keys::set(const keys &keys)
{ {
const auto &state_key const auto &state_key
{ {
unquote(at<"server_name"_>(key)) unquote(at<"server_name"_>(keys))
}; };
const m::user::id::buf sender const m::user::id::buf sender
{ {
"ircd", unquote(at<"server_name"_>(key)) "ircd", unquote(at<"server_name"_>(keys))
}; };
const json::strung content const json::strung content
{ {
key keys
}; };
json::iov event; json::iov event;
@ -840,17 +671,17 @@ ircd::m::keys::set(const key &key)
{ event, json::member { "content", content }} { event, json::member { "content", content }}
}; };
key::keys.send(event); keys::room.send(event);
} }
/// Verify this key data (with itself). /// Verify this key data (with itself).
bool bool
ircd::m::key::verify() ircd::m::keys::verify(const keys &keys)
const noexcept try noexcept try
{ {
const auto &valid_until_ts const auto &valid_until_ts
{ {
at<"valid_until_ts"_>(*this) at<"valid_until_ts"_>(keys)
}; };
if(valid_until_ts < ircd::time<milliseconds>()) if(valid_until_ts < ircd::time<milliseconds>())
@ -858,7 +689,7 @@ const noexcept try
const json::object &verify_keys const json::object &verify_keys
{ {
at<"verify_keys"_>(*this) at<"verify_keys"_>(keys)
}; };
const string_view &key_id const string_view &key_id
@ -881,12 +712,12 @@ const noexcept try
const json::object &signatures const json::object &signatures
{ {
at<"signatures"_>(*this) at<"signatures"_>(keys)
}; };
const string_view &server_name const string_view &server_name
{ {
unquote(at<"server_name"_>(*this)) unquote(at<"server_name"_>(keys))
}; };
const json::object &server_signatures const json::object &server_signatures
@ -900,15 +731,15 @@ const noexcept try
}}; }};
///TODO: XXX ///TODO: XXX
m::key copy{*this}; m::keys copy{keys};
at<"signatures"_>(copy) = string_view{}; 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); return pk.verify(const_raw_buffer{preimage}, sig);
} }
catch(const std::exception &e) catch(const std::exception &e)
{ {
log.error("key verification for '%s' failed: %s", log.error("key verification for '%s' failed: %s",
json::get<"server_name"_>(*this, "<no server name>"_sv), json::get<"server_name"_>(keys, "<no server name>"_sv),
e.what()); e.what());
return false; return false;

View file

@ -517,9 +517,273 @@ try
}}; }};
return m::vm::test(query, test); return m::vm::test(query, test);
*/
return false;
} }
catch(const std::exception &e) catch(const std::exception &e)
{ {
tab.error = std::make_exception_ptr(e); tab.error = std::make_exception_ptr(e);
return false; 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<json::object &>(*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<name::content, string_view>,
json::property<name::destination, string_view>,
json::property<name::method, string_view>,
json::property<name::origin, string_view>,
json::property<name::uri, string_view>
>
{
string_view generate(const mutable_buffer &out);
using super_type::tuple;
};
void
ircd::m::io::request::operator()(const vector_view<const http::header> &addl_headers)
const
{
}
void
ircd::m::io::request::operator()(server &server,
const vector_view<const http::header> &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<mutable_buffer, 131072> 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<mutable_buffer, 128> 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);
}

View file

@ -89,7 +89,7 @@ try
// 3.3.1 Required. The desired password for the account. // 3.3.1 Required. The desired password for the account.
const auto &password 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 // 3.3.1 If true, the server binds the email used for authentication to the