#include #include #include #include #include #include using mesh::ReplayResetKind; using mesh::ReplayResetNonce; using mesh::ReplayResetRequest; using Result = mesh::ReplayResetNonce::IssueResult; static void check(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } static ReplayResetKind parse(const std::string& command) { ReplayResetRequest request; const auto result = mesh::parseReplayResetCommand(command.c_str(), request); check(result == request.kind, "parser result disagrees with request"); return result; } static void parserTests() { const std::string key = "0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789ABCDEF"; const std::string token = "0123456789abcdefFEDCBA9876543210"; const std::string base = "replay reset " + key; check(parse(base) == ReplayResetKind::ExactKey, "exact key"); check(parse(base + " " + token) == ReplayResetKind::ExactKeyConfirm, "exact confirmation"); check(parse(" \tQ7| RePlAy\tRESET\t" + key + "\r\n") == ReplayResetKind::ExactKey, "normalization and companion prefix"); check(parse("ab|replay reset " + key + " " + token + " \t\r\n") == ReplayResetKind::ExactKeyConfirm, "prefixed confirmation"); check(parse("replay reset all CONFIRM") == ReplayResetKind::AllConfirm, "all confirmation"); check(parse(" REPLAY reset ALL confirm\r\n") == ReplayResetKind::AllConfirm, "all case fold"); ReplayResetRequest request; memset(&request, 0xff, sizeof(request)); check(mesh::parseReplayResetCommand(nullptr, request) == ReplayResetKind::NotReplay, "null command"); for (size_t i = 0; i < sizeof(request.key); ++i) check(request.key[i] == 0, "request key initialized"); for (size_t i = 0; i < sizeof(request.token); ++i) check(request.token[i] == 0, "request token initialized"); mesh::parseReplayResetCommand((base + " " + token).c_str(), request); check(request.key[0] == 0x01 && request.key[7] == 0xef && request.key[31] == 0xef, "decoded exact full key"); check(request.token[0] == 0x01 && request.token[8] == 0xfe && request.token[15] == 0x10, "decoded full nonce"); const std::string invalid[] = { "replay", "replay ", "replay reset", "replay reset all", "replay reset all 123", "replay reset all CONFIRM extra", "replay RESET " + key.substr(2), "replay reset " + key + "00", "replay reset g" + key.substr(1), "replay reset " + key.substr(0, 20) + " " + key.substr(20), base + " " + token.substr(2), base + " " + token + "00", base + " " + token + " extra", base + " g" + token.substr(1), base + " CONFIRM", "replay reset0 " + key, "replay bogus", "replay.reset " + key, " ab|replay reset all", "replay reset all\nCONFIRM extra", }; for (const auto& command : invalid) { check(parse(command) == ReplayResetKind::Invalid, command.c_str()); } const std::string unrelated[] = {"", " ", "a", "ab", "ab|", "replayx", "replay-reset", "get replay", "reboot"}; for (const auto& command : unrelated) { check(parse(command) == ReplayResetKind::NotReplay, command.c_str()); } } struct Identities { uint8_t issuer[32] = {1}; uint8_t target[32] = {2}; uint8_t other[32] = {3}; uint8_t random[16] = {4}; uint8_t later_random[16] = {5}; uint8_t zero[16] = {}; }; static void lifecycleTests() { Identities ids; ReplayResetNonce nonce; check(!nonce.matches(ids.issuer, ids.target, ids.random, 100, 2000000000), "unissued nonce"); check(nonce.issue(ids.issuer, ids.target, ids.random, 100, 2000000000) == Result::Issued, "issue nonce"); check(memcmp(nonce.token(), ids.random, 16) == 0, "exposes original token"); check(nonce.matches(ids.issuer, ids.target, ids.random, 1100, 2000000001), "matches issuer and target"); check(!nonce.matches(ids.other, ids.target, ids.random, 1100, 2000000001), "different issuer"); check(!nonce.matches(ids.issuer, ids.other, ids.random, 1100, 2000000001), "different target"); check(!nonce.matches(ids.issuer, ids.target, ids.later_random, 1100, 2000000001), "wrong token"); check(nonce.issue(ids.issuer, ids.target, ids.later_random, 20100, 2000000020) == Result::Reused, "prepare retry reuses existing token"); check(memcmp(nonce.token(), ids.random, 16) == 0, "retry cannot replace token"); check(nonce.issue(ids.other, ids.target, ids.later_random, 20100, 2000000020) == Result::Busy, "other issuer cannot replace challenge"); check(nonce.issue(ids.issuer, ids.other, ids.later_random, 20100, 2000000020) == Result::Busy, "other target cannot replace challenge"); check(!nonce.consume(ids.issuer, ids.target, ids.later_random, 20100, 2000000020), "bad token does not consume challenge"); check(nonce.consume(ids.issuer, ids.target, ids.random, 20100, 2000000020), "valid token consumed"); check(!nonce.consume(ids.issuer, ids.target, ids.random, 20100, 2000000020), "cannot consume twice"); check(!nonce.matches(ids.issuer, ids.target, ids.random, 21100, 2000000021), "captured confirmation rejected"); for (size_t i = 0; i < 16; ++i) check(nonce.token()[i] == 0, "consumption clears token"); check(nonce.issue(ids.issuer, ids.target, ids.later_random, 21100, 2000000021) == Result::Issued, "reissue after consumption"); check(!nonce.matches(ids.issuer, ids.target, ids.random, 21100, 2000000021), "old token rejected after reissue"); nonce.clear(); check(!nonce.matches(ids.issuer, ids.target, ids.later_random, 21100, 2000000021), "USB invalidation"); ReplayResetNonce rebooted; check(!rebooted.matches(ids.issuer, ids.target, ids.random, 100, 2000000000), "reboot invalidates captured token"); check(rebooted.issue(ids.issuer, ids.issuer, ids.random, 100, 2000000000) == Result::Issued, "self reset allowed by nonce layer"); check(rebooted.consume(ids.issuer, ids.issuer, ids.random, 100, 2000000000), "self reset consumed once"); } static void timeTests() { Identities ids; ReplayResetNonce nonce; check(ReplayResetNonce::RESEND_WINDOW_MILLIS == 120000, "resend window is exactly 120 seconds"); check(ReplayResetNonce::LIFETIME_MILLIS == 300000, "confirmation expires at exactly 300 seconds"); check(nonce.issue(ids.issuer, ids.target, ids.random, 100, 2000000000) == Result::Issued, "time fixture"); check(nonce.remainingSeconds(100, 2000000000) == 300, "initial TTL is 300 seconds"); check(nonce.issue(ids.issuer, ids.target, ids.later_random, 120099, 2000000119) == Result::Reused, "119999ms remains in the resend window"); check(nonce.matches(ids.issuer, ids.target, ids.random, 120099, 2000000119), "119999ms original token valid"); check(nonce.remainingSeconds(120099, 2000000119) == 180, "last resend advertises original 180-second TTL"); check(nonce.issue(ids.issuer, ids.target, ids.later_random, 120100, 2000000120) == Result::AwaitingConfirmation, "120000ms starts confirmation-only window"); check(nonce.matches(ids.issuer, ids.target, ids.random, 120100, 2000000120), "120000ms token still valid"); check(nonce.remainingSeconds(120100, 2000000120) == 180, "confirmation-only boundary retains original TTL"); check(nonce.issue(ids.issuer, ids.target, ids.later_random, 300099, 2000000299) == Result::AwaitingConfirmation, "299999ms cannot reissue or resend original token"); check(nonce.matches(ids.issuer, ids.target, ids.random, 300099, 2000000299), "299999ms original token valid"); check(nonce.remainingSeconds(300099, 2000000299) == 0, "TTL rounds down during valid final subsecond"); check(memcmp(nonce.token(), ids.random, 16) == 0, "confirmation-only retries cannot replace token"); check(!nonce.matches(ids.issuer, ids.target, ids.random, 300100, 2000000300), "300000ms expiration boundary"); check(nonce.remainingSeconds(300100, 2000000300) == 0, "expired TTL is zero"); check(nonce.issue(ids.issuer, ids.target, ids.later_random, 300100, 2000000300) == Result::Issued, "expired challenge replaceable"); check(memcmp(nonce.token(), ids.later_random, 16) == 0, "new lifetime uses fresh token"); check(nonce.matches(ids.issuer, ids.target, ids.later_random, 301100, 2000000306), "positive clock tolerance"); check(nonce.matches(ids.issuer, ids.target, ids.later_random, 301100, 2000000296), "negative clock tolerance"); check(!nonce.matches(ids.issuer, ids.target, ids.later_random, 301100, 2000000307), "forward clock jump"); check(nonce.remainingSeconds(301100, 2000000307) == 0, "clock-invalid TTL is zero"); check(!nonce.matches(ids.issuer, ids.target, ids.later_random, 301100, 2000000295), "backward clock jump"); check(!nonce.matches(ids.issuer, ids.target, ids.later_random, 301100, 0), "zero clock invalid"); check(!nonce.matches(ids.issuer, ids.target, ids.later_random, 300099, 2000000300), "monotonic backwards fails closed"); nonce.clear(); nonce.issue(ids.issuer, ids.target, ids.random, 0, 2000000000); for (uint32_t age : {119999U, 120000U, 299999U}) { check(nonce.issue(ids.other, ids.target, ids.later_random, age, 2000000000 + age / 1000) == Result::Busy, "another issuer stays blocked through confirmation-only window"); check(nonce.issue(ids.issuer, ids.other, ids.later_random, age, 2000000000 + age / 1000) == Result::Busy, "another target stays blocked through confirmation-only window"); } check(nonce.issue(ids.other, ids.target, ids.later_random, 300000, 2000000300) == Result::Issued, "another identity can start only after 300000ms"); nonce.clear(); const uint32_t started = UINT32_MAX - 500; check(nonce.issue(ids.issuer, ids.target, ids.random, started, 2000000000) == Result::Issued, "rollover issue"); check(nonce.matches(ids.issuer, ids.target, ids.random, 499, 2000000001), "millis rollover valid"); check(nonce.issue(ids.issuer, ids.target, ids.later_random, started + 119999U, 2000000119) == Result::Reused, "rollover 119999ms resend"); check(nonce.issue(ids.issuer, ids.target, ids.later_random, started + 120000U, 2000000120) == Result::AwaitingConfirmation, "rollover 120000ms confirmation-only"); check(nonce.matches(ids.issuer, ids.target, ids.random, started + 299999U, 2000000299), "rollover 299999ms original confirmation valid"); check(!nonce.matches(ids.issuer, ids.target, ids.random, started + 300000U, 2000000300), "rollover 300000ms expiration"); nonce.clear(); check(nonce.issue(ids.issuer, ids.target, ids.random, 0, UINT32_MAX) == Result::Issued, "epoch boundary issue"); check(!nonce.matches(ids.issuer, ids.target, ids.random, 1000, UINT32_MAX), "epoch overflow rejected"); check(!nonce.matches(ids.issuer, ids.target, ids.random, 1000, 1), "epoch wrap rejected"); nonce.clear(); nonce.issue(ids.issuer, ids.target, ids.random, 0, 2000000000); check(nonce.issue(ids.issuer, ids.target, ids.later_random, 119999, 2000000119) == Result::Reused, "last possible resend still reuses"); check(nonce.consume(ids.issuer, ids.target, ids.random, 299999, 2000000299), "original confirmation works exactly 180 seconds after last possible resend"); check(nonce.remainingSeconds(299999, 2000000299) == 0, "consumed token TTL is zero"); nonce.issue(ids.issuer, ids.target, ids.random, 0, 2000000000); for (uint32_t age : {119000U, 119999U, 120000U, 200000U, 299999U}) { const auto expected = age < 120000 ? Result::Reused : Result::AwaitingConfirmation; check(nonce.issue(ids.issuer, ids.target, ids.later_random, age, 2000000000 + age / 1000) == expected, "repeated requests obey original window"); } check(!nonce.matches(ids.issuer, ids.target, ids.random, 300000, 2000000300), "retries never extend lifetime"); nonce.clear(); nonce.issue(ids.issuer, ids.target, ids.random, 0, 2000000000); check(nonce.matches(ids.issuer, ids.target, ids.random, 180000, 2000000185), "confirmation-only phase allows positive clock tolerance"); check(nonce.matches(ids.issuer, ids.target, ids.random, 180000, 2000000175), "confirmation-only phase allows negative clock tolerance"); check(!nonce.matches(ids.issuer, ids.target, ids.random, 180000, 2000000186), "forward correction invalidates confirmation-only token"); check(!nonce.matches(ids.issuer, ids.target, ids.random, 180000, 2000000174), "backward correction invalidates confirmation-only token"); } static void invalidTests() { Identities ids; ReplayResetNonce nonce; check(nonce.issue(nullptr, ids.target, ids.random, 0, 1) == Result::Invalid, "null issuer"); check(nonce.issue(ids.issuer, nullptr, ids.random, 0, 1) == Result::Invalid, "null target"); check(nonce.issue(ids.issuer, ids.target, nullptr, 0, 1) == Result::Invalid, "null random"); check(nonce.issue(ids.issuer, ids.target, ids.zero, 0, 1) == Result::Invalid, "zero random"); check(nonce.issue(ids.issuer, ids.target, ids.random, 0, 0) == Result::Invalid, "zero epoch"); check(nonce.issue(ids.issuer, ids.target, ids.random, 0, 2000000000) == Result::Issued, "valid after invalid attempts"); check(!nonce.matches(nullptr, ids.target, ids.random, 0, 2000000000), "null matching issuer"); check(!nonce.matches(ids.issuer, nullptr, ids.random, 0, 2000000000), "null matching target"); check(!nonce.matches(ids.issuer, ids.target, nullptr, 0, 2000000000), "null matching token"); check(nonce.issue(ids.issuer, ids.target, nullptr, 0, 2000000000) == Result::Reused, "existing challenge does not require replacement randomness"); check(nonce.issue(ids.issuer, ids.target, nullptr, 120000, 2000000120) == Result::AwaitingConfirmation, "confirmation-only phase needs no replacement randomness"); for (size_t i = 0; i < 32; ++i) { uint8_t changed[32]; memcpy(changed, ids.issuer, 32); changed[i] ^= 1; check(!nonce.matches(changed, ids.target, ids.random, 0, 2000000000), "every issuer byte bound"); memcpy(changed, ids.target, 32); changed[i] ^= 1; check(!nonce.matches(ids.issuer, changed, ids.random, 0, 2000000000), "every target byte bound"); } for (size_t i = 0; i < 16; ++i) { uint8_t changed[16]; memcpy(changed, ids.random, 16); changed[i] ^= 1; check(!nonce.matches(ids.issuer, ids.target, changed, 0, 2000000000), "every token byte bound"); } } int main(int argc, char** argv) { try { if (argc != 2) throw std::runtime_error("expected test case"); const std::string test = argv[1]; if (test == "parser") parserTests(); else if (test == "lifecycle") lifecycleTests(); else if (test == "time") timeTests(); else if (test == "invalid") invalidTests(); else throw std::runtime_error("unknown test case"); } catch (const std::exception& error) { std::cerr << error.what() << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS; }