blob: 97c53040f760da1c828c3e57107c4d6e93016874 [file] [edit]
#include <doctest/doctest.h>
#include <sframe/sframe.h>
#include <map> // for map
#include <stdexcept> // for invalid_argument
#include <string> // for basic_string, operator==
using namespace sframe;
static bytes
from_hex(const std::string& hex)
{
if (hex.length() % 2 == 1) {
throw std::invalid_argument("Odd-length hex string");
}
auto len = int(hex.length() / 2);
auto out = bytes(len);
for (int i = 0; i < len; i += 1) {
auto byte = hex.substr(2 * i, 2);
out[i] = static_cast<uint8_t>(strtol(byte.c_str(), nullptr, 16));
}
return out;
}
template<typename T>
bytes
to_bytes(const T& range)
{
return bytes(range.begin(), range.end());
}
TEST_CASE("SFrame Known-Answer")
{
struct KnownAnswerTest
{
bytes key;
bytes short_kid_ctr0;
bytes short_kid_ctr1;
bytes short_kid_ctr2;
bytes long_kid_short_ctr;
bytes long_kid_long_ctr;
};
const auto short_kid = KeyID(0x07);
const auto long_kid = KeyID(0xffff);
const auto long_ctr = KeyID(0x0100);
const auto plaintext = from_hex("00010203");
const std::map<CipherSuite, KnownAnswerTest> cases{
{ CipherSuite::AES_CM_128_HMAC_SHA256_4,
{
from_hex("101112131415161718191a1b1c1d1e1f"),
from_hex("070023b51101cc7ebc3d"),
from_hex("0701aa0743f6aa3a2b9b"),
from_hex("0702eae82433853983b7"),
from_hex("0affff0023b51101efb2441d"),
from_hex("1affff01001981bb4f7281d098"),
} },
{ CipherSuite::AES_CM_128_HMAC_SHA256_8,
{
from_hex("202122232425262728292a2b2c2d2e2f"),
from_hex("070022067e92bbacd94627c087b8"),
from_hex("0701d868b21f4ba897d19490eaa5"),
from_hex("070266de5b93b640ba637ae569dc"),
from_hex("0affff0022067e92dac1fe9af8fd6a07"),
from_hex("1affff01005ba58d136415a9799dc921f9"),
} },
{ CipherSuite::AES_GCM_128_SHA256,
{
from_hex("303132333435363738393a3b3c3d3e3f"),
from_hex("070048310f3bb26f3ee3ceed7756efe2f32078766c56"),
from_hex("070145c8c2cd60103b4a5f3477635e1b1e82f62fd280"),
from_hex("07021ea6e7b06ca32c143772066478856563dd255634"),
from_hex("0affff0048310f3b6ac967bc3e472395932eff498d3eebab"),
from_hex("1affff0100f1f838df579e32e95341dc97ae8bbd21b77c8494"),
} },
{ CipherSuite::AES_GCM_256_SHA512,
{
from_hex(
"404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"),
from_hex("0700b591faaff9b9965fcabfd0949a2bb67be4179753"),
from_hex("0701d555e6652a3f2ee36ea8c6723e57c4544025d0e7"),
from_hex("070222e5fcd46f28a2a9fa784f3b2d1aa03d481b7acc"),
from_hex("0affff00b591faafd7f6bfe6ab4dc1de52c78338395796ab"),
from_hex("1affff01007b0e9ee925743869e071d413def89374beab3bb4"),
} },
};
auto pt_out = bytes(plaintext.size());
auto ct_out = bytes(plaintext.size() + max_overhead);
for (auto& pair : cases) {
auto& suite = pair.first;
auto& tc = pair.second;
auto ctx = Context(suite);
ctx.add_key(short_kid, tc.key);
ctx.add_key(long_kid, tc.key);
// KID=0x07, CTR=0, 1, 2
auto ct0 = to_bytes(ctx.protect(short_kid, ct_out, plaintext));
auto ct1 = to_bytes(ctx.protect(short_kid, ct_out, plaintext));
auto ct2 = to_bytes(ctx.protect(short_kid, ct_out, plaintext));
CHECK(ct0 == tc.short_kid_ctr0);
CHECK(ct1 == tc.short_kid_ctr1);
CHECK(ct2 == tc.short_kid_ctr2);
CHECK(plaintext == to_bytes(ctx.unprotect(pt_out, ct0)));
CHECK(plaintext == to_bytes(ctx.unprotect(pt_out, ct1)));
CHECK(plaintext == to_bytes(ctx.unprotect(pt_out, ct2)));
// KID=0xffff, CTR=0
auto ctLS = to_bytes(ctx.protect(long_kid, ct_out, plaintext));
for (Counter ctr = 1; ctr < long_ctr; ctr++) {
ctx.protect(long_kid, ct_out, plaintext);
}
auto ctLL = to_bytes(ctx.protect(long_kid, ct_out, plaintext));
CHECK(to_bytes(ctLS) == tc.long_kid_short_ctr);
CHECK(to_bytes(ctLL) == tc.long_kid_long_ctr);
CHECK(plaintext == to_bytes(ctx.unprotect(pt_out, ct0)));
CHECK(plaintext == to_bytes(ctx.unprotect(pt_out, ct1)));
CHECK(plaintext == to_bytes(ctx.unprotect(pt_out, ct2)));
}
}
TEST_CASE("SFrame Round-Trip")
{
const auto rounds = 1 << 9;
const auto kid = KeyID(0x42);
const auto plaintext = from_hex("00010203");
const std::map<CipherSuite, bytes> keys{
{ CipherSuite::AES_CM_128_HMAC_SHA256_4,
from_hex("101112131415161718191a1b1c1d1e1f") },
{ CipherSuite::AES_CM_128_HMAC_SHA256_8,
from_hex("202122232425262728292a2b2c2d2e2f") },
{ CipherSuite::AES_GCM_128_SHA256,
from_hex("303132333435363738393a3b3c3d3e3f") },
{ CipherSuite::AES_GCM_256_SHA512,
from_hex("404142434445464748494a4b4c4d4e4f"
"505152535455565758595a5b5c5d5e5f") },
};
auto pt_out = bytes(plaintext.size());
auto ct_out = bytes(plaintext.size() + max_overhead);
for (auto& pair : keys) {
auto& suite = pair.first;
auto& key = pair.second;
auto send = Context(suite);
send.add_key(kid, key);
auto recv = Context(suite);
recv.add_key(kid, key);
for (int i = 0; i < rounds; i++) {
auto encrypted = to_bytes(send.protect(kid, ct_out, plaintext));
auto decrypted = to_bytes(recv.unprotect(pt_out, encrypted));
CHECK(decrypted == plaintext);
}
}
}
TEST_CASE("MLS Known-Answer")
{
struct KnownAnswerTest
{
using Epoch = std::vector<bytes>;
std::vector<Epoch> epochs;
};
const auto plaintext = from_hex("00010203");
const auto epoch_bits = 4;
const auto epoch_ids = std::vector<MLSContext::EpochID>{
0x00,
0x0f,
0x10,
};
const auto epoch_secrets = std::vector<bytes>{
from_hex("00000000000000000000000000000000"),
from_hex("0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f"),
from_hex("10101010101010101010101010101010"),
};
const auto sender_ids = std::vector<MLSContext::SenderID>{
0x0a,
0xaa,
0xaaa,
};
const std::map<CipherSuite, KnownAnswerTest> cases{
{ CipherSuite::AES_CM_128_HMAC_SHA256_4,
{ {
{
from_hex("09a000c92bf2b7154e0356"),
from_hex("0a0aa000c84890cf4a814ce0"),
from_hex("0aaaa0004361be8c2c549ae5"),
},
{
from_hex("09af0086adc8a87988307c"),
from_hex("0a0aaf006870557dd62e9409"),
from_hex("0aaaaf00a0e68b60eab24b27"),
},
{
from_hex("09a0001ad5829b23a11c33"),
from_hex("0a0aa0004769a13c95568e30"),
from_hex("0aaaa000586b97fa7f7fe096"),
},
} } },
{ CipherSuite::AES_CM_128_HMAC_SHA256_8,
{ {
{
from_hex("09a000c92bf2b7154e0356b4be009e"),
from_hex("0a0aa000c84890cf4a814ce031fae9f7"),
from_hex("0aaaa0004361be8c2c549ae53384b5f1"),
},
{
from_hex("09af0086adc8a87988307c147e8138"),
from_hex("0a0aaf006870557dd62e94097fdfaae8"),
from_hex("0aaaaf00a0e68b60eab24b275b63f964"),
},
{
from_hex("09a0001ad5829b23a11c33a5ac2f11"),
from_hex("0a0aa0004769a13c95568e30f9bafee2"),
from_hex("0aaaa000586b97fa7f7fe0965aee99da"),
},
} } },
{ CipherSuite::AES_GCM_128_SHA256,
{ {
{
from_hex("09a000bb7d6b3b4f32219f00516841cca349101f942ba1"),
from_hex("0a0aa000382032d088cb627c73b2c55968092d7039538f02"),
from_hex("0aaaa0006aa1aa44bbd1911b07ad876350c29790dad1fa04"),
},
{
from_hex("09af0077d0682046fdb1e08315c63b6c5f9f205a9e76c4"),
from_hex("0a0aaf00a99857f0a8aa0b2d0b5825ea2c0d71621f2bb7aa"),
from_hex("0aaaaf00662bf029595d34ea58a68edc7390c78e0fcc6de4"),
},
{
from_hex("09a0000661fb1f7b2b2dd820b225f5239cafc817da7821"),
from_hex("0a0aa0008140a14bb80a6d5793637d820de7c01df4f7f676"),
from_hex("0aaaa00084da92dbd31c174d3d7423c9470a48849c5c83b3"),
},
} } },
{ CipherSuite::AES_GCM_256_SHA512,
{ {
{
from_hex("09a000414462ccdfcb5473b8e0e4f686362e35a2985182"),
from_hex("0a0aa000c013c6d92f0683dbf87ccca3c2c11d3eca3c382f"),
from_hex("0aaaa0009a2a9ab03c3eca040928d8ef17ea531696b8163a"),
},
{
from_hex("09af00466bc33b10ec4405e4ff7241d11c63b21192b535"),
from_hex("0a0aaf00f7219487f1bf6ccec5c40888bcd79bb135900e02"),
from_hex("0aaaaf0043a23ff53ae88f41084ad503f1ec82613983b00b"),
},
{
from_hex("09a0004f4d239d260255899228d119099fa23f09d8b880"),
from_hex("0a0aa000b592b5e3bc31c7ac13eea1e69e83826233f90a4d"),
from_hex("0aaaa000fd95ba9acefc244e4652db355d4ce0a5c5137492"),
},
} } },
};
auto pt_out = bytes(plaintext.size());
auto ct_out = bytes(plaintext.size() + max_overhead);
for (const auto& pair : cases) {
auto& suite = pair.first;
auto& tc = pair.second;
auto ctx = MLSContext(suite, epoch_bits);
CHECK(tc.epochs.size() == epoch_ids.size());
for (size_t i = 0; i < tc.epochs.size(); i++) {
ctx.add_epoch(epoch_ids[i], epoch_secrets[i]);
CHECK(tc.epochs[i].size() == sender_ids.size());
for (size_t j = 0; j < tc.epochs[i].size(); j++) {
auto encrypted =
ctx.protect(epoch_ids[i], sender_ids[j], ct_out, plaintext);
CHECK(tc.epochs[i][j] == to_bytes(encrypted));
auto decrypted = ctx.unprotect(pt_out, tc.epochs[i][j]);
CHECK(plaintext == to_bytes(decrypted));
}
}
}
}
TEST_CASE("MLS Round-Trip")
{
const auto epoch_bits = 2;
const auto test_epochs = 1 << (epoch_bits + 1);
const auto epoch_rounds = 10;
const auto plaintext = from_hex("00010203");
const auto sender_id_a = MLSContext::SenderID(0xA0A0A0A0);
const auto sender_id_b = MLSContext::SenderID(0xA1A1A1A1);
const std::vector<CipherSuite> suites{
CipherSuite::AES_CM_128_HMAC_SHA256_4,
CipherSuite::AES_CM_128_HMAC_SHA256_8,
CipherSuite::AES_GCM_128_SHA256,
CipherSuite::AES_GCM_256_SHA512,
};
auto pt_out = bytes(plaintext.size());
auto ct_out = bytes(plaintext.size() + max_overhead);
for (auto& suite : suites) {
auto member_a = MLSContext(suite, epoch_bits);
auto member_b = MLSContext(suite, epoch_bits);
for (MLSContext::EpochID epoch_id = 0; epoch_id < test_epochs; epoch_id++) {
const auto sframe_epoch_secret = bytes(8, uint8_t(epoch_id));
member_a.add_epoch(epoch_id, sframe_epoch_secret);
member_b.add_epoch(epoch_id, sframe_epoch_secret);
for (int i = 0; i < epoch_rounds; i++) {
auto encrypted_ab =
member_a.protect(epoch_id, sender_id_a, ct_out, plaintext);
auto decrypted_ab = member_b.unprotect(pt_out, encrypted_ab);
CHECK(plaintext == to_bytes(decrypted_ab));
auto encrypted_ba =
member_b.protect(epoch_id, sender_id_b, ct_out, plaintext);
auto decrypted_ba = member_a.unprotect(pt_out, encrypted_ba);
CHECK(plaintext == to_bytes(decrypted_ba));
}
}
}
}
TEST_CASE("MLS Failure after Purge")
{
const auto suite = CipherSuite::AES_GCM_128_SHA256;
const auto epoch_bits = 2;
const auto plaintext = from_hex("00010203");
const auto sender_id_a = MLSContext::SenderID(0xA0A0A0A0);
const auto sframe_epoch_secret_1 = bytes(32, 1);
const auto sframe_epoch_secret_2 = bytes(32, 2);
auto pt_out = bytes(plaintext.size());
auto ct_out = bytes(plaintext.size() + max_overhead);
auto member_a = MLSContext(suite, epoch_bits);
auto member_b = MLSContext(suite, epoch_bits);
// Install epoch 1 and create a cipihertext
const auto epoch_id_1 = MLSContext::EpochID(1);
member_a.add_epoch(epoch_id_1, sframe_epoch_secret_1);
member_b.add_epoch(epoch_id_1, sframe_epoch_secret_1);
const auto enc_ab_1 =
member_a.protect(epoch_id_1, sender_id_a, ct_out, plaintext);
const auto enc_ab_1_data = to_bytes(enc_ab_1);
// Install epoch 2
const auto epoch_id_2 = MLSContext::EpochID(2);
member_a.add_epoch(epoch_id_2, sframe_epoch_secret_2);
member_b.add_epoch(epoch_id_2, sframe_epoch_secret_2);
// Purge epoch 1 and verify failure
member_a.purge_before(epoch_id_2);
member_b.purge_before(epoch_id_2);
CHECK_THROWS_AS(member_a.protect(epoch_id_1, sender_id_a, ct_out, plaintext),
invalid_parameter_error);
CHECK_THROWS_AS(member_b.unprotect(pt_out, enc_ab_1_data),
invalid_parameter_error);
const auto enc_ab_2 =
member_a.protect(epoch_id_2, sender_id_a, ct_out, plaintext);
const auto dec_ab_2 = member_b.unprotect(pt_out, enc_ab_2);
CHECK(plaintext == to_bytes(dec_ab_2));
}