Public Access
One firmware: the Debug Console in every build, off until switched on, with the device's own token
There is no Debug Build any more (ADR 0010, issue #68, Q188 to Q195). The console and the test commands are compiled into every firmware. It listens only while Settings > Debug Console is on, which isn't the default; off, neither its task nor its 4 KB ring exists. The token is made by the device and shown on that page; a client proves it knows it by answering a challenge with an HMAC, so it never crosses the network, and five wrong answers close the console for a minute. DBG in the Status Bar while it listens. Over USB serial only: debug on, debug token <value>, debug token new. scripts/flash.sh --debug uses them to set a device up with the developer's token. scripts/rdbg.py takes the token from -t, $RORO_DEBUG_TOKEN or the file, answers the challenge, and fetches a release's ELF to decode a crash. Gone: the cardputer-adv-debug environment, RORO_DEBUG, the +debug version, scripts/debug_flags.py, update install ... force, and the rule that a Debug Build doesn't install releases. Old clients and old firmwares don't talk to each other. Against the builds it replaces: 30 KB more flash and 88 bytes more static RAM than the release, 4 KB less RAM than the Debug Build. 468 host tests. Checked on the device: off by default, login, the pause after wrong tokens, Safe Mode with the console, the setting surviving an update, debug off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
#include <unity.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "debug_auth.h"
|
||||
|
||||
using namespace roro::debug;
|
||||
|
||||
void setUp() {}
|
||||
void tearDown() {}
|
||||
|
||||
static std::string hmacHex(const std::string& key, const std::string& message) {
|
||||
uint8_t mac[32];
|
||||
hmacSha256(reinterpret_cast<const uint8_t*>(key.data()), key.size(), reinterpret_cast<const uint8_t*>(message.data()),
|
||||
message.size(), mac);
|
||||
return toHex(mac, sizeof mac);
|
||||
}
|
||||
|
||||
// RFC 4231, test cases 1, 2 and 6.
|
||||
void test_hmac_matches_the_rfc_vectors() {
|
||||
TEST_ASSERT_EQUAL_STRING("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7",
|
||||
hmacHex(std::string(20, '\x0b'), "Hi There").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843",
|
||||
hmacHex("Jefe", "what do ya want for nothing?").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54",
|
||||
hmacHex(std::string(131, '\xaa'), "Test Using Larger Than Block-Size Key - Hash Key First").c_str());
|
||||
}
|
||||
|
||||
void test_a_made_token_is_20_characters_without_lookalikes() {
|
||||
uint8_t zeros[kTokenRandom] = {}, ones[kTokenRandom], counting[kTokenRandom];
|
||||
memset(ones, 0xff, sizeof ones);
|
||||
for (size_t i = 0; i < sizeof counting; i++) counting[i] = static_cast<uint8_t>(i);
|
||||
TEST_ASSERT_EQUAL_STRING("00000000000000000000", makeToken(zeros).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("ZZZZZZZZZZZZZZZZZZZZ", makeToken(ones).c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("000G40R40M30E209185G", makeToken(counting).c_str()); // as Python's reference gives
|
||||
for (char c : makeToken(counting)) TEST_ASSERT_NULL(strchr("ILOU", c));
|
||||
TEST_ASSERT_TRUE(validToken(makeToken(counting)));
|
||||
}
|
||||
|
||||
void test_a_typed_token_is_tidied() {
|
||||
TEST_ASSERT_EQUAL_STRING("K7QF3M2X9WBDHT4P6RNC", tidyToken("k7qf-3m2x 9wbd-HT4P-6rnc\n").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("K7QF-3M2X-9WBD-HT4P-6RNC", groupToken("K7QF3M2X9WBDHT4P6RNC").c_str());
|
||||
TEST_ASSERT_EQUAL_STRING("K7QF3M2X9WBDHT4P6RNC", tidyToken(groupToken("K7QF3M2X9WBDHT4P6RNC")).c_str());
|
||||
// Misread as letters the alphabet doesn't have: taken for the digits they look like.
|
||||
TEST_ASSERT_EQUAL_STRING("1010", tidyToken("IOlo").c_str());
|
||||
}
|
||||
|
||||
void test_a_token_needs_16_to_64_printable_characters() {
|
||||
TEST_ASSERT_FALSE(validToken(""));
|
||||
TEST_ASSERT_FALSE(validToken("1234"));
|
||||
TEST_ASSERT_FALSE(validToken(std::string(15, 'A')));
|
||||
TEST_ASSERT_TRUE(validToken(std::string(16, 'A')));
|
||||
TEST_ASSERT_TRUE(validToken(std::string(64, 'A')));
|
||||
TEST_ASSERT_FALSE(validToken(std::string(65, 'A')));
|
||||
TEST_ASSERT_FALSE(validToken("AAAAAAAAAAAAAAA A")); // untidied: a space
|
||||
TEST_ASSERT_FALSE(validToken("aaaaaaaaaaaaaaaaaaaa")); // untidied: small letters
|
||||
TEST_ASSERT_FALSE(validToken("AAAAAAAAAAAAAAAAAAAO")); // untidied: an O nobody could ever match
|
||||
TEST_ASSERT_TRUE(validToken(tidyToken("hello-world-this-is-long")));
|
||||
TEST_ASSERT_FALSE(validToken(std::string(16, '\x01')));
|
||||
// The token of before, 32 hex digits from a file, still fits once tidied.
|
||||
TEST_ASSERT_TRUE(validToken(tidyToken("0f1e2d3c4b5a69788796a5b4c3d2e1f0")));
|
||||
}
|
||||
|
||||
// The same vector scripts/rdbg.py is checked against: Python's hmac.new(token, nonce, sha256).
|
||||
void test_the_answer_is_the_hmac_of_the_nonce() {
|
||||
uint8_t nonce[kNonceBytes];
|
||||
for (size_t i = 0; i < sizeof nonce; i++) nonce[i] = static_cast<uint8_t>(i);
|
||||
TEST_ASSERT_EQUAL_STRING("e1246c7e74d711cdb27d8d9346515829a01cf46d79fbbc1137885446f12ed2ba",
|
||||
answerFor("K7QF3M2X9WBDHT4P6RNC", nonce).c_str());
|
||||
nonce[0] ^= 1; // another challenge, another answer: a recorded one is no use
|
||||
TEST_ASSERT_FALSE(answerFor("K7QF3M2X9WBDHT4P6RNC", nonce) == "e1246c7e74d711cdb27d8d9346515829a01cf46d79fbbc1137885446f12ed2ba");
|
||||
}
|
||||
|
||||
void test_same_text() {
|
||||
TEST_ASSERT_TRUE(sameText("abc", "abc"));
|
||||
TEST_ASSERT_FALSE(sameText("abc", "abd"));
|
||||
TEST_ASSERT_FALSE(sameText("ab", "abc"));
|
||||
TEST_ASSERT_FALSE(sameText("abcd", "abc"));
|
||||
TEST_ASSERT_FALSE(sameText("", "abc"));
|
||||
TEST_ASSERT_TRUE(sameText("", ""));
|
||||
}
|
||||
|
||||
void test_five_wrong_answers_lock_for_a_minute() {
|
||||
AuthGate gate;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
TEST_ASSERT_FALSE(gate.failed(1000 + i));
|
||||
TEST_ASSERT_FALSE(gate.locked(1000 + i));
|
||||
}
|
||||
TEST_ASSERT_TRUE(gate.failed(2000)); // the fifth starts the pause
|
||||
TEST_ASSERT_TRUE(gate.locked(2001));
|
||||
TEST_ASSERT_FALSE(gate.failed(2002)); // nothing is counted meanwhile
|
||||
TEST_ASSERT_TRUE(gate.locked(2000 + AuthGate::kLockMs - 1));
|
||||
TEST_ASSERT_FALSE(gate.locked(2000 + AuthGate::kLockMs));
|
||||
TEST_ASSERT_EQUAL(0, gate.failures()); // and the count starts again
|
||||
}
|
||||
|
||||
void test_a_right_answer_forgets_the_wrong_ones() {
|
||||
AuthGate gate;
|
||||
for (int i = 0; i < 4; i++) gate.failed(i);
|
||||
gate.succeeded();
|
||||
for (int i = 0; i < 4; i++) TEST_ASSERT_FALSE(gate.failed(10 + i));
|
||||
TEST_ASSERT_FALSE(gate.locked(20));
|
||||
}
|
||||
|
||||
void test_the_lock_holds_across_the_clock_wrap() {
|
||||
AuthGate gate;
|
||||
uint32_t t = 0xFFFFFFF0u; // 16 ms before millis() wraps
|
||||
for (int i = 0; i < 5; i++) gate.failed(t);
|
||||
TEST_ASSERT_TRUE(gate.locked(t + 100)); // after the wrap: still locked
|
||||
TEST_ASSERT_TRUE(gate.locked(t + AuthGate::kLockMs - 1));
|
||||
TEST_ASSERT_FALSE(gate.locked(t + AuthGate::kLockMs));
|
||||
}
|
||||
|
||||
int main(int, char**) {
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_hmac_matches_the_rfc_vectors);
|
||||
RUN_TEST(test_a_made_token_is_20_characters_without_lookalikes);
|
||||
RUN_TEST(test_a_typed_token_is_tidied);
|
||||
RUN_TEST(test_a_token_needs_16_to_64_printable_characters);
|
||||
RUN_TEST(test_the_answer_is_the_hmac_of_the_nonce);
|
||||
RUN_TEST(test_same_text);
|
||||
RUN_TEST(test_five_wrong_answers_lock_for_a_minute);
|
||||
RUN_TEST(test_a_right_answer_forgets_the_wrong_ones);
|
||||
RUN_TEST(test_the_lock_holds_across_the_clock_wrap);
|
||||
return UNITY_END();
|
||||
}
|
||||
Reference in New Issue
Block a user