Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 25 additions & 13 deletions node/hardware_binding_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
DB_PATH = os.environ.get('RUSTCHAIN_DB_PATH') or os.environ.get('DB_PATH') or '/root/rustchain/rustchain_v2.db'
ENTROPY_TOLERANCE = 0.30 # 30% tolerance for entropy drift
MIN_COMPARABLE_FIELDS = 3 # require at least 3 non-zero entropy fields for quality
CORE_ENTROPY_FIELDS = ['clock_cv', 'cache_l1', 'cache_l2', 'thermal_ratio', 'jitter_cv']

def init_hardware_bindings_v2():
"""Create the v2 bindings table with entropy profiles."""
Expand Down Expand Up @@ -61,6 +62,14 @@ def extract_entropy_profile(fingerprint: dict) -> Dict:

return profile


def _count_nonzero_fields(profile: Dict) -> int:
return sum(1 for k in CORE_ENTROPY_FIELDS if float(profile.get(k, 0)) > 0)


def _count_comparable_nonzero_fields(stored: Dict, current: Dict) -> int:
return sum(1 for k in CORE_ENTROPY_FIELDS if float(stored.get(k, 0)) > 0 and float(current.get(k, 0)) > 0)

def compare_entropy_profiles(stored: Dict, current: Dict) -> Tuple[bool, float, str]:
"""
Compare two entropy profiles.
Expand Down Expand Up @@ -88,11 +97,12 @@ def compare_entropy_profiles(stored: Dict, current: Dict) -> Tuple[bool, float,
count = 0
hard_fails = 0

for key in ['clock_cv', 'cache_l1', 'cache_l2', 'thermal_ratio', 'jitter_cv']:
for key in CORE_ENTROPY_FIELDS:
stored_val = float(stored.get(key, 0))
current_val = float(current.get(key, 0))

if stored_val > 0:
# Compare only when BOTH sides provide non-zero signal for this field.
if stored_val > 0 and current_val > 0:
diff = abs(stored_val - current_val) / stored_val
field_tol = FIELD_TOLERANCE.get(key, ENTROPY_TOLERANCE)
total_diff += min(diff, 1.0) # Cap at 100% for averaging
Expand All @@ -106,12 +116,12 @@ def compare_entropy_profiles(stored: Dict, current: Dict) -> Tuple[bool, float,

# FIX: Handle no-fingerprint miners (both profiles are zeros)
if count == 0:
current_count = sum(1 for key in ['clock_cv', 'cache_l1', 'cache_l2', 'thermal_ratio', 'jitter_cv']
if float(current.get(key, 0)) > 0)
current_count = _count_nonzero_fields(current)
if current_count == 0:
return True, 1.0, 'no_fingerprint_data'
else:
return True, 0.5, 'stored_empty_current_has_data'
# No overlapping comparable fields; caller should treat as low-confidence comparison.
return True, 0.5, 'insufficient_comparable_overlap'

avg_diff = total_diff / count
similarity = 1.0 - avg_diff
Expand All @@ -133,8 +143,7 @@ def check_entropy_collision(entropy_profile: Dict, exclude_serial: str = None) -
Sparse profiles are considered low-quality and are ignored for collision matching.
"""
# Count non-zero fields in current profile
nonzero_fields = sum(1 for k in ['clock_cv', 'cache_l1', 'cache_l2', 'thermal_ratio', 'jitter_cv']
if float(entropy_profile.get(k, 0)) > 0)
nonzero_fields = _count_nonzero_fields(entropy_profile)

if nonzero_fields < MIN_COMPARABLE_FIELDS:
# Not enough entropy data to detect collisions reliably
Expand All @@ -152,15 +161,19 @@ def check_entropy_collision(entropy_profile: Dict, exclude_serial: str = None) -
if stored_json:
stored = json.loads(stored_json)
# Also require stored profile to have enough data
stored_nonzero = sum(1 for k in ['clock_cv', 'cache_l1', 'cache_l2', 'thermal_ratio', 'jitter_cv']
if float(stored.get(k, 0)) > 0)
stored_nonzero = _count_nonzero_fields(stored)
if stored_nonzero < MIN_COMPARABLE_FIELDS:
continue

comparable_nonzero = _count_comparable_nonzero_fields(stored, entropy_profile)
if comparable_nonzero < MIN_COMPARABLE_FIELDS:
# Sparse overlap is too weak for collision decisions.
continue

is_similar, score, _ = compare_entropy_profiles(stored, entropy_profile)

# Require stronger confidence; do not let highly-tolerant clock_cv dominate.
if is_similar and score > 0.97: # Very similar on sufficiently rich profiles
# Require stronger confidence on sufficiently rich, comparable profiles.
if is_similar and score > 0.97:
return serial_hash # Collision detected!

return None
Expand Down Expand Up @@ -193,8 +206,7 @@ def bind_hardware_v2(

if row is None:
# NEW HARDWARE - enforce entropy quality first
nonzero_fields = sum(1 for k in ['clock_cv', 'cache_l1', 'cache_l2', 'thermal_ratio', 'jitter_cv']
if float(entropy_profile.get(k, 0)) > 0)
nonzero_fields = _count_nonzero_fields(entropy_profile)
if nonzero_fields < MIN_COMPARABLE_FIELDS:
return False, 'entropy_insufficient', {
'error': 'Entropy profile quality too low for secure binding',
Expand Down
55 changes: 55 additions & 0 deletions tests/test_hardware_binding_v2_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,58 @@ def test_detect_collision_with_rich_entropy_profiles(tmp_path):
assert not ok2
assert reason2 == 'entropy_collision'
assert 'collision_hash' in details2


def test_collision_check_requires_min_comparable_overlap(tmp_path):
db = tmp_path / 'hb.db'
hb.DB_PATH = str(db)
hb.init_hardware_bindings_v2()

# Baseline binding with rich profile
fp_base = _mk_fingerprint(clock=0.20, l1=100.0, l2=220.0, thermal=1.8, jitter=0.07)
ok, reason, _ = hb.bind_hardware_v2(
serial='SER-BASE-2',
wallet='RTCwalletBase2',
arch='x86_64',
cores=8,
fingerprint=fp_base,
)
assert ok and reason == 'new_binding'

# Sparse-overlap payload: three non-zero fields, but only one overlaps with baseline (clock_cv)
# This must NOT be used for collision decisions.
crafted = {
'clock_cv': 0.20, # overlaps
'cache_l1': 0.0, # no overlap
'cache_l2': 0.0, # no overlap
'thermal_ratio': 0.0, # no overlap
'jitter_cv': 0.30, # non-zero but not present in stored if attacker manipulates payloads
}

# Force one more non-overlap non-zero to satisfy input quality gate
crafted['cache_l1'] = 0.01

# Make stored comparable overlap effectively < MIN by editing stored profile directly
with sqlite3.connect(str(db)) as conn:
conn.execute(
"UPDATE hardware_bindings_v2 SET entropy_profile = ? WHERE serial_hash = ?",
(
json.dumps({'clock_cv': 0.21, 'cache_l1': 0, 'cache_l2': 0, 'thermal_ratio': 0, 'jitter_cv': 0}),
hb.compute_serial_hash('SER-BASE-2', 'x86_64'),
),
)
conn.commit()

collision = hb.check_entropy_collision(crafted)
assert collision is None


def test_compare_entropy_profiles_marks_sparse_overlap_low_confidence():
stored = {'clock_cv': 0.2, 'cache_l1': 0, 'cache_l2': 0, 'thermal_ratio': 0, 'jitter_cv': 0}
current = {'clock_cv': 0.21, 'cache_l1': 0.0, 'cache_l2': 0.0, 'thermal_ratio': 0.0, 'jitter_cv': 0.3}

ok, score, reason = hb.compare_entropy_profiles(stored, current)
assert ok
assert reason in ('entropy_ok', 'insufficient_comparable_overlap')
# comparable overlap is only one field; ensure score does not imply a strong multi-signal match
assert score <= 1.0
Loading