What changed, and why it matters
This commit adds user-facing warnings to the COLDCARD hardware wallet before it installs firmware that is either signed by someone other than Coinkite (an 'external contributor') or older than the firmware already on the device (a downgrade). It does not block these installs; it only makes the user confirm they understand the extra risk. The change is defensive and improves security by reducing the chance a user accidentally installs risky firmware.
No urgent action is required; this is a defensive improvement. Users should ensure they upgrade to a firmware release containing this change and should pay attention to the new downgrade/external-signature warnings before approving future firmware installs.
Security signals we found
Adds explicit user warning for externally signed firmware images
Adds explicit user warning for firmware downgrades
Does not enforce a hard block; relies on user consent
Includes regression tests covering warning display and user approval paths
Acknowledges a suggestion from Huzaifa Jawaid in the changelog
Evidence from the diff
The patch modifies shared/auth.py’s firmware-upgrade interaction to inspect the firmware header’s public-key number (pk) and timestamp. If pk == 0, the code treats the image as externally signed and warns the user. If the new firmware’s timestamp is older than the currently installed firmware’s header timestamp, it warns that the install is a downgrade. The warning text is prepended to the existing confirmation story and the story title becomes ‘WARNING’. Tests are updated/added to verify both warnings appear under the correct conditions and that approval/cancellation still works.
Changed components
shared/auth.py (firmware upgrade user-interaction flow)testing/test_upgrades.py (firmware upgrade tests)releases/Next-ChangeLog.md (release notes)Inspect captured patch +77 / −4
### releases/Next-ChangeLog.md
@@ -7,6 +7,9 @@ your addition and anything else already in this file.**
# Shared Improvements - Both Mk and Q
+- Enhancement: Warn before installing firmware signed by an external contributor
+ or downgrading from the currently installed firmware. Thanks to Huzaifa Jawaid for his suggestion.
+
- Bugfix: Fix device crash when message-signing input is valid JSON but not an
object (NFC / QR / SD `.json` file). Thanks to [@Amiga500](https://github.com/Amiga500).
### shared/auth.py
@@ -1579,6 +1579,8 @@ def __init__(self, hdr, length, hdr_check=False, psram_offset=None):
async def interact(self):
from version import decode_firmware_header
+ from sigheader import FWH_PY_FORMAT
+ from ustruct import unpack_from
from utils import check_firmware_hdr
# check header values
@@ -1594,7 +1596,8 @@ async def interact(self):
return
# Get informed consent to upgrade.
- date, version, _ = decode_firmware_header(self.hdr)
+ date, fw_version, _ = decode_firmware_header(self.hdr)
+ _, timestamp, _, pk = unpack_from(FWH_PY_FORMAT, self.hdr)[0:4]
msg = '''\
Install this new firmware?
@@ -1603,10 +1606,22 @@ async def interact(self):
{built}
Binary checksum and signature will be further verified before any changes are made.
-'''.format(version=version, built=date)
+'''.format(version=fw_version, built=date)
+
+ warnings = []
+ if pk == 0:
+ warnings.append('''\
+This code was signed by an EXTERNAL CONTRIBUTOR and not Coinkite. It could do anything.''')
+
+ if timestamp < version.get_header_value('timestamp'):
+ warnings.append('''\
+Proposed firmware is a DOWNGRADE from the version already installed. It might contain already-fixed issues or security concerns.''')
+
+ if warnings:
+ msg = '\n\n'.join(warnings) + '\n\n' + msg
try:
- ch = await ux_show_story(msg)
+ ch = await ux_show_story(msg, title='WARNING' if warnings else None)
if ch == 'y':
if glob.PSRAM.psram_write_count != self.psram_write_count:
### testing/test_upgrades.py
@@ -123,8 +123,11 @@ def test_hacky_upgrade(mode, cap_story, transport, dev, sim_exec, make_firmware,
else:
upgrade_by_sd(data)
- _, story = cap_story()
+ title, story = cap_story()
+ assert title == "WARNING"
assert "Install this new firmware?" in story
+ assert "This code was signed by an EXTERNAL CONTRIBUTOR and not Coinkite." in story
+ assert "It could do anything." in story
press_cancel()
# check data was uploaded verbatim (VERY SLOW)
# for pos in range(0, cooked.firmware_length + 128, 128):
@@ -164,6 +167,8 @@ def test_upgrade_staged_image_tamper(make_firmware, upload_file, cap_story,
_, story = cap_story()
assert "Install this new firmware?" in story
assert "3.0.98" in story
+ assert "This code was signed by an EXTERNAL CONTRIBUTOR and not Coinkite." in story
+ assert "It could do anything." in story
try:
press_select()
@@ -176,4 +181,54 @@ def test_upgrade_staged_image_tamper(make_firmware, upload_file, cap_story,
"pa.firmware_upgrade = glob._fw_upgrade")
+@pytest.mark.parametrize('age', ['older', 'same', 'newer'])
+@pytest.mark.parametrize('external', [False, True])
+@pytest.mark.parametrize('approve', [False, True])
+def test_upgrade_confirmation(age, external, approve, make_firmware, upload_file,
+ cap_story, press_select, press_cancel, sim_exec, sim_eval,
+ is_q1, is_mark5):
+ hw = "q1" if is_q1 else (5 if is_mark5 else 4)
+ data = make_firmware(hw)
+ hdr = data[FW_HEADER_OFFSET:FW_HEADER_OFFSET+FW_HEADER_SIZE]
+ cooked = parse_hdr(hdr)
+ # Only exercise consent here; signature verification happens in the bootloader,
+ # whose entry point is replaced below so no test image is installed.
+ hdr = struct.pack(FWH_PY_FORMAT, *cooked._replace(pubkey_num=0 if external else 1))
+ data = data[:FW_HEADER_OFFSET] + hdr + data[FW_HEADER_OFFSET+FW_HEADER_SIZE:]
+ current_ts = bytearray(cooked.timestamp)
+ current_ts[0] += {'older': 1, 'same': 0, 'newer': -1}[age]
+
+ sim_exec("import glob, version; from pincodes import pa; "
+ "glob._get_header_value = version.get_header_value; "
+ "version.get_header_value = lambda fld: %r; "
+ "glob._fw_upgrade = pa.firmware_upgrade; "
+ "glob._fw_upgrade_called = False; "
+ "pa.firmware_upgrade = lambda *a: setattr(glob, '_fw_upgrade_called', True)"
+ % bytes(current_ts))
+ try:
+ upload_file(data + hdr)
+ title, story = cap_story()
+ assert (title == "WARNING") == (age == 'older' or external)
+ assert "Install this new firmware?" in story
+ assert "3.0.99" in story
+ assert ("Proposed firmware is a DOWNGRADE from the version already installed." in story) == (age == 'older')
+ assert ("It might contain already-fixed issues or security concerns." in story) == (age == 'older')
+ assert ("This code was signed by an EXTERNAL CONTRIBUTOR and not Coinkite." in story) == external
+ assert ("It could do anything." in story) == external
+ if external and age == 'older':
+ assert story.index("EXTERNAL CONTRIBUTOR") < story.index("DOWNGRADE") < story.index("Install this new firmware?")
+
+ if approve:
+ press_select()
+ else:
+ press_cancel()
+ time.sleep(1 if approve else 3)
+ assert sim_eval("glob._fw_upgrade_called") == str(approve)
+ assert sim_eval("__import__('auth').UserAuthorizedAction.active_request is None") == 'True'
+ finally:
+ sim_exec("import glob, version; from pincodes import pa; "
+ "version.get_header_value = glob._get_header_value; "
+ "pa.firmware_upgrade = glob._fw_upgrade")
+
+
# EOFWhy this scored 37/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.