45642fda0e
- heading_ekf.py: 2-state Kalman filter fusing PGN 127250 heading and 127251 ROT with shortest-arc innovation and symmetric covariance update - adaptive_tuner.py: gradient-descent outer-loop Kp/Ki adjuster bounded to ±adaptive_max_deviation_pct; oscillation vs steady-state detection - hwid.py: HMAC-SHA256 activation token (verify side); hwid_from_mac_words converts three Modbus uint16 MAC words to 12-char hex HWID - audit.py: SHA-256 hash-chain -- each JSONL line carries prev_hash and line_hash; verify_chain() detects tampering, deletion, insertion - firmware/system/hwid.h+cpp: esp_efuse_mac_get_default wrapper + FNV-32 hash + "AA:BB:CC:DD:EE:FF" formatter - modbus_registers.yaml + generated .h/.py: HWID_MAC_01/23/45 at input addrs 9/10/11 (three 16-bit words = 6-byte MAC) - modbus_slave.cpp: INPUT_HWID_MAC_01/23/45 cases read eFuse MAC - main.cpp: logs HWID string + FNV-32 hash at boot (activation traceability) - tests: 72 new tests (audit signing, EKF, adaptive tuner, HWID) -- 398 total Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
145 lines
5.0 KiB
Python
145 lines
5.0 KiB
Python
"""Tests for HWID activation token -- Sprint 8."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from arautopilot.core.hwid import (
|
|
STUB_SECRET_KEY,
|
|
TOKEN_BYTES,
|
|
format_hwid,
|
|
generate_token,
|
|
hwid_from_mac_words,
|
|
verify_token,
|
|
)
|
|
|
|
|
|
SAMPLE_HWID = "aabbccddeeff" # 12-char lower-case hex
|
|
|
|
|
|
class TestHwidFromMacWords:
|
|
def test_known_bytes(self):
|
|
# mac01=0xAABB, mac23=0xCCDD, mac45=0xEEFF → "aabbccddeeff"
|
|
result = hwid_from_mac_words(0xAABB, 0xCCDD, 0xEEFF)
|
|
assert result == "aabbccddeeff"
|
|
|
|
def test_zero_mac(self):
|
|
result = hwid_from_mac_words(0, 0, 0)
|
|
assert result == "000000000000"
|
|
|
|
def test_all_ones(self):
|
|
result = hwid_from_mac_words(0xFFFF, 0xFFFF, 0xFFFF)
|
|
assert result == "ffffffffffff"
|
|
|
|
def test_returns_lowercase(self):
|
|
result = hwid_from_mac_words(0xAABB, 0xCCDD, 0xEEFF)
|
|
assert result == result.lower()
|
|
|
|
def test_result_is_12_chars(self):
|
|
result = hwid_from_mac_words(0x0102, 0x0304, 0x0506)
|
|
assert len(result) == 12
|
|
|
|
def test_byte_order(self):
|
|
# 0x1234 → bytes [0x12, 0x34]
|
|
result = hwid_from_mac_words(0x1234, 0x0000, 0x0000)
|
|
assert result[:4] == "1234"
|
|
|
|
|
|
class TestGenerateToken:
|
|
def test_returns_32_hex_chars(self):
|
|
token = generate_token(SAMPLE_HWID)
|
|
assert len(token) == TOKEN_BYTES * 2 # 32
|
|
assert all(c in "0123456789abcdef" for c in token.lower())
|
|
|
|
def test_deterministic_with_stub_key(self, monkeypatch):
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY", raising=False)
|
|
t1 = generate_token(SAMPLE_HWID)
|
|
t2 = generate_token(SAMPLE_HWID)
|
|
assert t1 == t2
|
|
|
|
def test_different_hwid_different_token(self, monkeypatch):
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY", raising=False)
|
|
t1 = generate_token("aabbccddeeff")
|
|
t2 = generate_token("112233445566")
|
|
assert t1 != t2
|
|
|
|
def test_uses_env_key_when_set(self, monkeypatch):
|
|
monkeypatch.setenv("AR_ACTIVATION_KEY", "test-production-key")
|
|
prod_token = generate_token(SAMPLE_HWID)
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY")
|
|
stub_token = generate_token(SAMPLE_HWID)
|
|
assert prod_token != stub_token
|
|
|
|
def test_case_insensitive_hwid(self, monkeypatch):
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY", raising=False)
|
|
t_lower = generate_token("aabbccddeeff")
|
|
t_upper = generate_token("AABBCCDDEEFF")
|
|
assert t_lower == t_upper
|
|
|
|
def test_matches_manual_hmac(self, monkeypatch):
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY", raising=False)
|
|
expected = hmac.new(
|
|
STUB_SECRET_KEY, SAMPLE_HWID.encode(), hashlib.sha256
|
|
).hexdigest()[:TOKEN_BYTES * 2]
|
|
assert generate_token(SAMPLE_HWID) == expected
|
|
|
|
|
|
class TestVerifyToken:
|
|
def test_valid_token_returns_true(self, monkeypatch):
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY", raising=False)
|
|
token = generate_token(SAMPLE_HWID)
|
|
assert verify_token(SAMPLE_HWID, token) is True
|
|
|
|
def test_wrong_token_returns_false(self, monkeypatch):
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY", raising=False)
|
|
assert verify_token(SAMPLE_HWID, "a" * 32) is False
|
|
|
|
def test_wrong_hwid_returns_false(self, monkeypatch):
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY", raising=False)
|
|
token = generate_token(SAMPLE_HWID)
|
|
assert verify_token("000000000000", token) is False
|
|
|
|
def test_case_insensitive_token_comparison(self, monkeypatch):
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY", raising=False)
|
|
token = generate_token(SAMPLE_HWID)
|
|
assert verify_token(SAMPLE_HWID, token.upper()) is True
|
|
|
|
def test_constant_time_compare(self, monkeypatch):
|
|
"""verify_token must use hmac.compare_digest (checked by smoke-test, not timing)."""
|
|
monkeypatch.delenv("AR_ACTIVATION_KEY", raising=False)
|
|
token = generate_token(SAMPLE_HWID)
|
|
# Calling with correct and incorrect tokens both return without exception
|
|
assert verify_token(SAMPLE_HWID, token) is True
|
|
assert verify_token(SAMPLE_HWID, "x" * 32) is False
|
|
|
|
|
|
class TestFormatHwid:
|
|
def test_formats_correctly(self):
|
|
result = format_hwid("aabbccddeeff")
|
|
assert result == "AA:BB:CC:DD:EE:FF"
|
|
|
|
def test_uppercase_output(self):
|
|
result = format_hwid("aabbccddeeff")
|
|
assert result == result.upper().replace("X", ":") # colons preserved
|
|
assert result == "AA:BB:CC:DD:EE:FF"
|
|
|
|
def test_colon_separated_6_groups(self):
|
|
result = format_hwid("112233445566")
|
|
parts = result.split(":")
|
|
assert len(parts) == 6
|
|
assert all(len(p) == 2 for p in parts)
|
|
|
|
def test_invalid_length_raises(self):
|
|
with pytest.raises(ValueError, match="12 hex chars"):
|
|
format_hwid("aabb")
|
|
|
|
def test_all_zeros(self):
|
|
assert format_hwid("000000000000") == "00:00:00:00:00:00"
|
|
|
|
def test_all_ff(self):
|
|
assert format_hwid("ffffffffffff") == "FF:FF:FF:FF:FF:FF"
|