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
11 changes: 1 addition & 10 deletions cc2flash/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1343,13 +1343,7 @@ def analyze_image(
errors.append(str(exc))
config_info = None

if config_info is not None and UOID_PATTERN.fullmatch(uoid) is not None:
serial_value = config_info["serial_value"]
if serial_value[:12] != uoid[:12]:
errors.append(
"serial.cfg and the HWCONFIG UOID do not share the expected "
"12-byte unit prefix"
)
if config_info is not None:
if config_info["serial_source"] != "live-current":
warnings.append(
"serial.cfg was recovered from a unique historical JFFS2 node "
Expand Down Expand Up @@ -1382,7 +1376,6 @@ def analyze_image(
"serial_value": config_info["serial_value"],
"serial_sha256": sha256(config_info["serial_payload"]),
"serial_source": config_info["serial_source"],
"serial_uoid_prefix_match": True,
"config_node_count": config_info["node_count"],
"config_current_node_count": config_info["current_node_count"],
"config_obsolete_node_count": config_info["obsolete_node_count"],
Expand Down Expand Up @@ -1483,7 +1476,6 @@ def format_analysis(analysis: dict[str, Any], show_serial: bool = False) -> str:
serial.cfg source: {analysis['serial_source']}
serial.cfg value: {serial_display}
serial.cfg SHA-256: {analysis['serial_sha256']}
Serial/UOID prefix: PASS

JFFS2 config
------------
Expand Down Expand Up @@ -1715,7 +1707,6 @@ def build_recovery(
"serial_masked": mask_value(analysis["serial_value"]),
"serial_sha256": analysis["serial_sha256"],
"uoid_masked": mask_value(analysis["uoid"]),
"serial_uoid_prefix_match": True,
"config_usage_percent_before": round(
analysis["config_usage_percent"], 6
),
Expand Down
5 changes: 2 additions & 3 deletions hardware-recovery/CC2_RECOVERY_VERIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,11 @@ The validator excludes only the known unit-specific HWCONFIG fields and mutable
- the exact stock or audited patched SquashFS window;
- a 94-byte UOID with the observed character structure;
- exactly one recoverable serial matching the observed serial format;
- the observed 12-byte common prefix between serial and UOID;
- valid JFFS2 CRCs and only known config filenames.

Every raw image requires three byte-identical physical reads by default. `--allow-fewer-reads` is an explicit reduced-confidence escape hatch and is recorded with a warning in generated artifacts.

The proprietary full serial↔UOID derivation and the meaning of the two-byte HWCONFIG check value remain unknown. The tool therefore preserves all of those bytes exactly but cannot cryptographically prove their full mutual relationship. This is the principal residual cross-device identity risk. Requiring multiple physical reads minimizes corruption risk without inventing an unverified formula.
The proprietary full serial↔UOID derivation and the meaning of the two-byte HWCONFIG check value remain unknown. The tool therefore validates the serial and UOID structures independently and preserves all of those bytes exactly, but it cannot prove their full mutual relationship. This is the principal residual cross-device identity risk. Requiring multiple physical reads minimizes corruption risk without inventing an unverified formula.

## Auditability improvements in v1.1

Expand Down Expand Up @@ -95,7 +94,7 @@ All executed tests passed:
- an already patched canonical output rebuilt idempotently with no write region;
- a structurally valid unseen serial/UOID with three matching reads was accepted and preserved;
- one-bit mutations in kernel or the system patch window were rejected;
- a serial from one real unit combined with the other unit's UOID was rejected;
- structurally valid serial and UOID values with different prefixes were accepted and preserved;
- keeping either exhausted config was rejected;
- a one-bit programmer readback mismatch was rejected;
- the deterministic XZ, readable source replacement, JFFS2 writer/parser, strict post-build validation, and shell syntax checks all passed.
Expand Down
2 changes: 1 addition & 1 deletion hardware-recovery/REFERENCE_FINGERPRINTS.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"raw_hash_is_not_invariant": true,
"size": 131072,
"start": "0x7E0000",
"validation": "JFFS2 structure/CRCs, known names, unambiguous serial, serial/UOID prefix"
"validation": "JFFS2 structure/CRCs, known names, unambiguous serial"
},
"format_version": 3,
"full_size": 8388608,
Expand Down
13 changes: 6 additions & 7 deletions hardware-recovery/TECHNICAL_DETAILS.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,7 @@ The validator therefore performs the strongest safe equivalent of “100% match
`--wipe-unknown-config` is explicit;
- preserve-files accepts additional live names only when every entry can be
safely reconstructed as a regular root file;
- exactly one unambiguous `serial.cfg` value must be recoverable;
- the serial and UOID must share the expected 12-byte unit prefix.
- exactly one unambiguous `serial.cfg` value must be recoverable.

No other differences are accepted. See `REFERENCE_FINGERPRINTS.json`.

Expand Down Expand Up @@ -200,12 +199,12 @@ This is a mitigation for the deterministic per-boot write leak. It does not repa
The tool supports only the exact firmware build and HWCONFIG shapes represented by the embedded fingerprints. A future Elegoo/Jovision build or another HWCONFIG record shape must be analyzed and fingerprinted separately.

Three real unit identities were directly inspected, including one with the
261-byte HWCONFIG record. Cross-serial support is fail-closed: invariant
firmware must match the identified variant exactly; the UOID and serial must
each match their observed structure and share the observed 12-byte prefix; all
unit-specific HWCONFIG bytes and the recovered serial payload are preserved
261-byte HWCONFIG record. Invariant firmware must match the identified variant
exactly, while the UOID and serial must each match their observed structure.
All unit-specific HWCONFIG bytes and the recovered serial payload are preserved
exactly. Because the proprietary full serial↔UOID derivation is unknown, the
tool cannot prove more than that relationship for a previously unseen unit.
tool does not infer or enforce a relationship between those independently
validated fields.

Static analysis can prove that a bricked dump is transformed into the exact known recovery bytes and that all relevant filesystems/checksums validate. Actual boot recovery still requires a controlled flash, full readback verification, and an observed successful boot; software-only review cannot replace that hardware test.

Expand Down
3 changes: 2 additions & 1 deletion hardware-recovery/TEST_RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ byte-for-byte with the newly generated clean-data image in this review.
rejected unless `--allow-fewer-reads` is explicit.
- One-bit kernel mutation: rejected.
- One-bit system-patch-window mutation: rejected.
- Serial from one unit combined with another unit's UOID: rejected.
- Structurally valid serial and UOID values with different prefixes: accepted
and preserved independently.
- `--keep-config` on either exhausted bricked partition: rejected.
- One-bit full-programmer readback mismatch: rejected with the affected region.
- Rebuilding an already patched canonical image: idempotent; no write region reported.
Expand Down
86 changes: 86 additions & 0 deletions tests/test_implementation.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"""Offline deterministic image-generation checks (no public CLI command)."""
import io
import json
import lzma
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
from contextlib import redirect_stdout
from unittest import mock

from cc2flash import image as tool
from cc2flash.image import (
ORIGINAL_COPY_BLOCK, PATCHED_COPY_BLOCK_VISIBLE, ValidationError,
build_squashfs_xz_fragment, serial_payload_is_valid, build_minimal_config,
Expand Down Expand Up @@ -62,3 +67,84 @@ def test_deterministic_patch_xz_and_jffs2(self):
with redirect_stdout(io.StringIO()):
implementation_checks()

def test_serial_and_uoid_are_validated_independently(self):
image = bytearray(b"\0" * tool.FLASH_SIZE)
uoid = b"12PSSSS4DIFF" + b"A" * 82
image[tool.HW_UOID_START:tool.HW_UOID_END] = uoid
image[tool.HW_CHECK_START:tool.HW_CHECK_END] = b"\x01\x02"

serial_payload = (
b"serial=12PSSSS4TEST000000000000000000000000\n"
)
serial_value = serial_payload.removeprefix(b"serial=").removesuffix(b"\n")
self.assertNotEqual(serial_value[:12], uoid[:12])
image[tool.CONFIG_START:tool.CONFIG_END] = tool.build_minimal_config(
serial_payload
)
image = bytes(image)

variant = {
"before_identity_sha256": tool.sha256(
image[0x7D0000:tool.HW_CHECK_START]
),
"after_uoid_sha256": tool.sha256(
image[tool.HW_UOID_END:tool.CONFIG_START]
),
"invariant_sha256": tool.sha256(tool.invariant_bytes(image)),
}

with TemporaryDirectory() as temp:
root = Path(temp)
primary = root / "primary.bin"
confirmation_one = root / "confirmation-one.bin"
confirmation_two = root / "confirmation-two.bin"
for path in (primary, confirmation_one, confirmation_two):
path.write_bytes(image)

output = root / "recovery"
with (
mock.patch.object(tool, "REFERENCE_SEGMENTS", {}),
mock.patch.object(
tool,
"PATCHED_PATCH_SHA256",
tool.sha256(image[tool.PATCH_START:tool.PATCH_END]),
),
mock.patch.object(
tool,
"identify_hwconfig_variant",
return_value=("synthetic", variant),
),
):
result = tool.build_recovery(
primary,
output,
confirmation_paths=(confirmation_one, confirmation_two),
config_mode="serial-only",
wipe_unknown_config=False,
show_serial=False,
allow_fewer_reads=False,
)

recovery = (output / "cc2-camera-recovery.bin").read_bytes()
self.assertEqual(
recovery[0x7D0000:tool.CONFIG_START],
image[0x7D0000:tool.CONFIG_START],
)
self.assertEqual(recovery[tool.HW_UOID_START:tool.HW_UOID_END], uoid)
self.assertEqual((output / "serial.cfg").read_bytes(), serial_payload)
rebuilt = tool.extract_serial_and_config_info(
recovery[tool.CONFIG_START:tool.CONFIG_END]
)
self.assertEqual(rebuilt["serial_payload"], serial_payload)
self.assertNotIn("serial_uoid_prefix_match", result["analysis"])
self.assertNotIn("serial_uoid_prefix_match", result["output_analysis"])
self.assertNotIn(
"Serial/UOID prefix",
(output / "VALIDATION.txt").read_text(encoding="utf-8"),
)
generated_manifest = json.loads(
(output / "MANIFEST.json").read_text(encoding="utf-8")
)
self.assertNotIn(
"serial_uoid_prefix_match", generated_manifest["validation"]
)