Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bootloader: add OP_HARDWARE api endpoint #1347

Merged
merged 2 commits into from
Jan 22, 2025

Conversation

Beerosagos
Copy link
Collaborator

@Beerosagos Beerosagos commented Dec 23, 2024

This will allow the bbapp to flash different FW versions, depending on the PCB variants. The initial implementation will respond with a single byte that identifies the Secure Chip: 0 for ATECC, 1 for Optiga.

Note: Based on the changes from #1341

@Beerosagos Beerosagos force-pushed the bootloader-sc branch 2 times, most recently from 16ab407 to fbf257d Compare December 23, 2024 14:52
@Beerosagos Beerosagos marked this pull request as ready for review December 23, 2024 15:11
@Beerosagos Beerosagos requested a review from benma December 23, 2024 15:11
Copy link
Collaborator

@benma benma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obligatory 😄
changelog

@@ -94,6 +94,15 @@ def versions(self) -> typing.Tuple[int, int]:
firmware_v, signing_pubkeys_v = struct.unpack("<II", response[:8])
return firmware_v, signing_pubkeys_v

def hardware(self) -> str:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please no stringly types. An enum like this would be better imho:

https://github.com/BitBoxSwiss/bitbox02-firmware/blob/master/py/bitbox02/bitbox02/communication/bitbox_api_protocol.py#L187

Also this should return the ATECC variant if the bootloader version does not support this endpoint. Right now this call fails for old bootloaders.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

Comment on lines 773 to 778
uint8_t type = 0;
if (memory_get_securechip_type() == MEMORY_SECURECHIP_TYPE_OPTIGA) {
type = 1;
}
memcpy(output + BOOT_OP_LEN, &type, 1);
return BOOT_OP_LEN + 1;
Copy link
Collaborator

@benma benma Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second byte of the output for all api commands is the success/error byte, but it's missing here. The bootloader ._query() call checks the second byte for error. It seems that you put the hardware type into this byte, so the Python client does not actually work as expected and raise an exception on the Optiga chip (maybe, haven't tested, I might be missing sth).

You should set it by using return _report_status(OP_STATUS_OK, output) + ...;

The memcpy of a single byte is a bit awkward. You can just do output[2] = type

Copy link
Collaborator

@benma benma Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at some other api calls, _api_set_show_firmware_hash is borked - it says it returns a disabled/enabled byte and sets it like this

output[BOOT_OP_LEN] = data.fields.show_firmware_hash;
return _report_status(result, output) + 1;

But _report_status call overwrites the second byte with the success byte. The client libraries also don't return any result here. Could you also add a commit or PR that cleans this up?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_api_versions is also a bit borked: returns the size of three versions, but only returns two versions 🤦

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at some other api calls, _api_set_show_firmware_hash is borked - it says it returns a disabled/enabled byte and sets it like this

output[BOOT_OP_LEN] = data.fields.show_firmware_hash;
return _report_status(result, output) + 1;

But _report_status call overwrites the second byte with the success byte. The client libraries also don't return any result here. Could you also add a commit or PR that cleans this up?

I don't think this is right. BOOT_OP_LEN is 2, so the show_firmware_hash result is in the 3rd byte of the array. Am I missing something?

Copy link
Collaborator Author

@Beerosagos Beerosagos Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_api_versions is also a bit borked: returns the size of three versions, but only returns two versions 🤦

Right 😅 The Py client is expecting only two versions. For backward compatibility, I guess it's better to change the return size instead of adding the third version. Wdyt?

[EDIT] On the other side, we need to be able to tell the bootloader version, I guess. Also because calling the OP_HARDWARE on an older bootloader shows an error on the display, so it would be better to avoid calling it, if possible.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bootloader version is available through the HID descriptor serial field.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a second commit

@Beerosagos
Copy link
Collaborator Author

Obligatory 😄

Let's make a PR template that includes a changelog checkbox, pls pls pls 🙏 😂

return Hardware.OPTIGA
except:
# If the bootloader does not support the call, it must have the ATECC SC.
pass
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You said the device will show an error on the screen in this case. Better to check the bootloader version as you suggested in the other comment.

While you are at it, you should bump the bootloader version to v1.1.0 then, as it's a new API call and not a patch.

@@ -191,6 +191,13 @@ class BitBox02Edition(enum.Enum):
BTCONLY = "btconly"


class Hardware(enum.Enum):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this here instead of in bootloader.py?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved

CHANGELOG.md Outdated
@@ -150,6 +150,9 @@ customers cannot upgrade their bootloader, its changes are recorded separately.

## Bootloader

### [Unreleased]
- Implement OP_HARDWARE endpoint, to identify the SC model
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SC=secure chip. Imho abbreviations just make it harder to read, not everyone knows what it means.

@Beerosagos Beerosagos force-pushed the bootloader-sc branch 2 times, most recently from d977ff5 to 48561ef Compare January 14, 2025 16:15
@Beerosagos
Copy link
Collaborator Author

rebased

@Beerosagos
Copy link
Collaborator Author

@benma this should be ready for a new review, PTAL 🙏

Returns (hardware variant).
"""
# Previous bootloader versions does not support the call and have ATECC SC.
if self.version > "1.0.6":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

">= 1.1.0".

Copy link
Collaborator Author

@Beerosagos Beerosagos Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did this initially, but it wasn't working well with dev version, e.g. 1.1.0-pre+48561ef480.dev >= 1.1.0, seemed to return false in my tests. I'll double check, tho.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked again, it doesn't work for versions 1.1.0 with prerelease. I'd keep > 1.0.6, if you agree

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check this out:

# Delete the prelease part, as it messes with the comparison (e.g. 3.0.0-pre < 3.0.0 is
# True, but the 3.0.0-pre has already the same API breaking changes like 3.0.0...).
self.version = semver.VersionInfo(
self.version.major, self.version.minor, self.version.patch, build=self.version.build
)

The >1.0.6 way does not work consistently, so imho better to apply the above here as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's nice! But why >1.0.6 doesn't work consistently? Anyway, your suggestion sounds better 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. I found a more readble/compact way to remove the pre-release self.version.replace(prerelease=None). Should I update also the code you linked while I'm here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that's nice! But why >1.0.6 doesn't work consistently? Anyway, your suggestion sounds better 👍

1.0.7 could be released that patches something in 1.0.6 before 1.1.0 goes out, for example. Less likely in the bootloader, but did happen in the firmware a few times.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benma Good point, I didn't think about it. Anyway, I updated the code. Hopefully this could be the last round of review 🙏

# Previous bootloader versions does not support the call and have ATECC SC.
if self.version > "1.0.6":
response = self._query(b"W")
if response[:2] == b"\x01":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can two bytes equal one byte? 😁

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shoot, how did I miss this? 🙈

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol, it worked by coincidence because the query call already strips the first 2 bytes, so response[:2] was actually 1 byte long

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 👍

@Beerosagos Beerosagos force-pushed the bootloader-sc branch 3 times, most recently from 21de44c to 941c857 Compare January 15, 2025 11:09
@Beerosagos Beerosagos requested a review from benma January 15, 2025 11:10
The endpoint provides firmware and signing pubkeys versions, but
the function was returning the lenght of three versions struct,
instead of two.
@Beerosagos Beerosagos force-pushed the bootloader-sc branch 2 times, most recently from 53efaf9 to fc64fee Compare January 15, 2025 15:17
Copy link
Collaborator

@benma benma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good, but I still want to test it. Left a tiny nit.

@@ -75,6 +84,9 @@ def __init__(self, transport: TransportLayer, device_info: DeviceInfo):
"bb02btc-bootloader": SIGDATA_MAGIC_BTCONLY,
"bitboxbase-bootloader": SIGDATA_MAGIC_BITBOXBASE_STANDARD,
}.get(device_info["product_string"])
self.version = parse_device_version(device_info["serial_number"])
# we remove the prerelease as it messes up same-version comparisons.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the other comment was clearer 😇

        # Delete the prelease part, as it messes with the comparison (e.g. 3.0.0-pre < 3.0.0 is
        # True, but the 3.0.0-pre has already the same API breaking changes like 3.0.0...).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied the same comment here 👍

Copy link
Collaborator

@benma benma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK

@@ -94,6 +107,17 @@ def versions(self) -> typing.Tuple[int, int]:
firmware_v, signing_pubkeys_v = struct.unpack("<II", response[:8])
return firmware_v, signing_pubkeys_v

def hardware(self) -> Hardware:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When adding the same API to the Go package I realized that this endpoint could return more HW info in the future, so having Hardware==Securechip like here will be a breaking change when that happens. It's not that important as I think there is no production use of the bootloader Python API, but technically Hardware should be an object (TypedDict?) with a SecureChipModel enum inside.

BitBoxSwiss/bitbox02-api-go#116

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, I also updated the check of the response to print unexpected values, so it behaves the same as your PR

This will allow the bbapp to flash different FW versions, depending
on the PCB variants. The initial implementation will respond with a
single byte that identifies the Secure Chip: 0 for ATECC, 1 for Optiga.
Copy link
Collaborator

@benma benma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM thanks!

@Beerosagos Beerosagos merged commit 10f2229 into BitBoxSwiss:master Jan 22, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants