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>
72 lines
2.4 KiB
Python
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))
|