ruvnet / ruvnet/RuView

device_digest is not unique across boards, weakening the USB provisioning anti-swap guard

Open
#1,942 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug firmware
Dominant language
Rust
Stars
94.5k
Forks
12.5k
Avg merge
21h 27m
Merged PRs (30d)
43

Description

device_digest is not unique across boards, weakening the USB provisioning anti-swap guard

Repo: ruvnet/RuView (firmware) · Severity: Medium-High · Measured: 2026-09-14, three ESP32-C6 boards on 0.8.12

Symptom

Two physically distinct boards report the same device_digest over RUVIEW_HELLO_V1:

node 11   device_digest=062009cef304369d
node 12   device_digest=062009cef304369d    <-- identical
node 13   device_digest=e1303fbd5cd4cebd

Read in one pass across three ports, each nonce-matched, stable across reboots and across a
firmware reflash. Confirmed distinct hardware — esptool reports different efuse base MACs
for all three.

Not duplicate efuses

The firmware computes SHA-256("ruview-device-v1" || esp_efuse_mac_get_default())[0..8].
With distinct base MACs the digests cannot collide, so this was checked directly. Brute
force over the documented formula and every plausible variant — domain with and without the
NUL, raw / reversed / lowercase-hex / uppercase-hex / colon-separated MAC, 6-byte base and
8-byte extended MAC, prefix and suffix ordering — reproduces none of the three observed
digests
, including the unique one.

So the shipped digest is not a function of the MAC in the way the source describes, and it
is not the all-zeros failure path either: the firmware believes it succeeded. It is silently
wrong rather than failing closed.

Impact

deviceDigest is the anti-swap guard in Mac USB provisioning
(RuViewNodeOnboardingModule.swift:119):

let attempt = try hello(on: descriptor, path: portPath, ...)
guard let hello = attempt.node, hello.deviceDigest == expectedDeviceDigest else {
  throw RuViewNodeOnboardingError.identityMismatch
}

It re-runs HELLO before writing NVS to confirm the board is the one the operator selected.
With a colliding digest that guard cannot distinguish those two boards, so it degrades from
"verified identity" to "verified chip family". Serial paths do re-enumerate in practice —
observed during flashing — so a mix-up is reachable, and would write the wrong node ID to
the wrong board.

Detection is unaffected: the device list keys on serial path, so all boards appear
correctly.

Caveat on attribution

release_bins/c6-onboarding/esp32-csi-node.bin is a tracked binary blob, so it cannot be
proven that the serial_onboarding.c in the tree is the source that built it. The behaviour
is measured and reproducible; the cause needs a from-source rebuild to pin down.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with serial_onboarding.c and the RUVIEW_HELLO_V1 digest path, then compare it with the tracked release_bins/c6-onboarding/esp32-csi-node.bin; the issue notes that the binary may not come from the tree. Use the reported ESP32-C6 boards and efuse MACs to reproduce the mismatch from a source build. Check RuViewNodeOnboardingModule.swift:119 afterward to verify that distinct boards cannot pass the same identity guard.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, swift
Domain
embedded-iot, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.