Files
alro65 45642fda0e sprint-8: EKF + adaptive tuner + HWID + SHA-256 audit hash-chain
- 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>
2026-05-20 03:07:27 -04:00

72 lines
2.4 KiB
Python

"""Hardware ID binding and activation token -- Sprint 8.
The 6-byte ESP32 MAC (read via Modbus INPUT_HWID_MAC_01/23/45) is used to
bind a project license to a specific hardware unit.
Activation token format
-----------------------
The factory generates a token by HMAC-SHA256(key=SECRET, msg=hwid_hex),
where ``hwid_hex`` is the 12-character lower-case hex representation of the
6-byte MAC. The token is the first 16 bytes (32 hex chars) of the HMAC
output.
The SECRET is a per-product deployment key embedded in the Studio binary
(not in the open-source firmware). This file ships the *verification* side
only; token generation happens offline in the factory tooling.
For development / local testing a deterministic stub secret is used
(STUB_SECRET_KEY). The production key must be injected via the environment
variable ``AR_ACTIVATION_KEY``.
"""
from __future__ import annotations
import hashlib
import hmac
import os
STUB_SECRET_KEY = b"AR-Autopilot-Dev-Key-2026"
TOKEN_BYTES = 16 # 32 hex chars
def _get_secret() -> bytes:
key = os.environ.get("AR_ACTIVATION_KEY", "").encode()
return key if key else STUB_SECRET_KEY
def hwid_from_mac_words(mac01: int, mac23: int, mac45: int) -> str:
"""Convert three 16-bit Modbus words to a 12-char lower-case hex HWID string."""
mac = bytes([
(mac01 >> 8) & 0xFF, mac01 & 0xFF,
(mac23 >> 8) & 0xFF, mac23 & 0xFF,
(mac45 >> 8) & 0xFF, mac45 & 0xFF,
])
return mac.hex()
def generate_token(hwid_hex: str) -> str:
"""Generate the activation token for a given HWID.
Should only be called by factory tooling. In the Studio this is
called only during development / bench testing with the stub key.
"""
h = hmac.new(_get_secret(), hwid_hex.lower().encode(), hashlib.sha256)
return h.hexdigest()[:TOKEN_BYTES * 2]
def verify_token(hwid_hex: str, token: str) -> bool:
"""Return True iff the token is valid for the given HWID.
Uses constant-time comparison to resist timing attacks.
"""
expected = generate_token(hwid_hex)
return hmac.compare_digest(expected.lower(), token.lower())
def format_hwid(hwid_hex: str) -> str:
"""Format a 12-char hex HWID as 'AA:BB:CC:DD:EE:FF'."""
if len(hwid_hex) != 12:
raise ValueError(f"HWID must be 12 hex chars, got {len(hwid_hex)}")
h = hwid_hex.upper()
return ":".join(h[i:i+2] for i in range(0, 12, 2))