-
Notifications
You must be signed in to change notification settings - Fork 98
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
Conversation
16ab407
to
fbf257d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@@ -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: |
There was a problem hiding this comment.
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:
Also this should return the ATECC variant if the bootloader version does not support this endpoint. Right now this call fails for old bootloaders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👍
src/bootloader/bootloader.c
Outdated
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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 🤦
There was a problem hiding this comment.
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 thisoutput[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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
Let's make a PR template that includes a changelog checkbox, pls pls pls 🙏 😂 |
fbf257d
to
ff9d269
Compare
return Hardware.OPTIGA | ||
except: | ||
# If the bootloader does not support the call, it must have the ATECC SC. | ||
pass |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
d977ff5
to
48561ef
Compare
rebased |
@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": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
">= 1.1.0".
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check this out:
bitbox02-firmware/py/bitbox02/bitbox02/communication/bitbox_api_protocol.py
Lines 557 to 562 in c016621
# 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.
There was a problem hiding this comment.
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 👍
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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": |
There was a problem hiding this comment.
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? 😁
There was a problem hiding this comment.
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? 🙈
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed 👍
21de44c
to
941c857
Compare
941c857
to
626ec5d
Compare
The endpoint provides firmware and signing pubkeys versions, but the function was returning the lenght of three versions struct, instead of two.
53efaf9
to
fc64fee
Compare
There was a problem hiding this 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. |
There was a problem hiding this comment.
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...).
There was a problem hiding this comment.
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 👍
fc64fee
to
6428c9e
Compare
There was a problem hiding this 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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
6428c9e
to
b16fa93
Compare
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.
b16fa93
to
699fba6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM thanks!
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