Skip to content

Commit

Permalink
Merge pull request #73 from jagerman/key-promotion
Browse files Browse the repository at this point in the history
Add group keys function for key promotion
  • Loading branch information
jagerman committed Nov 17, 2023
2 parents b4c8e53 + f3af485 commit c52f689
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 0 deletions.
23 changes: 23 additions & 0 deletions include/session/config/groups/keys.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,29 @@ LIBSESSION_EXPORT const unsigned char* groups_keys_get_key(const config_group_ke
/// - `true` if we have admin keys, `false` otherwise.
LIBSESSION_EXPORT bool groups_keys_is_admin(const config_group_keys* conf);

/// API: groups/groups_keys_load_admin_key
///
/// Loads the admin keys, effectively upgrading this keys object from a member to an admin.
///
/// This does nothing if the keys object already has admin keys.
///
/// Inputs:
/// - `conf` -- the groups keys config object
/// - `secret` -- pointer to the 32-byte group seed. (This a 64-byte libsodium "secret key" begins
/// with the seed, this can also be a given a pointer to such a value).
/// - `group_info_conf` -- the group info config instance (the key will be added)
/// - `group_members_conf` -- the group members config instance (the key will be added)
///
/// Outputs:
/// - `true` if the object has been upgraded to admin status, or was already admin status; `false`
/// if the given seed value does not match the group's public key. If this returns `true` then
/// after the call a call to `groups_keys_is_admin` would also return `true`.
LIBSESSION_EXPORT bool groups_keys_load_admin_key(
config_group_keys* conf,
const unsigned char* secret,
config_object* group_info_conf,
config_object* group_members_conf);

/// API: groups/groups_keys_rekey
///
/// Generates a new encryption key for the group and returns an encrypted key message to be pushed
Expand Down
23 changes: 23 additions & 0 deletions include/session/config/groups/keys.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,29 @@ class Keys final : public ConfigSig {
/// - `true` if this object knows the group's master key
bool admin() const { return _sign_sk && _sign_pk; }

/// API: groups/Keys::load_admin_key
///
/// Loads the group secret key into the Keys object (as well as passing it along to the Info and
/// Members objects).
///
/// The primary use of this is when accepting a promotion-to-admin: the Keys object would be
/// constructed as a regular member (without the admin key) then this method "upgrades" the
/// object with the group signing key.
///
/// This will do nothing if the secret key is already known; it will throw if
/// the given secret key does not yield the group's public key. The given key can be either the
/// 32 byte seed, or the libsodium 64 byte "secret key" (which is just the seed and cached
/// public key stuck together).
///
/// Inputs:
/// - `secret` -- the group's 64-byte secret key or 32-byte seed
/// - `info` and `members` -- will be loaded with the group keys if the key is loaded
/// successfully.
///
/// Outputs: nothing. After a successful call, `admin()` will return true. Throws if the given
/// secret key does not match the group's pubkey.
void load_admin_key(ustring_view secret, Info& info, Members& members);

/// API: groups/Keys::rekey
///
/// Generate a new encryption key for the group and returns an encrypted key message to be
Expand Down
41 changes: 41 additions & 0 deletions src/config/groups/keys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,30 @@ ustring_view Keys::group_enc_key() const {
return {key.data(), key.size()};
}

void Keys::load_admin_key(ustring_view seed, Info& info, Members& members) {
if (admin())
return;

if (seed.size() == 64)
seed.remove_suffix(32);
else if (seed.size() != 32)
throw std::invalid_argument{
"Failed to load admin key: invalid secret key (expected 32 or 64 bytes)"};

std::array<unsigned char, 32> pk;
sodium_cleared<std::array<unsigned char, 64>> sk;
crypto_sign_ed25519_seed_keypair(pk.data(), sk.data(), seed.data());

if (_sign_pk.has_value() && *_sign_pk != pk)
throw std::runtime_error{
"Failed to load admin key: given secret key does not match group pubkey"};

auto seckey = to_sv(sk);
set_sig_keys(seckey);
info.set_sig_keys(seckey);
members.set_sig_keys(seckey);
}

static std::array<unsigned char, 32> compute_xpk(const unsigned char* ed25519_pk) {
std::array<unsigned char, 32> xpk;
if (0 != crypto_sign_ed25519_pk_to_curve25519(xpk.data(), ed25519_pk))
Expand Down Expand Up @@ -1424,6 +1448,23 @@ LIBSESSION_C_API bool groups_keys_is_admin(const config_group_keys* conf) {
return unbox(conf).admin();
}

LIBSESSION_C_API bool groups_keys_load_admin_key(
config_group_keys* conf,
const unsigned char* secret,
config_object* info,
config_object* members) {
try {
unbox(conf).load_admin_key(
ustring_view{secret, 32},
*unbox<groups::Info>(info),
*unbox<groups::Members>(members));
} catch (const std::exception& e) {
set_error(conf, e.what());
return false;
}
return true;
}

LIBSESSION_C_API bool groups_keys_rekey(
config_group_keys* conf,
config_object* info,
Expand Down
90 changes: 90 additions & 0 deletions tests/test_group_keys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -887,3 +887,93 @@ TEST_CASE("Group Keys - swarm authentication", "[config][groups][keys][swarm]")
}
}
}

TEST_CASE("Group Keys promotion", "[config][groups][keys][promotion]") {

const ustring group_seed =
"0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210"_hexbytes;
const ustring admin1_seed =
"0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"_hexbytes;
const ustring member1_seed =
"000111222333444555666777888999aaabbbcccdddeeefff0123456789abcdef"_hexbytes;

std::array<unsigned char, 32> group_pk;
std::array<unsigned char, 64> group_sk;

crypto_sign_ed25519_seed_keypair(group_pk.data(), group_sk.data(), group_seed.data());
REQUIRE(oxenc::to_hex(group_seed.begin(), group_seed.end()) ==
oxenc::to_hex(group_sk.begin(), group_sk.begin() + 32));

pseudo_client admin{admin1_seed, true, group_pk.data(), group_sk.data()};
pseudo_client member{member1_seed, false, group_pk.data(), std::nullopt};

std::vector<std::pair<std::string, ustring_view>> configs;
{
auto m = admin.members.get_or_construct(admin.session_id);
m.admin = true;
m.name = "Lrrr";
admin.members.set(m);
}
{
auto m = admin.members.get_or_construct(member.session_id);
m.admin = false;
m.name = "Nibbler";
admin.members.set(m);
}
admin.info.set_name("Omicron Persei 8");
auto [mseq, mdata, mobs] = admin.members.push();
admin.members.confirm_pushed(mseq, "mpush1");
auto [iseq, idata, iobs] = admin.info.push();
admin.info.confirm_pushed(mseq, "ipush1");

REQUIRE(admin.keys.pending_config());
member.keys.load_key_message(
"keyhash1",
*admin.keys.pending_config(),
get_timestamp_ms(),
member.info,
member.members);
admin.keys.load_key_message(
"keyhash1",
*admin.keys.pending_config(),
get_timestamp_ms(),
member.info,
member.members);

member.keys.load_key_message(
"keyhash2",
admin.keys.key_supplement(member.session_id),
get_timestamp_ms(),
member.info,
member.members);

configs.emplace_back("mpush1", mdata);
CHECK(member.members.merge(configs) == std::vector{{"mpush1"s}});

configs.clear();
configs.emplace_back("ipush1", idata);
CHECK(member.info.merge(configs) == std::vector{{"ipush1"s}});

REQUIRE(admin.keys.admin());
REQUIRE_FALSE(member.keys.admin());
REQUIRE(member.info.is_readonly());
REQUIRE(member.members.is_readonly());

member.keys.load_admin_key(to_usv(group_sk), member.info, member.members);

CHECK(member.keys.admin());
CHECK_FALSE(member.members.is_readonly());
CHECK_FALSE(member.info.is_readonly());

member.info.set_name("new name"s);

CHECK(member.info.needs_push());
auto [iseq2, idata2, iobs2] = member.info.push();

configs.clear();
configs.emplace_back("ihash2", idata2);

CHECK(admin.info.merge(configs) == std::vector{{"ihash2"s}});

CHECK(admin.info.get_name() == "new name");
}

0 comments on commit c52f689

Please sign in to comment.