spank-olm

WIP Do not look
git clone git://archive.git.mtrnord.blog/MTRNord/spank-olm.git
Log | Files | Refs | README | LICENSE

commit 7dcf03e5ddd490294396cce470c1a5315ed734e2
parent 5377cfb1bf5123f48d5e95a447092dcc1e1cfe36
Author: MTRNord <mtrnord1@gmail.com>
Date:   Wed,  7 Aug 2024 10:14:49 +0200

Fix pickling and unpickling of the account and add a test suite

Diffstat:
Minclude/account.hpp | 4++--
Minclude/list.hpp | 4++--
Mmeson.build | 3++-
Msrc/account.cpp | 115+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Atests/account_test.cpp | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 172 insertions(+), 48 deletions(-)

diff --git a/include/account.hpp b/include/account.hpp @@ -209,12 +209,12 @@ namespace spank_olm /** * \brief Lookup a one time key with the given public key */ - [[nodiscard]] std::optional<OneTimeKey const*> lookup_key(Botan::X25519_PublicKey const& key) const; + [[nodiscard]] std::optional<OneTimeKey const*> lookup_key(Botan::Public_Key const& key) const; /** * \brief Remove a one time key with the given public key */ - void remove_key(Botan::X25519_PublicKey const& key); + void remove_key(Botan::Public_Key const& key); [[nodiscard]] std::vector<uint8_t> pickle() const; diff --git a/include/list.hpp b/include/list.hpp @@ -1,7 +1,6 @@ #pragma once #include <cstddef> #include <iostream> -#include <iterator> #include <utility> #include <memory> @@ -24,7 +23,7 @@ namespace spank_olm */ FixedSizeArray() : current_size(0) { - data = std::make_unique<T*[]>(max_size); + data = std::make_unique<T*[]>(max_size + 1); } /** @@ -86,6 +85,7 @@ namespace spank_olm } data[index] = new T(value); } + return SUCCESS; } diff --git a/meson.build b/meson.build @@ -34,6 +34,7 @@ spank_olm_dep = declare_dependency( snitch_dep = dependency('snitch') -test('list_test', executable('spank-olm-test', 'tests/list_test.cpp', dependencies : [snitch_dep, spank_olm_dep], include_directories : incdir)) +test('list_test', executable('list_test', 'tests/list_test.cpp', dependencies : [snitch_dep, spank_olm_dep], include_directories : incdir)) +test('account_test', executable('account_test', 'tests/account_test.cpp', dependencies : [snitch_dep, spank_olm_dep], include_directories : incdir)) subdir('fuzz') \ No newline at end of file diff --git a/src/account.cpp b/src/account.cpp @@ -20,13 +20,15 @@ namespace spank_olm * @param value The 32-bit unsigned integer to serialize. * @return Pointer to the position in the byte array after the serialized data. */ - std::uint8_t* pickle(std::uint8_t* pos, const std::uint32_t value) + std::uint8_t* pickle(std::uint8_t* pos, std::uint32_t value) { - for (int i = 3; i >= 0; --i) + pos += 4; + for (unsigned i = 4; i--;) { - *(pos++) = (value >> (i * 8)) & 0xFF; + *(--pos) = value; + value >>= 8; } - return pos; + return pos + 4; } /** @@ -41,9 +43,10 @@ namespace spank_olm { value = 0; if (!pos || end < pos + 4) return nullptr; - for (unsigned i = 0; i < 4; ++i) + for (unsigned i = 4; i--;) { - value = (value << 8) | *(pos++); + value <<= 8; + value |= *(pos++); } return pos; } @@ -145,21 +148,13 @@ namespace spank_olm return pos + size; } - /** - * Serializes a OneTimeKey object into a byte array. - * - * @param pos Pointer to the current position in the byte array. - * @param value The OneTimeKey object to serialize. - * @return Pointer to the position in the byte array after the serialized data. - */ - static std::uint8_t* pickle( + std::uint8_t* pickle( std::uint8_t* pos, - OneTimeKey const& value - ) + const std::optional<OneTimeKey>& value) { - pos = pickle(pos, value.id); - pos = pickle(pos, value.published); - pos = pickle(pos, value.key.private_key_bits()); + pos = pickle(pos, value->id); + pos = pickle(pos, value->published); + pos = pickle(pos, value->key.raw_private_key_bits()); return pos; } @@ -168,23 +163,26 @@ namespace spank_olm * * @param pos Pointer to the current position in the byte array. * @param end Pointer to the end of the byte array. - * @param value Reference to the OneTimeKey object to store the deserialized value. - * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure. + * @return A pair containing the pointer to the position in the byte array after the deserialized data and the deserialized OneTimeKey object. */ - static std::uint8_t const* unpickle( - std::uint8_t const* pos, std::uint8_t const* end, - OneTimeKey& value + static std::pair<std::uint8_t const*, std::optional<OneTimeKey>> unpickle_otk( + std::uint8_t const* pos, std::uint8_t const* end ) { - pos = unpickle(pos, end, value.id); - UNPICKLE_OK(pos); - pos = unpickle(pos, end, value.published); - UNPICKLE_OK(pos); + std::uint32_t id; ///< The unique identifier for the one-time key. + bool published; ///< Indicates whether the key has been published. + + pos = unpickle(pos, end, id); + if (!id) + { + return {nullptr, std::nullopt}; + } + pos = unpickle(pos, end, published); Botan::secure_vector<uint8_t> key_bits; pos = unpickle(pos, end, key_bits); - UNPICKLE_OK(pos); - value.key = Botan::X25519_PrivateKey(key_bits); - return pos; + auto otk = OneTimeKey{id, published, Botan::X25519_PrivateKey(key_bits)}; + + return {pos, otk}; } /** @@ -205,7 +203,7 @@ namespace spank_olm pos = pickle(pos, static_cast<std::uint32_t>(list.size())); for (auto const& value : list) { - pos = spank_olm::pickle(pos, *value); + pos = pickle(pos, *value); } return pos; } @@ -213,17 +211,16 @@ namespace spank_olm /** * Deserializes a FixedSizeArray object from a byte array. * - * @tparam T The type of elements in the FixedSizeArray. * @tparam max_size The maximum size of the FixedSizeArray. * @param pos Pointer to the current position in the byte array. * @param end Pointer to the end of the byte array. * @param list Reference to the FixedSizeArray object to store the deserialized values. * @return Pointer to the position in the byte array after the deserialized data, or nullptr on failure. */ - template <typename T, std::size_t max_size> + template <std::size_t max_size> std::uint8_t const* unpickle( std::uint8_t const* pos, std::uint8_t const* end, - FixedSizeArray<T, max_size>& list + FixedSizeArray<OneTimeKey, max_size>& list ) { std::uint32_t size; @@ -235,13 +232,13 @@ namespace spank_olm while (size-- && pos != end) { - T value; - pos = spank_olm::unpickle(pos, end, value); + auto [temp_pos, value] = unpickle_otk(pos, end); + pos = temp_pos; if (!pos) { return nullptr; } - list.insert(value); + list.insert(value.value()); } return pos; @@ -440,7 +437,7 @@ namespace spank_olm } } - std::optional<OneTimeKey const*> Account::lookup_key(Botan::X25519_PublicKey const& key) const + std::optional<OneTimeKey const*> Account::lookup_key(Botan::Public_Key const& key) const { for (const auto& one_time_key : one_time_keys) { @@ -462,7 +459,7 @@ namespace spank_olm return std::nullopt; } - void Account::remove_key(Botan::X25519_PublicKey const& key) + void Account::remove_key(Botan::Public_Key const& key) { // Use iterator to find and remove the key. for (const auto& one_time_key : one_time_keys) @@ -510,10 +507,10 @@ namespace spank_olm if (num_fallback_keys >= 1) { - pos = spank_olm::pickle(pos, current_fallback_key->key.raw_private_key_bits()); + pos = spank_olm::pickle(pos, current_fallback_key); if (num_fallback_keys >= 2) { - pos = spank_olm::pickle(pos, prev_fallback_key->key.raw_private_key_bits()); + pos = spank_olm::pickle(pos, prev_fallback_key); } } @@ -559,7 +556,15 @@ namespace spank_olm } pos = spank_olm::unpickle(pos, end, value.identity_keys); + if (!pos) + { + throw SpankOlmErrorCorruptedAccountPickle(); + } pos = spank_olm::unpickle(pos, end, value.one_time_keys); + if (!pos) + { + throw SpankOlmErrorCorruptedAccountPickle(); + } if (pickle_version <= 2) { @@ -568,7 +573,15 @@ namespace spank_olm else if (pickle_version == 3) { pos = spank_olm::unpickle(pos, end, value.current_fallback_key); + if (!pos) + { + throw SpankOlmErrorCorruptedAccountPickle(); + } pos = spank_olm::unpickle(pos, end, value.prev_fallback_key); + if (!pos) + { + throw SpankOlmErrorCorruptedAccountPickle(); + } if (value.current_fallback_key->published) { if (value.prev_fallback_key->published) @@ -588,12 +601,24 @@ namespace spank_olm else { pos = spank_olm::unpickle(pos, end, value.num_fallback_keys); + if (!pos) + { + throw SpankOlmErrorCorruptedAccountPickle(); + } if (value.num_fallback_keys >= 1) { pos = spank_olm::unpickle(pos, end, value.current_fallback_key); + if (!pos) + { + throw SpankOlmErrorCorruptedAccountPickle(); + } if (value.num_fallback_keys >= 2) { pos = spank_olm::unpickle(pos, end, value.prev_fallback_key); + if (!pos) + { + throw SpankOlmErrorCorruptedAccountPickle(); + } if (value.num_fallback_keys >= 3) { throw SpankOlmErrorCorruptedAccountPickle(); @@ -602,7 +627,11 @@ namespace spank_olm } } - spank_olm::unpickle(pos, end, value.next_one_time_key_id); + pos = spank_olm::unpickle(pos, end, value.next_one_time_key_id); + if (!pos) + { + throw SpankOlmErrorCorruptedAccountPickle(); + } return value; } diff --git a/tests/account_test.cpp b/tests/account_test.cpp @@ -0,0 +1,94 @@ +#include <snitch/snitch.hpp> +#include "account.hpp" +#include "errors.hpp" +#include <botan/auto_rng.h> +#include <botan/pubkey.h> + +using namespace spank_olm; + +TEST_CASE("Account serialization and deserialization") +{ + Botan::AutoSeeded_RNG rng; + Account account; + account.new_account(rng); + account.generate_one_time_keys(rng, 5); + account.generate_fallback_key(rng); + + const auto serialized = account.pickle(); + const auto deserialized = Account::unpickle(serialized); + + REQUIRE( + account.identity_keys->ed25519_key.public_key()->raw_public_key_bits() == deserialized.identity_keys-> + ed25519_key.public_key()->raw_public_key_bits()); + REQUIRE( + account.identity_keys->curve25519_key.public_key()->raw_public_key_bits() == deserialized.identity_keys-> + curve25519_key.public_key()->raw_public_key_bits()); + REQUIRE(account.one_time_keys.size() == deserialized.one_time_keys.size()); + REQUIRE(account.num_fallback_keys == deserialized.num_fallback_keys); + REQUIRE(account.next_one_time_key_id == deserialized.next_one_time_key_id); +} + +TEST_CASE("Account sign and verify") +{ + Botan::AutoSeeded_RNG rng; + Account account; + account.new_account(rng); + + const std::string message = "Test message"; + auto signature = account.sign(message); + + Botan::PK_Verifier verifier(account.identity_keys->ed25519_key, "Ed25519ph"); + verifier.update(message); + REQUIRE(verifier.check_signature(signature)); +} + +TEST_CASE("Account generate and mark keys as published") +{ + Botan::AutoSeeded_RNG rng; + Account account; + account.new_account(rng); + account.generate_one_time_keys(rng, 5); + + REQUIRE(account.one_time_keys.size() == 5); + + const auto published_count = account.mark_keys_as_published(); + REQUIRE(published_count == 5); + + for (const auto& key : account.one_time_keys) + { + REQUIRE(key->published == true); + } +} + +TEST_CASE("Account generate and forget fallback key") +{ + Botan::AutoSeeded_RNG rng; + Account account; + account.new_account(rng); + account.generate_fallback_key(rng); + + REQUIRE(account.num_fallback_keys == 1); + + account.generate_fallback_key(rng); + REQUIRE(account.num_fallback_keys == 2); + + account.forget_old_fallback_key(); + REQUIRE(account.num_fallback_keys == 1); + REQUIRE(account.prev_fallback_key == std::nullopt); +} + +TEST_CASE("Account lookup and remove key") +{ + Botan::AutoSeeded_RNG rng; + Account account; + account.new_account(rng); + account.generate_one_time_keys(rng, 1); + + const auto key = account.one_time_keys[0].key.public_key(); + auto lookup_result = account.lookup_key(*key); + REQUIRE(lookup_result.has_value()); + + account.remove_key(*key); + lookup_result = account.lookup_key(*key); + REQUIRE(!lookup_result.has_value()); +}