From df82692fa6a839574ea6cb7b19fef1e96970bb65 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Wed, 31 Jul 2024 16:55:07 +0200 Subject: [PATCH] Vendor SPSDK dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit is extracted from: https://github.com/Nitrokey/pynitrokey/pull/519 I’ve further reduced the included code so that we can get rid of even more dependencies: We only need crcmod, cryptography and libusbsio. We now also pass strict mypy checks on all imported modules (except for the libusbsio imports). Co-authored-by: Sosthène Guédon --- .flake8 | 3 +- CHANGELOG.md | 4 + poetry.lock | 996 +-------- pyproject.toml | 11 +- src/nitrokey/nk3/updates.py | 5 +- src/nitrokey/trussed/_bootloader/lpc55.py | 18 +- .../_bootloader/lpc55_upload/README.md | 6 + .../_bootloader/lpc55_upload/__init__.py | 13 + .../lpc55_upload/crypto/__init__.py | 7 + .../lpc55_upload/crypto/certificate.py | 397 ++++ .../lpc55_upload/crypto/exceptions.py | 18 + .../_bootloader/lpc55_upload/crypto/hash.py | 107 + .../_bootloader/lpc55_upload/crypto/hmac.py | 53 + .../_bootloader/lpc55_upload/crypto/keys.py | 1225 ++++++++++++ .../_bootloader/lpc55_upload/crypto/rng.py | 22 + .../lpc55_upload/crypto/symmetric.py | 300 +++ .../_bootloader/lpc55_upload/crypto/types.py | 72 + .../_bootloader/lpc55_upload/crypto/utils.py | 81 + .../_bootloader/lpc55_upload/exceptions.py | 94 + .../lpc55_upload/mboot/__init__.py | 10 + .../lpc55_upload/mboot/commands.py | 525 +++++ .../lpc55_upload/mboot/error_codes.py | 353 ++++ .../lpc55_upload/mboot/exceptions.py | 58 + .../lpc55_upload/mboot/interfaces/__init__.py | 8 + .../lpc55_upload/mboot/interfaces/usb.py | 118 ++ .../_bootloader/lpc55_upload/mboot/mcuboot.py | 1779 +++++++++++++++++ .../lpc55_upload/mboot/memories.py | 240 +++ .../lpc55_upload/mboot/properties.py | 868 ++++++++ .../lpc55_upload/mboot/protocol/__init__.py | 8 + .../lpc55_upload/mboot/protocol/base.py | 16 + .../mboot/protocol/bulk_protocol.py | 119 ++ .../_bootloader/lpc55_upload/sbfile/misc.py | 205 ++ .../lpc55_upload/sbfile/sb2/__init__.py | 8 + .../lpc55_upload/sbfile/sb2/commands.py | 1074 ++++++++++ .../lpc55_upload/sbfile/sb2/headers.py | 181 ++ .../lpc55_upload/sbfile/sb2/images.py | 370 ++++ .../lpc55_upload/sbfile/sb2/sections.py | 305 +++ .../lpc55_upload/utils/__init__.py | 8 + .../lpc55_upload/utils/abstract.py | 41 + .../lpc55_upload/utils/crypto/__init__.py | 8 + .../lpc55_upload/utils/crypto/cert_blocks.py | 850 ++++++++ .../lpc55_upload/utils/crypto/rkht.py | 292 +++ .../lpc55_upload/utils/exceptions.py | 13 + .../lpc55_upload/utils/interfaces/__init__.py | 8 + .../lpc55_upload/utils/interfaces/commands.py | 34 + .../utils/interfaces/device/__init__.py | 8 + .../utils/interfaces/device/base.py | 76 + .../utils/interfaces/device/usb_device.py | 217 ++ .../utils/interfaces/protocol/__init__.py | 8 + .../interfaces/protocol/protocol_base.py | 90 + .../_bootloader/lpc55_upload/utils/misc.py | 502 +++++ .../lpc55_upload/utils/spsdk_enum.py | 175 ++ .../lpc55_upload/utils/usbfilter.py | 294 +++ src/nitrokey/trussed/_utils.py | 6 - stubs/crcmod/__init__.pyi | 0 stubs/crcmod/predefined.pyi | 3 + 56 files changed, 11297 insertions(+), 1013 deletions(-) create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/README.md create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/certificate.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/exceptions.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/hash.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/hmac.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/keys.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/rng.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/symmetric.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/types.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/utils.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/exceptions.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/commands.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/error_codes.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/exceptions.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/interfaces/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/interfaces/usb.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/mcuboot.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/memories.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/properties.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/base.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/misc.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/commands.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/headers.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/images.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/sections.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/abstract.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/cert_blocks.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/rkht.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/exceptions.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/commands.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/base.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/usb_device.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/protocol/__init__.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/misc.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/spsdk_enum.py create mode 100644 src/nitrokey/trussed/_bootloader/lpc55_upload/utils/usbfilter.py create mode 100644 stubs/crcmod/__init__.pyi create mode 100644 stubs/crcmod/predefined.pyi diff --git a/.flake8 b/.flake8 index 562ca9f..0078796 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,8 @@ [flake8] # E203,E701 suggested by black, see: # https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#flake8 +# E221 for alignment in mboot code # E501 (line length) disabled as this is handled by black which takes better care of edge cases -extend-ignore = E203,E501,E701 +extend-ignore = E203,E221,E501,E701 max-complexity = 18 extend-exclude = src/nitrokey/trussed/_bootloader/nrf52_upload diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e8273..9c443f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - `trussed.admin_app`: Add error codes `CONFIG_ERROR` and `RNG_ERROR` to `InitStatus` enum +### Other Changes + +- Vendor `spsdk` dependency to reduce the total number of dependencies + ## [v0.1.0](https://github.com/Nitrokey/nitrokey-sdk-py/releases/tag/v0.1.0) (2024-07-29) Initial release with support for Nitrokey 3 and Nitrokey Passkey devices and the admin, provisioner and secrets app. diff --git a/poetry.lock b/poetry.lock index e1d766f..d1d9f68 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,199 +1,5 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = "*" -files = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] - -[[package]] -name = "argparse-addons" -version = "0.12.0" -description = "Additional argparse types and actions." -optional = false -python-versions = ">=3.6" -files = [ - {file = "argparse_addons-0.12.0-py3-none-any.whl", hash = "sha256:48b70ecd719054fcb0d7e6f25a1fecc13607aac61d446e83f47d211b4ead0d61"}, - {file = "argparse_addons-0.12.0.tar.gz", hash = "sha256:6322a0dcd706887e76308d23136d5b86da0eab75a282dc6496701d1210b460af"}, -] - -[[package]] -name = "asn1crypto" -version = "1.5.1" -description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" -optional = false -python-versions = "*" -files = [ - {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, - {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, -] - -[[package]] -name = "bincopy" -version = "17.14.5" -description = "Mangling of various file formats that conveys binary information (Motorola S-Record, Intel HEX and binary files)." -optional = false -python-versions = ">=3.6" -files = [ - {file = "bincopy-17.14.5-py3-none-any.whl", hash = "sha256:54d963954047d3bd23d46358dbb2bb7e49d749376850cb702a94791e0894d917"}, - {file = "bincopy-17.14.5.tar.gz", hash = "sha256:5f4de7c37a3db7adcf3edc4833a223f3356d9bf08be72ec6e431c9f0acd29f18"}, -] - -[package.dependencies] -argparse-addons = ">=0.4.0" -humanfriendly = "*" -pyelftools = "*" - -[[package]] -name = "bitarray" -version = "2.9.2" -description = "efficient arrays of booleans -- C extension" -optional = false -python-versions = "*" -files = [ - {file = "bitarray-2.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:917905de565d9576eb20f53c797c15ba88b9f4f19728acabec8d01eee1d3756a"}, - {file = "bitarray-2.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b35bfcb08b7693ab4bf9059111a6e9f14e07d57ac93cd967c420db58ab9b71e1"}, - {file = "bitarray-2.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea1923d2e7880f9e1959e035da661767b5a2e16a45dfd57d6aa831e8b65ee1bf"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0b63a565e8a311cc8348ff1262d5784df0f79d64031d546411afd5dd7ef67d"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf0620da2b81946d28c0b16f3e3704d38e9837d85ee4f0652816e2609aaa4fed"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:79a9b8b05f2876c7195a2b698c47528e86a73c61ea203394ff8e7a4434bda5c8"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:345c76b349ff145549652436235c5532e5bfe9db690db6f0a6ad301c62b9ef21"}, - {file = "bitarray-2.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e2936f090bf3f4d1771f44f9077ebccdbc0415d2b598d51a969afcb519df505"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f9346e98fc2abcef90b942973087e2462af6d3e3710e82938078d3493f7fef52"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e6ec283d4741befb86e8c3ea2e9ac1d17416c956d392107e45263e736954b1f7"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:962892646599529917ef26266091e4cb3077c88b93c3833a909d68dcc971c4e3"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e8da5355d7d75a52df5b84750989e34e39919ec7e59fafc4c104cc1607ab2d31"}, - {file = "bitarray-2.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:603e7d640e54ad764d2b4da6b61e126259af84f253a20f512dd10689566e5478"}, - {file = "bitarray-2.9.2-cp310-cp310-win32.whl", hash = "sha256:f00079f8e69d75c2a417de7961a77612bb77ef46c09bc74607d86de4740771ef"}, - {file = "bitarray-2.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:1bb33673e7f7190a65f0a940c1ef63266abdb391f4a3e544a47542d40a81f536"}, - {file = "bitarray-2.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fe71fd4b76380c2772f96f1e53a524da7063645d647a4fcd3b651bdd80ca0f2e"}, - {file = "bitarray-2.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d527172919cdea1e13994a66d9708a80c3d33dedcf2f0548e4925e600fef3a3a"}, - {file = "bitarray-2.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:052c5073bdcaa9dd10628d99d37a2f33ec09364b86dd1f6281e2d9f8d3db3060"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e064caa55a6ed493aca1eda06f8b3f689778bc780a75e6ad7724642ba5dc62f7"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:508069a04f658210fdeee85a7a0ca84db4bcc110cbb1d21f692caa13210f24a7"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4da73ebd537d75fa7bccfc2228fcaedea0803f21dd9d0bf0d3b67fef3c4af294"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cb378eaa65cd43098f11ff5d27e48ee3b956d2c00d2d6b5bfc2a09fe183be47"}, - {file = "bitarray-2.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d14c790b91f6cbcd9b718f88ed737c78939980c69ac8c7f03dd7e60040c12951"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7eea9318293bc0ea6447e9ebfba600a62f3428bea7e9c6d42170ae4f481dbab3"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b76ffec27c7450b8a334f967366a9ebadaea66ee43f5b530c12861b1a991f503"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:76b76a07d4ee611405045c6950a1e24c4362b6b44808d4ad6eea75e0dbc59af4"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c7d16beeaaab15b075990cd26963d6b5b22e8c5becd131781514a00b8bdd04bd"}, - {file = "bitarray-2.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60df43e868a615c7e15117a1e1c2e5e11f48f6457280eba6ddf8fbefbec7da99"}, - {file = "bitarray-2.9.2-cp311-cp311-win32.whl", hash = "sha256:e788608ed7767b7b3bbde6d49058bccdf94df0de9ca75d13aa99020cc7e68095"}, - {file = "bitarray-2.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:a23397da092ef0a8cfe729571da64c2fc30ac18243caa82ac7c4f965087506ff"}, - {file = "bitarray-2.9.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:90e3a281ffe3897991091b7c46fca38c2675bfd4399ffe79dfeded6c52715436"}, - {file = "bitarray-2.9.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:bed637b674db5e6c8a97a4a321e3e4d73e72d50b5c6b29950008a93069cc64cd"}, - {file = "bitarray-2.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e49066d251dbbe4e6e3a5c3937d85b589e40e2669ad0eef41a00f82ec17d844b"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4344e96642e2211fb3a50558feff682c31563a4c64529a931769d40832ca79"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aeb60962ec4813c539a59fbd4f383509c7222b62c3fb1faa76b54943a613e33a"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed0f7982f10581bb16553719e5e8f933e003f5b22f7d25a68bdb30fac630a6ff"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c71d1cabdeee0cdda4669168618f0e46b7dace207b29da7b63aaa1adc2b54081"}, - {file = "bitarray-2.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0ef2d0a6f1502d38d911d25609b44c6cc27bee0a4363dd295df78b075041b60"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6f71d92f533770fb027388b35b6e11988ab89242b883f48a6fe7202d238c61f8"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba0734aa300757c924f3faf8148e1b8c247176a0ac8e16aefdf9c1eb19e868f7"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:d91406f413ccbf4af6ab5ae7bc78f772a95609f9ddd14123db36ef8c37116d95"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:87abb7f80c0a042f3fe8e5264da1a2756267450bb602110d5327b8eaff7682e7"}, - {file = "bitarray-2.9.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b558ce85579b51a2e38703877d1e93b7728a7af664dd45a34e833534f0b755d"}, - {file = "bitarray-2.9.2-cp312-cp312-win32.whl", hash = "sha256:dac2399ee2889fbdd3472bfc2ede74c34cceb1ccf29a339964281a16eb1d3188"}, - {file = "bitarray-2.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:48a30d718d1a6dfc22a49547450107abe8f4afdf2abdcbe76eb9ed88edc49498"}, - {file = "bitarray-2.9.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2c6be1b651fad8f3adb7a5aa12c65b612cd9b89530969af941844ae680f7d981"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5b399ae6ab975257ec359f03b48fc00b1c1cd109471e41903548469b8feae5c"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b3543c8a1cb286ad105f11c25d8d0f712f41c5c55f90be39f0e5a1376c7d0b0"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:03adaacb79e2fb8f483ab3a67665eec53bb3fd0cd5dbd7358741aef124688db3"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae5b0657380d2581e13e46864d147a52c1e2bbac9f59b59c576e42fa7d10cf0"}, - {file = "bitarray-2.9.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c1f4bf6ea8eb9d7f30808c2e9894237a96650adfecbf5f3643862dc5982f89e"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a8873089be2aa15494c0f81af1209f6e1237d762c5065bc4766c1b84321e1b50"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:677e67f50e2559efc677a4366707070933ad5418b8347a603a49a070890b19bc"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:a620d8ce4ea2f1c73c6b6b1399e14cb68c6915e2be3fad5808c2998ed55b4acf"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:64115ccabbdbe279c24c367b629c6b1d3da9ed36c7420129e27c338a3971bfee"}, - {file = "bitarray-2.9.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:5d6fb422772e75385b76ad1c52f45a68bd4efafd8be8d0061c11877be74c4d43"}, - {file = "bitarray-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:852e202875dd6dfd6139ce7ec4e98dac2b17d8d25934dc99900831e81c3adaef"}, - {file = "bitarray-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7dfefdcb0dc6a3ba9936063cec65a74595571b375beabe18742b3d91d087eefd"}, - {file = "bitarray-2.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b306c4cf66912511422060f7f5e1149c8bdb404f8e00e600561b0749fdd45659"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a09c4f81635408e3387348f415521d4b94198c562c23330f560596a6aaa26eaf"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5361413fd2ecfdf44dc8f065177dc6aba97fa80a91b815586cb388763acf7f8d"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8a9475d415ef1eaae7942df6f780fa4dcd48fce32825eda591a17abba869299"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b87baa7bfff9a5878fcc1bffe49ecde6e647a72a64b39a69cd8a2992a43a34"}, - {file = "bitarray-2.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb6b86cfdfc503e92cb71c68766a24565359136961642504a7cc9faf936d9c88"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cd56b8ae87ebc71bcacbd73615098e8a8de952ecbb5785b6b4e2b07da8a06e1f"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3fa909cfd675004aed8b4cc9df352415933656e0155a6209d878b7cb615c787e"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b069ca9bf728e0c5c5b60e00a89df9af34cc170c695c3bfa3b372d8f40288efb"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6067f2f07a7121749858c7daa93c8774325c91590b3e81a299621e347740c2ae"}, - {file = "bitarray-2.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:321841cdad1dd0f58fe62e80e9c9c7531f8ebf8be93f047401e930dc47425b1e"}, - {file = "bitarray-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:54e16e32e60973bb83c315de9975bc1bcfc9bd50bb13001c31da159bc49b0ca1"}, - {file = "bitarray-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f4dcadb7b8034aa3491ee8f5a69b3d9ba9d7d1e55c3cc1fc45be313e708277f8"}, - {file = "bitarray-2.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c8919fdbd3bb596b104388b56ae4b266eb28da1f2f7dff2e1f9334a21840fe96"}, - {file = "bitarray-2.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eb7a9d8a2e400a1026de341ad48e21670a6261a75b06df162c5c39b0d0e7c8f4"}, - {file = "bitarray-2.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6ec84668dd7b937874a2b2c293cd14ba84f37be0d196dead852e0ada9815d807"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2de9a31c34e543ae089fd2a5ced01292f725190e379921384f695e2d7184bd3"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9521f49ae121a17c0a41e5112249e6fa7f6a571245b1118de81fb86e7c1bc1ce"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6cc6545d6d76542aee3d18c1c9485fb7b9812b8df4ebe52c4535ec42081b48f"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:856bbe1616425f71c0df5ef2e8755e878d9504d5a531acba58ab4273c52c117a"}, - {file = "bitarray-2.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4bba8042ea6ab331ade91bc435d81ad72fddb098e49108610b0ce7780c14e68"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a035da89c959d98afc813e3c62f052690d67cfd55a36592f25d734b70de7d4b0"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d70b1579da7fb71be5a841a1f965d19aca0ef27f629cfc07d06b09aafd0a333"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:405b83bed28efaae6d86b6ab287c75712ead0adbfab2a1075a1b7ab47dad4d62"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7eb8be687c50da0b397d5e0ab7ca200b5ebb639e79a9f5e285851d1944c94be9"}, - {file = "bitarray-2.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eceb551dfeaf19c609003a69a0cf8264b0efd7abc3791a11dfabf4788daf0d19"}, - {file = "bitarray-2.9.2-cp38-cp38-win32.whl", hash = "sha256:bb198c6ed1edbcdaf3d1fa3c9c9d1cdb7e179a5134ef5ee660b53cdec43b34e7"}, - {file = "bitarray-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:648d2f2685590b0103c67a937c2fb9e09bcc8dfb166f0c7c77bd341902a6f5b3"}, - {file = "bitarray-2.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ea816dc8f8e65841a8bbdd30e921edffeeb6f76efe6a1eb0da147b60d539d1cf"}, - {file = "bitarray-2.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4d0e32530f941c41eddfc77600ec89b65184cb909c549336463a738fab3ed285"}, - {file = "bitarray-2.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4a22266fb416a3b6c258bf7f83c9fe531ba0b755a56986a81ad69dc0f3bcc070"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc6d3e80dd8239850f2604833ff3168b28909c8a9357abfed95632cccd17e3e7"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f135e804986b12bf14f2cd1eb86674c47dea86c4c5f0fa13c88978876b97ebe6"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87580c7f7d14f7ec401eda7adac1e2a25e95153e9c339872c8ae61b3208819a1"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64b433e26993127732ac7b66a7821b2537c3044355798de7c5fcb0af34b8296f"}, - {file = "bitarray-2.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e497c535f2a9b68c69d36631bf2dba243e05eb343b00b9c7bbdc8c601c6802d"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e40b3cb9fa1edb4e0175d7c06345c49c7925fe93e39ef55ecb0bc40c906b0c09"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f2f8692f95c9e377eb19ca519d30d1f884b02feb7e115f798de47570a359e43f"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f0b84fc50b6dbeced4fa390688c07c10a73222810fb0e08392bd1a1b8259de36"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d656ad38c942e38a470ddbce26b5020e08e1a7ea86b8fd413bb9024b5189993a"}, - {file = "bitarray-2.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ab0f1dbfe5070db98771a56aa14797595acd45a1af9eadfb193851a270e7996"}, - {file = "bitarray-2.9.2-cp39-cp39-win32.whl", hash = "sha256:0a99b23ac845a9ea3157782c97465e6ae026fe0c7c4c1ed1d88f759fd6ea52d9"}, - {file = "bitarray-2.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:9bbcfc7c279e8d74b076e514e669b683f77b4a2a328585b3f16d4c5259c91222"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:43847799461d8ba71deb4d97b47250c2c2fb66d82cd3cb8b4caf52bb97c03034"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f44381b0a4bdf64416082f4f0e7140377ae962c0ced6f983c6d7bbfc034040"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a484061616fb4b158b80789bd3cb511f399d2116525a8b29b6334c68abc2310f"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ff9e38356cc803e06134cf8ae9758e836ccd1b793135ef3db53c7c5d71e93bc"}, - {file = "bitarray-2.9.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b44105792fbdcfbda3e26ee88786790fda409da4c71f6c2b73888108cf8f062f"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7e913098de169c7fc890638ce5e171387363eb812579e637c44261460ac00aa2"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6fe315355cdfe3ed22ef355b8bdc81a805ca4d0949d921576560e5b227a1112"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f708e91fdbe443f3bec2df394ed42328fb9b0446dff5cb4199023ac6499e09fd"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b7b09489b71f9f1f64c0fa0977e250ec24500767dab7383ba9912495849cadf"}, - {file = "bitarray-2.9.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:128cc3488176145b9b137fdcf54c1c201809bbb8dd30b260ee40afe915843b43"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:21f21e7f56206be346bdbda2a6bdb2165a5e6a11821f88fd4911c5a6bbbdc7e2"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f4dd3af86dd8a617eb6464622fb64ca86e61ce99b59b5c35d8cd33f9c30603d"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6465de861aff7a2559f226b37982007417eab8c3557543879987f58b453519bd"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbaf2bb71d6027152d603f1d5f31e0dfd5e50173d06f877bec484e5396d4594b"}, - {file = "bitarray-2.9.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2f32948c86e0d230a296686db28191b67ed229756f84728847daa0c7ab7406e3"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be94e5a685e60f9d24532af8fe5c268002e9016fa80272a94727f435de3d1003"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5cc9381fd54f3c23ae1039f977bfd6d041a5c3c1518104f616643c3a5a73b15"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd926e8ae4d1ed1ac4a8f37212a62886292f692bc1739fde98013bf210c2d175"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:461a3dafb9d5fda0bb3385dc507d78b1984b49da3fe4c6d56c869a54373b7008"}, - {file = "bitarray-2.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:393cb27fd859af5fd9c16eb26b1c59b17b390ff66b3ae5d0dd258270191baf13"}, - {file = "bitarray-2.9.2.tar.gz", hash = "sha256:a8f286a51a32323715d77755ed959f94bef13972e9a2fe71b609e40e6d27957e"}, -] - -[[package]] -name = "bitstring" -version = "4.1.4" -description = "Simple construction, analysis and modification of binary data." -optional = false -python-versions = ">=3.7" -files = [ - {file = "bitstring-4.1.4-py3-none-any.whl", hash = "sha256:da46c4d6f8f3fb75a85566fdd33d5083ba8b8f268ed76f34eefe5a00da426192"}, - {file = "bitstring-4.1.4.tar.gz", hash = "sha256:94f3f1c45383ebe8fd4a359424ffeb75c2f290760ae8fcac421b44f89ac85213"}, -] - -[package.dependencies] -bitarray = ">=2.8.0,<3.0.0" - [[package]] name = "black" version = "24.4.2" @@ -240,20 +46,6 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] -[[package]] -name = "capstone" -version = "4.0.2" -description = "Capstone disassembly engine" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "capstone-4.0.2-py2.py3-none-manylinux1_i686.whl", hash = "sha256:da442f979414cf27e4621e70e835880878c858ea438c4f0e957e132593579e37"}, - {file = "capstone-4.0.2-py2.py3-none-manylinux1_x86_64.whl", hash = "sha256:9d1a9096c5f875b11290317722ed44bb6e7c52e50cc79d791f142bce968c49aa"}, - {file = "capstone-4.0.2-py2.py3-none-win32.whl", hash = "sha256:c3d9b443d1adb40ee2d9a4e7341169b76476ddcf3a54c03793b16cdc7cd35c5a"}, - {file = "capstone-4.0.2-py2.py3-none-win_amd64.whl", hash = "sha256:0d65ffe8620920976ceadedc769f22318f6f150a592368d8a735612367ac8a1a"}, - {file = "capstone-4.0.2.tar.gz", hash = "sha256:2842913092c9b69fd903744bc1b87488e1451625460baac173056e1808ec1c66"}, -] - [[package]] name = "certifi" version = "2024.7.4" @@ -442,66 +234,6 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -[[package]] -name = "click-command-tree" -version = "1.1.1" -description = "click plugin to show the command tree of your CLI" -optional = false -python-versions = "*" -files = [ - {file = "click-command-tree-1.1.1.tar.gz", hash = "sha256:f1ba9c386174dff2fd29949b02a7e1ff7f0a85fca11e737d219eaa60014699a0"}, - {file = "click_command_tree-1.1.1-py3-none-any.whl", hash = "sha256:d3739a9a344c6d033d47b02842aa31ca7e45fc9e641f9cb6afd1d065802ebd3e"}, -] - -[package.dependencies] -click = "*" - -[[package]] -name = "click-option-group" -version = "0.5.6" -description = "Option groups missing in Click" -optional = false -python-versions = ">=3.6,<4" -files = [ - {file = "click-option-group-0.5.6.tar.gz", hash = "sha256:97d06703873518cc5038509443742b25069a3c7562d1ea72ff08bfadde1ce777"}, - {file = "click_option_group-0.5.6-py3-none-any.whl", hash = "sha256:38a26d963ee3ad93332ddf782f9259c5bdfe405e73408d943ef5e7d0c3767ec7"}, -] - -[package.dependencies] -Click = ">=7.0,<9" - -[package.extras] -docs = ["Pallets-Sphinx-Themes", "m2r2", "sphinx"] -tests = ["pytest"] -tests-cov = ["coverage", "coveralls", "pytest", "pytest-cov"] - -[[package]] -name = "cmsis-pack-manager" -version = "0.5.3" -description = "Python manager for CMSIS-Pack index and cache with fast Rust backend" -optional = false -python-versions = ">=3.6" -files = [ - {file = "cmsis_pack_manager-0.5.3-py3-none-linux_armv6l.whl", hash = "sha256:ba66bd40811071c1e4718436f4e46a163cdc08910c8930724c218b00d82db525"}, - {file = "cmsis_pack_manager-0.5.3-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:8359edcdd072f4f89ec849c06b6b975d775d1183424d19f0e10861ff60b37552"}, - {file = "cmsis_pack_manager-0.5.3-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:44459480aefe6d5503516f0f1685731845b4061d7f015a65881e755742ec8b12"}, - {file = "cmsis_pack_manager-0.5.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5915abda4e8e8c5badb527f37a904d25b31dc7b37931f5ab94bd57ebb97b0605"}, - {file = "cmsis_pack_manager-0.5.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2cad4e635a39dbf3adf4eb4a68187755845aa312bf1e8806c4b8f224cce54f"}, - {file = "cmsis_pack_manager-0.5.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11e24a6cebfb9bc013cf8b4fc330daa282af30e266603eae610f141a5a9dad42"}, - {file = "cmsis_pack_manager-0.5.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e27d6e7289ea8cc50752b33a9152c5f0486db0c2d6ccb4b8dad6b7fec2c3f81"}, - {file = "cmsis_pack_manager-0.5.3-py3-none-win32.whl", hash = "sha256:2987f3c33d11bc3bdc5817e685a08e0f9c5253439a97ce9ff6848e5be57d034b"}, - {file = "cmsis_pack_manager-0.5.3-py3-none-win_amd64.whl", hash = "sha256:169056c30b395626888a1701065312cb4da6321bcb6dcdc07b7c499f7ca3aa2f"}, - {file = "cmsis_pack_manager-0.5.3.tar.gz", hash = "sha256:980d9b92d23023066b8e2563e15b5cc0a40b263b10260ceb26b1e2132ba1fd28"}, -] - -[package.dependencies] -appdirs = ">=1.4,<2.0" -cffi = "*" -pyyaml = ">=6.0,<7.0" - -[package.extras] -test = ["hypothesis", "jinja2", "pytest (>=6.0)"] - [[package]] name = "colorama" version = "0.4.6" @@ -577,17 +309,6 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] -[[package]] -name = "deepmerge" -version = "1.1.1" -description = "a toolset to deeply merge python dictionaries." -optional = false -python-versions = "*" -files = [ - {file = "deepmerge-1.1.1-py3-none-any.whl", hash = "sha256:7219dad9763f15be9dcd4bcb53e00f48e4eed6f5ed8f15824223eb934bb35977"}, - {file = "deepmerge-1.1.1.tar.gz", hash = "sha256:53a489dc9449636e480a784359ae2aab3191748c920649551c8e378622f0eca4"}, -] - [[package]] name = "ecdsa" version = "0.19.0" @@ -606,20 +327,6 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] -[[package]] -name = "fastjsonschema" -version = "2.18.1" -description = "Fastest Python implementation of JSON schema" -optional = false -python-versions = "*" -files = [ - {file = "fastjsonschema-2.18.1-py3-none-any.whl", hash = "sha256:aec6a19e9f66e9810ab371cc913ad5f4e9e479b63a7072a2cd060a9369e329a8"}, - {file = "fastjsonschema-2.18.1.tar.gz", hash = "sha256:06dc8680d937628e993fa0cd278f196d20449a1adc087640710846b324d422ea"}, -] - -[package.extras] -devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benchmark", "pytest-cache", "validictory"] - [[package]] name = "fido2" version = "1.1.3" @@ -653,111 +360,6 @@ mccabe = ">=0.7.0,<0.8.0" pycodestyle = ">=2.12.0,<2.13.0" pyflakes = ">=3.2.0,<3.3.0" -[[package]] -name = "hexdump" -version = "3.3" -description = "dump binary data to hex format and restore from there" -optional = false -python-versions = "*" -files = [ - {file = "hexdump-3.3.zip", hash = "sha256:d781a43b0c16ace3f9366aade73e8ad3a7bd5137d58f0b45ab2d3f54876f20db"}, -] - -[[package]] -name = "hidapi" -version = "0.14.0" -description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi" -optional = false -python-versions = "*" -files = [ - {file = "hidapi-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f68bbf88805553911e7e5a9b91136c96a54042b6e3d82d39d733d2edb46ff9a6"}, - {file = "hidapi-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b264c6a1a1a0cacacc82299785415bec91184cb3e4a77d127c40016086705327"}, - {file = "hidapi-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01929fbbe206ebcb0bad9b8e925e16de0aa8f872bf80a263f599e519866d9900"}, - {file = "hidapi-0.14.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b4052f17321f5f0b641e020eae87db5bb0103f893198e61b2495358db83ddab"}, - {file = "hidapi-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:349976417f7f3371c7133a6427ed8f4faa06fbd93e9b5309d86689f25f191150"}, - {file = "hidapi-0.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7ff737cbb4adf238aa0da50e8b5c2f083e8f62b3c5132fbd732ba59918a909c"}, - {file = "hidapi-0.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b960bcf8c41bd861554adc5932d1d7e0ed169315ca87dbd4d23ec8337764247"}, - {file = "hidapi-0.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3b8af9ef71b7149e85f2118eaac9fd7e7ea95528029a66f351d0049877d5a179"}, - {file = "hidapi-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7ef0f40a02e0b56fe2e7c93dfc9810245f2feeaa0c2ea76654d0768722883639"}, - {file = "hidapi-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fdc08eb19f2fffb989124d1dbea3aa62dd0036615bbf464ceafee0353673bf4"}, - {file = "hidapi-0.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4046bbfc67c5587ca638b875858569a8787e6955eff5dea4e424044de09fe7e4"}, - {file = "hidapi-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:15f1fd34b0719d1e4d1bbc0bce325b318ee3e85c36fac0d23c6fb9d7f4d611db"}, - {file = "hidapi-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c0959d89bc95acb4f9e6d58c8562281e22694959e42c10108193a1362b4fcd9"}, - {file = "hidapi-0.14.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1b1ded4a823cc5c2075a622b48d02bc0a72f57579ea24c956ef29649a49eb66"}, - {file = "hidapi-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2906ad143ec40009c33348ab4b3f7a9bdaa87b65bdc55983399bed47ee90a818"}, - {file = "hidapi-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1927fc5f7099b98529a4cefe8e0cd92ffb026abf5c449310d1d359433c5d94a"}, - {file = "hidapi-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:76041e2e5d52c864bc4a381f082edeb89e85829130d1fef3366f320237da0580"}, - {file = "hidapi-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:93d7814aa1c7e0f1cce300b3b63828abecb024da72e9a10d46db811cf466e68e"}, - {file = "hidapi-0.14.0-cp311-cp311-win32.whl", hash = "sha256:651c2382e974e866d78334cdde3c290a04fcbab4cec940c0d3586d77d11b9566"}, - {file = "hidapi-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:de293e7291b1ec813a97e42625c2c0a41b0d25d495b3dc5864bbb3dbbb5a719d"}, - {file = "hidapi-0.14.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fc9ec2321bf3b0b4953910aa87c0c8ab5f93b1f113a9d3d4f18845ce54708d13"}, - {file = "hidapi-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a68820a5de54a54d145d88f31c74257965bd03ae454263eda054f02bf34dcc9c"}, - {file = "hidapi-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86752ca0db00e5a5e991ebc5854400ff16d3812d6d9a156fea4de7d5f10ba801"}, - {file = "hidapi-0.14.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b65cc159fcf1839d078d3de196146626c1a865bd9136fda5fa490f689e904c9"}, - {file = "hidapi-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ab1b1dc8b915a0faa7b976ed8291142cf93c2acecf533db8c748fc64be1a004"}, - {file = "hidapi-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:418de0a2ec786d610967984fe5d6cb9584413dcce8b9fdd23fff998596f80a95"}, - {file = "hidapi-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1a777912e93a9f773aa6359fdb7b152b654991bb9afd6d3ce20e52dfbf18db00"}, - {file = "hidapi-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:33098ba2f10f704a85b62720becf444a19753d3a1ee4b8dda7dc289c1d6eda9b"}, - {file = "hidapi-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5e3318f0e66c4d46977fc8ba73a2ad33c2de367d133b70b243051283d0ecdaca"}, - {file = "hidapi-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b4f3a4e41886a19dcb9ea872a6f75ef42baba124a150b5b0a03379da174e1f70"}, - {file = "hidapi-0.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1370bc6a364fd292accd580a8d7bac4219932144d149f3a513bb472581eac421"}, - {file = "hidapi-0.14.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6ef0bdc69310cfdff83faf96c75492ac3d8cf355af275904f1dd90a3c5f24a4"}, - {file = "hidapi-0.14.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e245719a5ede83c779dd99a4553002ae684d92d0f3e4274dcf06882b063f127"}, - {file = "hidapi-0.14.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:833a32c3e44780f37d46dffd559b8e245034c92ae25060f752e4f34e9c7efe24"}, - {file = "hidapi-0.14.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:103dfa1c19832b8928775ec491c3016c9f9063dd2ccdc37811bf12f3cc0a789f"}, - {file = "hidapi-0.14.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:78b176bc64a8908b37d5f34b3cce30158c1ebeaf1208c3b5ed62ad456fa1277d"}, - {file = "hidapi-0.14.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:1a63c0bc33329d0e572afe20b9dff27155d4fff34d0f2fa662e6704b9e2e18c4"}, - {file = "hidapi-0.14.0-cp36-cp36m-win32.whl", hash = "sha256:365d7c9fdcae71ae41797dc2dd062dfed4362d1b36d21fa62afbc16c5ec3cd5a"}, - {file = "hidapi-0.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:810ad22831e4a150c2d6f27141fcf2826fd085ccacf4262d5c742c90aa81cd54"}, - {file = "hidapi-0.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3ed9f993a6f8a611c11ef213968c6972e17d7e8b27936349884c475dc0309e71"}, - {file = "hidapi-0.14.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fb47a0a8c3a6797306ea9eb8d1bdad68e5493ef5c8fa2e644501d56f2677551"}, - {file = "hidapi-0.14.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4513311fad7e499ebb0d7a26178557b85044983199a280cb95c2038902fe1a0"}, - {file = "hidapi-0.14.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dff930adb37d1bcaeca3cf0dcec00eb72c109aa42c84858809cbae2972d79661"}, - {file = "hidapi-0.14.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cc654cd37d04bbb782c39901bf872b2af5d3c3ead2b1a23b084a81e469b6d0a7"}, - {file = "hidapi-0.14.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:de5af3941f31cfb044a87fc9d9b2f80b3b71b58b27481d9877061b76e9625a22"}, - {file = "hidapi-0.14.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e5f01e21648a58de56c24a093e4901fca039b9658074b413c2a4ceb16ea6473b"}, - {file = "hidapi-0.14.0-cp37-cp37m-win32.whl", hash = "sha256:60c034ec3ef3e5679232d9e6c003c4848e4772032d683f0b91ddb84b87d8698d"}, - {file = "hidapi-0.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8bba64d6ed49fa7ea4f4515986450223f5c744be448c846fb0614bc53b536bd"}, - {file = "hidapi-0.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:48e2cf77626f3cfdda9624de3be7f9c55e37efbb39882d2e96a92d38893a09cb"}, - {file = "hidapi-0.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a6edc57962a9f30bff73fc0cc80915c9da9ab3e0892c601263198f8d21d8dfff"}, - {file = "hidapi-0.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e822e899c13eb1e3a575712d7be5bd03a9103f6027b00ab4351c8404cec5719d"}, - {file = "hidapi-0.14.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb87cf8f23c15346bc1487e6f39d11b37d3ff7788037d3760b7907ea325b6d2c"}, - {file = "hidapi-0.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93697007df8ba38ab3ae3e777a6875cd1775fc720afe27e4c624cecbab7720de"}, - {file = "hidapi-0.14.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:832a2d2d8509d98381f0bf09b4e1f897765a9c8e0a72164174bcbf983d7d69a3"}, - {file = "hidapi-0.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6c000635c14f11ee3633530ef2d56de1ef266dc89b98f0a5f21e08ab8a9b151b"}, - {file = "hidapi-0.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:174be08584e5c686fb02a6f51cc159d6e491fd7a7c7d1978b28f913362c7ad11"}, - {file = "hidapi-0.14.0-cp38-cp38-win32.whl", hash = "sha256:b054abf40b5aa7122314af59d0244fa274a50c4276d20695d8b7ff69564beb95"}, - {file = "hidapi-0.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:f575381efa788e1a894c68439644817b152b8a68ead643e42c23ba28eeedc33b"}, - {file = "hidapi-0.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5299d74d96bdc9eaa83496c972048db0027d012a08440b33bdb6dd10a7491da9"}, - {file = "hidapi-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c78ff5c46128bdf68b2c4e4b08fac7765ef79f6ee7e17c8a2f7d3090a591d97"}, - {file = "hidapi-0.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e635c037d28e1ceded2043d81b879d81348a278d1ae668954a5a7a7d383f7d7"}, - {file = "hidapi-0.14.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1425f523258d25d8f32a6493978532477c4d7507f5f9252417b1d629427871e"}, - {file = "hidapi-0.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ecea60915212e59940db41c2a91709ebd4ec6a04e03b0db37a4ddb6825bee6"}, - {file = "hidapi-0.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:537fc17d59e1de48c1832d5bda60d63f56bcb1300cce7e382d45b8ef3bcacd53"}, - {file = "hidapi-0.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e43f2db347b7faf3fcefb6c39f45615d1d6f58db7305d4474bb63b2845ed4fc8"}, - {file = "hidapi-0.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fb4e94e45f6dddb20d59501187721e5d3b02e6cc8a59d261dd5cac739008582a"}, - {file = "hidapi-0.14.0-cp39-cp39-win32.whl", hash = "sha256:b4a0feac62d80eca36e2c8035fe4f57c440fbfcd9273a909112cb5bd9baae449"}, - {file = "hidapi-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:ed112c9ba0adf41d7e04bf5389dc150ada4d94a6ef1cb56c325d5aed1e4e07d2"}, - {file = "hidapi-0.14.0.tar.gz", hash = "sha256:a7cb029286ced5426a381286526d9501846409701a29c2538615c3d1a612b8be"}, -] - -[package.dependencies] -setuptools = ">=19.0" - -[[package]] -name = "humanfriendly" -version = "10.0" -description = "Human friendly output for text interfaces using Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477"}, - {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, -] - -[package.dependencies] -pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_version >= \"3.8\""} - [[package]] name = "idna" version = "3.7" @@ -769,43 +371,6 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] -[[package]] -name = "importlib-metadata" -version = "8.0.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, - {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] - -[[package]] -name = "importlib-resources" -version = "6.4.0" -description = "Read resources from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, - {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, -] - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] - [[package]] name = "intelhex" version = "2.3.0" @@ -817,19 +382,6 @@ files = [ {file = "intelhex-2.3.0.tar.gz", hash = "sha256:892b7361a719f4945237da8ccf754e9513db32f5628852785aea108dcd250093"}, ] -[[package]] -name = "intervaltree" -version = "3.1.0" -description = "Editable interval tree data structure for Python 2 and 3" -optional = false -python-versions = "*" -files = [ - {file = "intervaltree-3.1.0.tar.gz", hash = "sha256:902b1b88936918f9b2a19e0e5eb7ccb430ae45cde4f39ea4b36932920d33952d"}, -] - -[package.dependencies] -sortedcontainers = ">=2.0,<3.0" - [[package]] name = "isort" version = "5.13.2" @@ -844,99 +396,6 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] -[[package]] -name = "lark" -version = "1.1.9" -description = "a modern parsing library" -optional = false -python-versions = ">=3.6" -files = [ - {file = "lark-1.1.9-py3-none-any.whl", hash = "sha256:a0dd3a87289f8ccbb325901e4222e723e7d745dbfc1803eaf5f3d2ace19cf2db"}, - {file = "lark-1.1.9.tar.gz", hash = "sha256:15fa5236490824c2c4aba0e22d2d6d823575dcaf4cdd1848e34b6ad836240fba"}, -] - -[package.extras] -atomic-cache = ["atomicwrites"] -interegular = ["interegular (>=0.3.1,<0.4.0)"] -nearley = ["js2py"] -regex = ["regex"] - -[[package]] -name = "libusb-package" -version = "1.0.26.2" -description = "Package containing libusb so it can be installed via Python package managers" -optional = false -python-versions = ">=3.7" -files = [ - {file = "libusb_package-1.0.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:75ab092f84ee6cf61d25cf8b4f491b2488d8596d035d8347663c5ef6d58219ab"}, - {file = "libusb_package-1.0.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:992481beecac68fb1978b62da83eae5e34c79e4ae16a9104826b4f9c5b121beb"}, - {file = "libusb_package-1.0.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c56fabcd61c27c731e3a2f682a51cf91608a908d83d93f69c2431412a01c70fb"}, - {file = "libusb_package-1.0.26.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a6baa282fae2733389f16a3356e74a627a5d30d9b96e0a42f8080aa8799a513"}, - {file = "libusb_package-1.0.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f2409346c53d861cfef4663c003a217334fcd02a38b5521722ec385758a53db"}, - {file = "libusb_package-1.0.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8b5b8fc1d09daad512999ac4539cb224ec6379e08883f6d3f581875a98a94f05"}, - {file = "libusb_package-1.0.26.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:942217aac7364e6055b8b10b5186dd852e7fc167849adc3de067943b7c461cee"}, - {file = "libusb_package-1.0.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e566aa04957ae61c0c2eb4ec70e7767520fe79c5b9cae2ebbf5eaddbff73ea58"}, - {file = "libusb_package-1.0.26.2-cp310-cp310-win32.whl", hash = "sha256:32e03d0bfbb295cce3a37a18bca70b1f81b139fd8ae835fc0b91b1270b379e5b"}, - {file = "libusb_package-1.0.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:b1e70b24d9a7aa10cc9ce52a7139037e22d4444b152d54770c7c9342cf5e8781"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4aacc4fe1bf4cd892137f56c9783c81b388c80a249e8afacb1374ac52543e8f2"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1f2f14051983c8869296a1e1659c68e1796a42b5723b2698a30d6aa0e295696a"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73e910401d9a9c127b039e16e8b6a1ba4ec8d21a405e6cf12d7ed5ba207058b0"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b300f0ad23a986e7085731bb6a622619c550e5df6649463492d4c6c44f8c9d5"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08fd35ee52119ae322b10288d9a411e82c9cd87e82ad1d13d63e024236b6d6ba"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16089823afced6e407eb39b4519e9c094d17be759a1b0bb1976d581e7b2382ae"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b8a0208b1bc9b2e2b7097d33a7e24f4268e189b05ee5267f57e7346802653b9f"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec40b8a81df4ec972f494db1e0848d38b2a2c117f001aae0e4c739feec50205f"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-win32.whl", hash = "sha256:f1bed2fe1ce230e37826dc25fc077ae892d9edfe9d312aea112f3cbeba2a8588"}, - {file = "libusb_package-1.0.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:982a03c4cd4a5509540f18b127f5b873d54019d5cdc1438f07529dc9080be633"}, - {file = "libusb_package-1.0.26.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a76378f89cbdf457573b94a889666306f0fac2ab55d0e0b2aa26c8dcec9ac587"}, - {file = "libusb_package-1.0.26.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad8c5ed9a82efcffaa5bb216f8d39a8a244235a98fa14d2e837ef8088a068745"}, - {file = "libusb_package-1.0.26.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d95f3ddc12380ebd024e2c9a1ee4e0a34555275ddcc6658962df1379fedfaef4"}, - {file = "libusb_package-1.0.26.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8803e41bc1c5c5c1c16bb873308fc9cb4613ee51688084e94976d887e3d588b0"}, - {file = "libusb_package-1.0.26.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b6d74c3ba47d914c874bef8aa0efc0af99e07b44b7f735cde9ff93573ca33ad2"}, - {file = "libusb_package-1.0.26.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:62df8d3f38482864c35347978184c095556796e3af4005f24091dd5bcbed5b0a"}, - {file = "libusb_package-1.0.26.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:95cfd34bc9ddc4e4d2dbcec1ecf15c68ea0243d75518b13b69450d672189f3d7"}, - {file = "libusb_package-1.0.26.2-cp37-cp37m-win32.whl", hash = "sha256:818115cb123281fa54dc80cfa1033da95a0b0b481adb967e4147d726468a42a5"}, - {file = "libusb_package-1.0.26.2-cp37-cp37m-win_amd64.whl", hash = "sha256:9a109ab311327ce036b9ee0d984935009e0dad45a8a8c8387f580fa11ccfe6fc"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b158e37423191bfa42d14366ed4dc4fea0f4b6fcc0ad3d180711b8e9609bcd02"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:092ea7d50734750197459b7c37e024142cabd090df80cbda9254d8d93bd51ee5"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cb4e98e44efc87a0bbdcd231101797518a688cc2528355b9200a0f4b18522e"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655fded7314065a60e088d2e49b014155a700a96b01af081abb914ce1fa2e96"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93a59c9fdb4d76f2c4bb93c4fbc3c7450c746082e9aa9ba71d2b8f87a958121e"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f132290e962af25fd89a64c0960d5f5d2b46aab647bc52a959b09742d3a3a3b0"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a4875c15a3a6c19c999c7f11ff533bd7abadbc1e581f1c4135b0225344fdd94"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50a1c23c05a966094e1cc3876109228dc966e1d31cec6ffb69a6b2cde5d4049f"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-win32.whl", hash = "sha256:40befc6b6b774cfc34d19b79ba04800494150f256709f0b8e76f5f05b1c1c9ac"}, - {file = "libusb_package-1.0.26.2-cp38-cp38-win_amd64.whl", hash = "sha256:1c68660a83d25a32fccc9aa2d300800a3b523508268f01edc322fb2f68cf04a7"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:57f186097f3086b0fbaf91f823a70df0993f098c5780db2bfd941184a6e5fa42"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:67069b698c79c53e704b67b10147b94ca8e7cfb55fb84010c3d38218bda51c03"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51961a3b2e5eb7c276052d4a5d347de1b6d128751eae576e7162f95f6d1e466d"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:21304366f16aa684f6b2fc49a8603e8e1eb3c61f2db60cdbdea71ef4d8ea166b"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613c5ff62c19c18b248de2159a6c4093e8adac8f9f5655e8f32e71a9cb36e5df"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d69bf72ebdba7f892e9cf45fe55f8308b4b683db887728da28c3721cd6a624fd"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:45e791e7083a7b4500cdaff0854ff3f74da31d0d375c6f86aab70ae629934829"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b4e7f01b359e8178d2112b2eefdccbfe2fd36a1e57dbcebf1678aef266e691a"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-win32.whl", hash = "sha256:051feb717910fc868da437d271bcaa2d0b59fce951f304821a3139ababb3d69f"}, - {file = "libusb_package-1.0.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:ad1d6b7e8e2323a222b4f968f16f27dbe91f48e4c8cc802bf27022e324b8b99b"}, - {file = "libusb_package-1.0.26.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bea164dbeff193a052a2d64896dd136c0bfccd8b3000a2d233d2d23cddf18436"}, - {file = "libusb_package-1.0.26.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e77e0b2a30b72c963adb99c520f78414ab91940d97acba745ff791eb3477dd"}, - {file = "libusb_package-1.0.26.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc14c8b37f9acebadbf7b1ab7c788826e67dfecf4a872c6a2698777716856333"}, - {file = "libusb_package-1.0.26.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fba4e8fc77865cbf7464f5a847129c282a8bd26868da160b2b90d61c67192d4b"}, - {file = "libusb_package-1.0.26.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:86ffd4480e578e86277f5d2c65aad30db21c02bb7b8a439c44585ca65bb7fe7c"}, - {file = "libusb_package-1.0.26.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:28e5867f8a290ca3a448954d1d34b875ba7d0e5f775e4c775f9eebdecc68dfb1"}, - {file = "libusb_package-1.0.26.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:229d40f83a1d8ae0cd9d111a829972224527573a98487d092496840b98a82af2"}, - {file = "libusb_package-1.0.26.2-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5186ed5c9bf04f50aec6b30d4795ff48f227137a2f6c92b2d6503a29b0b9761b"}, - {file = "libusb_package-1.0.26.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1e01a04d7115900b6fb864b822d04a06159bc1c57dbefd119fad09506f7d242"}, - {file = "libusb_package-1.0.26.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:960069cd1478996e7f00d9c98347bfb1804f14e309fa1f00cbd26136dc1074f8"}, - {file = "libusb_package-1.0.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0970e9e92bc13564fe75c3fc74e60601cb31866fc0345cb1f6461b9f983d6062"}, - {file = "libusb_package-1.0.26.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75ec775bf3950ca2f9287ae86637a6de8a9a8addf463bfb176026403ea78c64b"}, - {file = "libusb_package-1.0.26.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74d0b0bd2a284d3ba9b1b99c334171230d88658ef2d64e2f83a71dc28c65f2da"}, - {file = "libusb_package-1.0.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a96262d51f2714a250cf89e063f354a621d81438a7019bca5035e3ab4a65988"}, - {file = "libusb_package-1.0.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5fbd4ea17c36806c94e6c2fc4d6a6548c628c0ad874937b0953946d49f29f23b"}, -] - -[package.dependencies] -importlib-resources = "*" - [[package]] name = "libusbsio" version = "2.1.12" @@ -1017,35 +476,6 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] -[[package]] -name = "natsort" -version = "8.4.0" -description = "Simple yet flexible natural sorting in Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c"}, - {file = "natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581"}, -] - -[package.extras] -fast = ["fastnumbers (>=2.0.0)"] -icu = ["PyICU (>=1.0.0)"] - -[[package]] -name = "oscrypto" -version = "1.3.0" -description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." -optional = false -python-versions = "*" -files = [ - {file = "oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085"}, - {file = "oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"}, -] - -[package.dependencies] -asn1crypto = ">=1.5.1" - [[package]] name = "packaging" version = "24.0" @@ -1084,23 +514,6 @@ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx- test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] type = ["mypy (>=1.8)"] -[[package]] -name = "prettytable" -version = "3.10.2" -description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" -optional = false -python-versions = ">=3.8" -files = [ - {file = "prettytable-3.10.2-py3-none-any.whl", hash = "sha256:1cbfdeb4bcc73976a778a0fb33cb6d752e75396f16574dcb3e2d6332fd93c76a"}, - {file = "prettytable-3.10.2.tar.gz", hash = "sha256:29ec6c34260191d42cd4928c28d56adec360ac2b1208a26c7e4f14b90cc8bc84"}, -] - -[package.dependencies] -wcwidth = "*" - -[package.extras] -tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] - [[package]] name = "protobuf" version = "3.20.3" @@ -1132,35 +545,6 @@ files = [ {file = "protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2"}, ] -[[package]] -name = "psutil" -version = "6.0.0" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, - {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, - {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, - {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, -] - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - [[package]] name = "pycodestyle" version = "2.12.0" @@ -1183,17 +567,6 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -[[package]] -name = "pyelftools" -version = "0.31" -description = "Library for analyzing ELF files and DWARF debugging information" -optional = false -python-versions = "*" -files = [ - {file = "pyelftools-0.31-py3-none-any.whl", hash = "sha256:f52de7b3c7e8c64c8abc04a79a1cf37ac5fb0b8a49809827130b858944840607"}, - {file = "pyelftools-0.31.tar.gz", hash = "sha256:c774416b10310156879443b81187d182d8d9ee499660380e645918b50bc88f99"}, -] - [[package]] name = "pyflakes" version = "3.2.0" @@ -1205,92 +578,6 @@ files = [ {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] -[[package]] -name = "pylink-square" -version = "1.2.1" -description = "Python interface for SEGGER J-Link." -optional = false -python-versions = "*" -files = [ - {file = "pylink_square-1.2.1-py2.py3-none-any.whl", hash = "sha256:ceb8341812416ef8f17d08810486b1dac68a871ee8c1e2f94232d791c58db810"}, - {file = "pylink_square-1.2.1.tar.gz", hash = "sha256:4b84b5fd1c22546189324e0ac6c716eb67a90bf7c18dfe50450924a4055bc348"}, -] - -[package.dependencies] -psutil = ">=5.2.2" -six = "*" - -[[package]] -name = "pyocd" -version = "0.35.1" -description = "Cortex-M debugger for Python" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "pyocd-0.35.1-py3-none-any.whl", hash = "sha256:b1e0ee1951a2f3b098ef830a02c4046d76295b9d8834df785b63109f46977da4"}, - {file = "pyocd-0.35.1.tar.gz", hash = "sha256:02e8084f4d3b26d4d7c7470bb470fd4edc6c2bf2ba781e3dd94da4f84571c975"}, -] - -[package.dependencies] -capstone = ">=4.0,<5.0" -cmsis-pack-manager = ">=0.5.2,<1.0" -colorama = "<1.0" -hidapi = {version = ">=0.10.1,<1.0", markers = "platform_system != \"Linux\""} -importlib-metadata = ">=3.6" -importlib-resources = "*" -intelhex = ">=2.0,<3.0" -intervaltree = ">=3.0.2,<4.0" -lark = ">=1.1.5,<2.0" -libusb-package = ">=1.0,<2.0" -natsort = ">=8.0.0,<9.0" -prettytable = ">=2.0,<4.0" -pyelftools = "<1.0" -pylink-square = ">=1.0,<2.0" -pyusb = ">=1.2.1,<2.0" -pyyaml = ">=6.0,<7.0" -six = ">=1.15.0,<2.0" -typing-extensions = ">=4.0,<5.0" - -[package.extras] -pemicro = ["pyocd-pemicro (>=1.0.6)"] -test = ["coverage", "flake8", "pylint", "pytest (>=6.2)", "pytest-cov", "tox"] - -[[package]] -name = "pyocd-pemicro" -version = "1.1.5" -description = "PyOCD debug probe plugin for PEMicro debug probes" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "pyocd-pemicro-1.1.5.tar.gz", hash = "sha256:fc3e7c8bfcf3acd4902d32e3c06ad87347a1cb0a6a9324d8bfda80eda78ca024"}, - {file = "pyocd_pemicro-1.1.5-py3-none-any.whl", hash = "sha256:a216e0638fdc9d7b775969aaffd7db4e677fd7585de7553ef3ae1358275f165c"}, -] - -[package.dependencies] -pypemicro = ">=0.1.11" - -[[package]] -name = "pypemicro" -version = "0.1.11" -description = "Python tool to control PEMicro Debug probes" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pypemicro-0.1.11-py3-none-any.whl", hash = "sha256:5af05c034edf4bbf2e17f3ac919b8c7bda927fdeb5664840998be6173e14f449"}, - {file = "pypemicro-0.1.11.tar.gz", hash = "sha256:284d3ce6ef7220fb2e12be3518d5b01c59ba2801e082fa86a8ff428464682c4d"}, -] - -[[package]] -name = "pyreadline3" -version = "3.4.1" -description = "A python implementation of GNU readline." -optional = false -python-versions = "*" -files = [ - {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, - {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, -] - [[package]] name = "pyserial" version = "3.5" @@ -1305,77 +592,6 @@ files = [ [package.extras] cp2110 = ["hidapi"] -[[package]] -name = "pyusb" -version = "1.2.1" -description = "Python USB access module" -optional = false -python-versions = ">=3.6.0" -files = [ - {file = "pyusb-1.2.1-py3-none-any.whl", hash = "sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36"}, - {file = "pyusb-1.2.1.tar.gz", hash = "sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - [[package]] name = "requests" version = "2.31.0" @@ -1397,83 +613,6 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "ruamel-yaml" -version = "0.17.40" -description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" -optional = false -python-versions = ">=3" -files = [ - {file = "ruamel.yaml-0.17.40-py3-none-any.whl", hash = "sha256:b16b6c3816dff0a93dca12acf5e70afd089fa5acb80604afd1ffa8b465b7722c"}, - {file = "ruamel.yaml-0.17.40.tar.gz", hash = "sha256:6024b986f06765d482b5b07e086cc4b4cd05dd22ddcbc758fa23d54873cf313d"}, -] - -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} - -[package.extras] -docs = ["mercurial (>5.7)", "ryd"] -jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.8" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -optional = false -python-versions = ">=3.6" -files = [ - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, - {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, - {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, -] - [[package]] name = "semver" version = "3.0.2" @@ -1485,44 +624,6 @@ files = [ {file = "semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc"}, ] -[[package]] -name = "setuptools" -version = "71.1.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-71.1.0-py3-none-any.whl", hash = "sha256:33874fdc59b3188304b2e7c80d9029097ea31627180896fb549c578ceb8a0855"}, - {file = "setuptools-71.1.0.tar.gz", hash = "sha256:032d42ee9fb536e33087fb66cac5f840eb9391ed05637b3f2a76a7c8fb477936"}, -] - -[package.extras] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "setuptools-scm" -version = "8.1.0" -description = "the blessed package to manage your versions by scm tags" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools_scm-8.1.0-py3-none-any.whl", hash = "sha256:897a3226a6fd4a6eb2f068745e49733261a21f70b1bb28fce0339feb978d9af3"}, - {file = "setuptools_scm-8.1.0.tar.gz", hash = "sha256:42dea1b65771cba93b7a515d65a65d8246e560768a66b9106a592c8e7f26c8a7"}, -] - -[package.dependencies] -packaging = ">=20" -setuptools = "*" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} -typing-extensions = {version = "*", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["entangled-cli (>=2.0,<3.0)", "mkdocs", "mkdocs-entangled-plugin", "mkdocs-material", "mkdocstrings[python]", "pygments"] -rich = ["rich"] -test = ["build", "pytest", "rich", "typing-extensions", "wheel"] - [[package]] name = "six" version = "1.16.0" @@ -1534,75 +635,6 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] -[[package]] -name = "sly" -version = "0.5" -description = "\"SLY - Sly Lex Yacc\"" -optional = false -python-versions = "*" -files = [ - {file = "sly-0.5-py3-none-any.whl", hash = "sha256:20485483259eec7f6ba85ff4d2e96a4e50c6621902667fc2695cc8bc2a3e5133"}, - {file = "sly-0.5.tar.gz", hash = "sha256:251d42015e8507158aec2164f06035df4a82b0314ce6450f457d7125e7649024"}, -] - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -optional = false -python-versions = "*" -files = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, -] - -[[package]] -name = "spsdk" -version = "2.2.0" -description = "Open Source Secure Provisioning SDK for NXP MCU/MPU" -optional = false -python-versions = ">=3.9" -files = [ - {file = "spsdk-2.2.0-py3-none-any.whl", hash = "sha256:833a245fb0e7fe4ae18bb1ad033f5598c02766c54bae451336ad49b911075e9e"}, - {file = "spsdk-2.2.0.tar.gz", hash = "sha256:c5c014243682a0d2b57f085a061eac3ef32fc34dd1e7d6b13633b45108cd6b03"}, -] - -[package.dependencies] -asn1crypto = ">=1.2,<1.6" -bincopy = ">=17.14.5,<20.1" -bitstring = ">=3.1,<4.3" -click = ">=7.1,<8.1.4 || >8.1.4,<8.2" -click-command-tree = "<1.3" -click-option-group = ">=0.3.0,<0.6" -colorama = ">=0.4.6,<0.5" -crcmod = "<1.8" -cryptography = ">=42.0.0,<42.1" -deepmerge = "<1.2" -fastjsonschema = ">=2.15.1,<2.20" -hexdump = "<3.4" -libusbsio = ">=2.1.12,<2.2" -oscrypto = "<1.4" -packaging = ">=23.2,<24.1" -platformdirs = ">=3.9.1,<4.3" -prettytable = ">=3.0.0,<3.11" -pyocd = ">=0.35.1,<0.37" -pyocd-pemicro = ">=1.1.5,<1.2" -pyserial = ">=3.1,<3.6" -requests = ">=2.0,<2.32" -"ruamel.yaml" = ">=0.17,<0.19" -setuptools-scm = "<8.2" -sly = "<0.6" -typing-extensions = "<4.12" - -[package.extras] -all = ["asn1tools (>=0.160,<1)", "flask", "ftd2xx", "gmssl (>=3.2,<4)", "ipython", "notebook", "pyftdi", "pylibftdi", "pyscard (==2.0.2)", "python-can (<4.4)", "requests", "spsdk-pqc (>=0.3,<1.0)"] -can = ["python-can (<4.4)"] -dk6 = ["ftd2xx", "pyftdi", "pylibftdi"] -examples = ["flask", "ipython", "notebook", "requests"] -oscca = ["asn1tools (>=0.160,<1)", "gmssl (>=3.2,<4)"] -pqc = ["spsdk-pqc (>=0.3,<1.0)"] -tp = ["pyscard (==2.0.2)"] - [[package]] name = "tlv8" version = "0.10.0" @@ -1666,33 +698,7 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "zipp" -version = "3.19.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, -] - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "d952aebb2cef9b04b644703a458901efd1cf19ee8df4f8e6db45ce3fff1c2960" +content-hash = "55ddde547db56902008fcb665f58babacad628375da72524bbc4c46c4139695b" diff --git a/pyproject.toml b/pyproject.toml index ab57204..0731a78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,10 +25,13 @@ semver = "^3" tlv8 = "^0.10" # lpc55 -spsdk = ">=2,<2.3" +crcmod = "^1.7" +cryptography = ">=42" +libusbsio = "^2.1" # nrf52 ecdsa = "^0.19" +intelhex = "^2.3" protobuf = "^3.17.3" pyserial = "^3.5" @@ -41,6 +44,7 @@ flake8 = "^7.1" isort = "^5.13.2" mypy = "^1.4" types-requests = "^2.32" +typing-extensions = "^4" [tool.black] target-version = ["py39"] @@ -64,3 +68,8 @@ ignore_errors = true [[tool.mypy.overrides]] module = "nitrokey.trussed._bootloader.nrf52" disallow_untyped_calls = false + +# libusbsio is used by lpc55_upload, will be replaced eventually +[[tool.mypy.overrides]] +module = ["libusbsio.*"] +ignore_missing_imports = true diff --git a/src/nitrokey/nk3/updates.py b/src/nitrokey/nk3/updates.py index 052d524..85d36f3 100644 --- a/src/nitrokey/nk3/updates.py +++ b/src/nitrokey/nk3/updates.py @@ -14,8 +14,6 @@ from io import BytesIO from typing import Any, Callable, Iterator, List, Optional -from spsdk.mboot.exceptions import McuBootConnectionError - from nitrokey._helpers import Retries from nitrokey.nk3 import NK3, NK3Bootloader from nitrokey.trussed import TimeoutException, TrussedBase, Version @@ -25,6 +23,9 @@ Variant, validate_firmware_image, ) +from nitrokey.trussed._bootloader.lpc55_upload.mboot.exceptions import ( + McuBootConnectionError, +) from nitrokey.trussed.admin_app import BootMode from nitrokey.updates import Asset, Release diff --git a/src/nitrokey/trussed/_bootloader/lpc55.py b/src/nitrokey/trussed/_bootloader/lpc55.py index 5e5ac30..39e7134 100644 --- a/src/nitrokey/trussed/_bootloader/lpc55.py +++ b/src/nitrokey/trussed/_bootloader/lpc55.py @@ -11,16 +11,15 @@ import sys from typing import Optional, TypeVar -from spsdk.mboot.interfaces.usb import MbootUSBInterface -from spsdk.mboot.mcuboot import McuBoot -from spsdk.mboot.properties import PropertyTag -from spsdk.sbfile.sb2.images import BootImageV21 -from spsdk.utils.interfaces.device.usb_device import UsbDevice -from spsdk.utils.usbfilter import USBDeviceFilter - from nitrokey.trussed import Uuid, Version from . import FirmwareMetadata, ProgressCallback, TrussedBootloader, Variant +from .lpc55_upload.mboot.interfaces.usb import MbootUSBInterface +from .lpc55_upload.mboot.mcuboot import McuBoot +from .lpc55_upload.mboot.properties import PropertyTag +from .lpc55_upload.sbfile.sb2.images import BootImageV21 +from .lpc55_upload.utils.interfaces.device.usb_device import UsbDevice +from .lpc55_upload.utils.usbfilter import USBDeviceFilter RKTH = bytes.fromhex("050aad3e77791a81e59c5b2ba5a158937e9460ee325d8ccba09734b8fdebb171") KEK = bytes([0xAA] * 32) @@ -135,7 +134,10 @@ def _open(cls: type[T], path: str) -> Optional[T]: def parse_firmware_image(data: bytes) -> FirmwareMetadata: image = BootImageV21.parse(data, kek=KEK) - version = Version.from_bcd_version(image.header.product_version) + bcd_version = image.header.product_version + version = Version( + major=bcd_version.major, minor=bcd_version.minor, patch=bcd_version.service + ) metadata = FirmwareMetadata(version=version) if image.cert_block: if image.cert_block.rkth == RKTH: diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/README.md b/src/nitrokey/trussed/_bootloader/lpc55_upload/README.md new file mode 100644 index 0000000..189b86c --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/README.md @@ -0,0 +1,6 @@ +# LPC55 Bootloader Firmware Upload Module + +Anything inside this directory is originally extracted from: https://github.com/nxp-mcuxpresso/spsdk/tree/master. +In detail anything that is needed to upload a signed firmware image to a Nitrokey 3 xN with an LPC55 MCU. + + diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/__init__.py new file mode 100644 index 0000000..1154216 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +version = "2.1.0" + +__author__ = "NXP" +__license__ = "BSD-3-Clause" +__version__ = version +__release__ = "beta" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/__init__.py new file mode 100644 index 0000000..cd69e9a --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Module for crypto operations (certificate and key management).""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/certificate.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/certificate.py new file mode 100644 index 0000000..ef9a5d4 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/certificate.py @@ -0,0 +1,397 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Module for certificate management (generating certificate, validating certificate, chains).""" + +from datetime import datetime, timedelta +from typing import TYPE_CHECKING, Dict, List, Optional, Union + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.x509.extensions import ExtensionNotFound + +from ..crypto.hash import EnumHashAlgorithm +from ..crypto.keys import PrivateKey, PublicKey, PublicKeyEcc, PublicKeyRsa +from ..crypto.types import ( + SPSDKEncoding, + SPSDKExtensionOID, + SPSDKExtensions, + SPSDKName, + SPSDKNameOID, + SPSDKObjectIdentifier, + SPSDKVersion, +) +from ..exceptions import SPSDKError, SPSDKValueError +from ..utils.abstract import BaseClass +from ..utils.misc import align_block, load_binary, write_file + +if TYPE_CHECKING: + from typing_extensions import Self + + +class SPSDKExtensionNotFoundError(SPSDKError, ExtensionNotFound): + """Extension not found error.""" + + +class Certificate(BaseClass): + """SPSDK Certificate representation.""" + + def __init__(self, certificate: x509.Certificate) -> None: + """Constructor of SPSDK Certificate. + + :param certificate: Cryptography Certificate representation. + """ + assert isinstance(certificate, x509.Certificate) + self.cert = certificate + + @staticmethod + def generate_certificate( + subject: x509.Name, + issuer: x509.Name, + subject_public_key: PublicKey, + issuer_private_key: PrivateKey, + serial_number: Optional[int] = None, + duration: Optional[int] = None, + extensions: Optional[List[x509.ExtensionType]] = None, + ) -> "Certificate": + """Generate certificate. + + :param subject: subject name that the CA issues the certificate to + :param issuer: issuer name that issued the certificate + :param subject_public_key: Public key of subject + :param issuer_private_key: Private key of issuer + :param serial_number: certificate serial number, if not specified, random serial number will be set + :param duration: how long the certificate will be valid (in days) + :param extensions: List of extensions to include in the certificate + :return: certificate + """ + before = datetime.utcnow() if duration else datetime(2000, 1, 1) + after = ( + datetime.utcnow() + timedelta(days=duration) + if duration + else datetime(9999, 12, 31) + ) + crt = x509.CertificateBuilder( + subject_name=subject, + issuer_name=issuer, + not_valid_before=before, + not_valid_after=after, + public_key=subject_public_key.key, + # we don't pass extensions directly, need to handle the "critical" flag + extensions=[], + serial_number=serial_number or x509.random_serial_number(), + ) + + if extensions: + for ext in extensions: + crt = crt.add_extension(ext, critical=True) + + return Certificate(crt.sign(issuer_private_key.key, hashes.SHA256())) + + def save( + self, + file_path: str, + encoding_type: SPSDKEncoding = SPSDKEncoding.PEM, + ) -> None: + """Save the certificate/CSR into file. + + :param file_path: path to the file where item will be stored + :param encoding_type: encoding type (PEM or DER) + """ + write_file(self.export(encoding_type), file_path, mode="wb") + + @classmethod + def load(cls, file_path: str) -> "Self": + """Load the Certificate from the given file. + + :param file_path: path to the file, where the key is stored + """ + data = load_binary(file_path) + return cls.parse(data=data) + + def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes: + """Convert certificates into bytes. + + :param encoding: encoding type + :return: certificate in bytes form + """ + if encoding == SPSDKEncoding.NXP: + return align_block(self.export(SPSDKEncoding.DER), 4, "zeros") + + return self.cert.public_bytes( + SPSDKEncoding.get_cryptography_encodings(encoding) + ) + + def get_public_key(self) -> PublicKey: + """Get public keys from certificate. + + :return: RSA public key + """ + pub_key = self.cert.public_key() + if isinstance(pub_key, rsa.RSAPublicKey): + return PublicKeyRsa(pub_key) + if isinstance(pub_key, ec.EllipticCurvePublicKey): + return PublicKeyEcc(pub_key) + + raise SPSDKError(f"Unsupported Certificate public key: {type(pub_key)}") + + @property + def version(self) -> SPSDKVersion: + """Returns the certificate version.""" + return self.cert.version + + @property + def signature(self) -> bytes: + """Returns the signature bytes.""" + return self.cert.signature + + @property + def tbs_certificate_bytes(self) -> bytes: + """Returns the tbsCertificate payload bytes as defined in RFC 5280.""" + return self.cert.tbs_certificate_bytes + + @property + def signature_hash_algorithm( + self, + ) -> Optional[hashes.HashAlgorithm]: + """Returns a HashAlgorithm corresponding to the type of the digest signed in the certificate.""" + return self.cert.signature_hash_algorithm + + @property + def extensions(self) -> SPSDKExtensions: + """Returns an Extensions object.""" + return self.cert.extensions + + @property + def issuer(self) -> SPSDKName: + """Returns the issuer name object.""" + return self.cert.issuer + + @property + def serial_number(self) -> int: + """Returns certificate serial number.""" + return self.cert.serial_number + + @property + def subject(self) -> SPSDKName: + """Returns the subject name object.""" + return self.cert.subject + + @property + def signature_algorithm_oid(self) -> SPSDKObjectIdentifier: + """Returns the ObjectIdentifier of the signature algorithm.""" + return self.cert.signature_algorithm_oid + + def validate_subject(self, subject_certificate: "Certificate") -> bool: + """Validate certificate. + + :param subject_certificate: Subject's certificate + :raises SPSDKError: Unsupported key type in Certificate + :return: true/false whether certificate is valid or not + """ + assert subject_certificate.signature_hash_algorithm + return self.get_public_key().verify_signature( + subject_certificate.signature, + subject_certificate.tbs_certificate_bytes, + EnumHashAlgorithm.from_label( + subject_certificate.signature_hash_algorithm.name + ), + ) + + def validate(self, issuer_certificate: "Certificate") -> bool: + """Validate certificate. + + :param issuer_certificate: Issuer's certificate + :raises SPSDKError: Unsupported key type in Certificate + :return: true/false whether certificate is valid or not + """ + assert self.signature_hash_algorithm + return issuer_certificate.get_public_key().verify_signature( + self.signature, + self.tbs_certificate_bytes, + EnumHashAlgorithm.from_label(self.signature_hash_algorithm.name), + ) + + @property + def ca(self) -> bool: + """Check if CA flag is set in certificate. + + :return: true/false depending whether ca flag is set or not + """ + extension = self.extensions.get_extension_for_oid( + SPSDKExtensionOID.BASIC_CONSTRAINTS + ) + return extension.value.ca # type: ignore # mypy can not handle property definition in cryptography + + @property + def self_signed(self) -> bool: + """Indication whether the Certificate is self-signed.""" + return self.validate(self) + + @property + def raw_size(self) -> int: + """Raw size of the certificate.""" + return len(self.export()) + + def public_key_hash( + self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 + ) -> bytes: + """Get key hash. + + :param algorithm: Used hash algorithm, defaults to sha256 + :return: Key Hash + """ + return self.get_public_key().key_hash(algorithm) + + def __repr__(self) -> str: + """Text short representation about the Certificate.""" + return f"Certificate, SN:{hex(self.cert.serial_number)}" + + def __str__(self) -> str: + """Text information about the Certificate.""" + not_valid_before = self.cert.not_valid_before.strftime("%d.%m.%Y (%H:%M:%S)") + not_valid_after = self.cert.not_valid_after.strftime("%d.%m.%Y (%H:%M:%S)") + nfo = "" + nfo += f" Certification Authority: {'YES' if self.ca else 'NO'}\n" + nfo += f" Serial Number: {hex(self.cert.serial_number)}\n" + nfo += f" Validity Range: {not_valid_before} - {not_valid_after}\n" + if self.signature_hash_algorithm: + nfo += ( + f" Signature Algorithm: {self.signature_hash_algorithm.name}\n" + ) + nfo += f" Self Issued: {'YES' if self.self_signed else 'NO'}\n" + + return nfo + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Deserialize object from bytes array. + + :param data: Data to be parsed + :returns: Recreated certificate + """ + + def load_der_certificate(data: bytes) -> x509.Certificate: + """Load the DER certificate from bytes. + + This function is designed to eliminate cryptography exception + when the padded data is provided. + + :param data: Data with DER certificate + :return: Certificate (from cryptography library) + :raises SPSDKError: Unsupported certificate to load + """ + while True: + try: + return x509.load_der_x509_certificate(data) + except ValueError as exc: + if ( + len(exc.args) + and "kind: ExtraData" in exc.args[0] + and data[-1:] == b"\00" + ): + data = data[:-1] + else: + raise SPSDKValueError(str(exc)) from exc + + try: + cert = { + SPSDKEncoding.PEM: x509.load_pem_x509_certificate, + SPSDKEncoding.DER: load_der_certificate, + }[SPSDKEncoding.get_file_encodings(data)]( + data + ) # type: ignore + return Certificate(cert) # type: ignore + except ValueError as exc: + raise SPSDKError(f"Cannot load certificate: ({str(exc)})") from exc + + +def validate_certificate_chain(chain_list: List[Certificate]) -> List[bool]: + """Validate chain of certificates. + + :param chain_list: list of certificates in chain + :return: list of boolean values, which corresponds to the certificate validation in chain + :raises SPSDKError: When chain has less than two certificates + """ + if len(chain_list) <= 1: + raise SPSDKError("The chain must have at least two certificates") + result = [] + for i in range(len(chain_list) - 1): + result.append(chain_list[i].validate(chain_list[i + 1])) + return result + + +def validate_ca_flag_in_cert_chain(chain_list: List[Certificate]) -> bool: + """Validate CA flag in certification chain. + + :param chain_list: list of certificates in the chain + :return: true/false depending whether ca flag is set or not + """ + return chain_list[0].ca + + +X509NameConfig = Union[List[Dict[str, str]], Dict[str, Union[str, List[str]]]] + + +def generate_name(config: X509NameConfig) -> x509.Name: + """Generate x509 Name. + + :param config: subject/issuer description + :return: x509.Name + """ + attributes: List[x509.NameAttribute] = [] + + def _get_name_oid(name: str) -> x509.ObjectIdentifier: + try: + oid = getattr(SPSDKNameOID, name) + assert isinstance(oid, x509.ObjectIdentifier) + return oid + except Exception as exc: + raise SPSDKError(f"Invalid value of certificate attribute: {name}") from exc + + if isinstance(config, list): + for item in config: + for key, value in item.items(): + name_oid = _get_name_oid(key) + attributes.append(x509.NameAttribute(name_oid, str(value))) + + if isinstance(config, dict): + for key_second, value_second in config.items(): + name_oid = _get_name_oid(key_second) + if isinstance(value_second, list): + for value in value_second: + attributes.append(x509.NameAttribute(name_oid, str(value))) + else: + attributes.append(x509.NameAttribute(name_oid, str(value_second))) + + return x509.Name(attributes) + + +class WPCQiAuthPolicy(x509.UnrecognizedExtension): + """WPC Qi Auth Policy x509 extension.""" + + oid = x509.ObjectIdentifier("2.23.148.1.1") + + def __init__(self, value: int) -> None: + """Initialize the extension with given policy number.""" + super().__init__( + oid=self.oid, + value=b"\x04\x04" + value.to_bytes(length=4, byteorder="big"), + ) + + +class WPCQiAuthRSID(x509.UnrecognizedExtension): + """WPC Qi Auth RSID x509 extension.""" + + oid = x509.ObjectIdentifier("2.23.148.1.2") + + def __init__(self, value: str) -> None: + """Initialize the extension with given RSID in form of a hex-string.""" + super().__init__( + oid=self.oid, + value=b"\x04\x09" + bytes.fromhex(value).zfill(9), + ) diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/exceptions.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/exceptions.py new file mode 100644 index 0000000..75a84d2 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/exceptions.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Exceptions used in the Crypto module.""" + +from ..exceptions import SPSDKError + + +class SPSDKPCryptoError(SPSDKError): + """General SPSDK Crypto Error.""" + + +class SPSDKKeysNotMatchingError(SPSDKPCryptoError): + """Key pair not matching error.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/hash.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/hash.py new file mode 100644 index 0000000..fdfcc85 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/hash.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""OpenSSL implementation Hash algorithms.""" + +# Used security modules + +from math import ceil + +from cryptography.hazmat.primitives import hashes + +from ..exceptions import SPSDKError +from ..utils.misc import Endianness +from ..utils.spsdk_enum import SpsdkEnum + + +class EnumHashAlgorithm(SpsdkEnum): + """Hash algorithm enum.""" + + SHA1 = (0, "sha1", "SHA1") + SHA256 = (1, "sha256", "SHA256") + SHA384 = (2, "sha384", "SHA384") + SHA512 = (3, "sha512", "SHA512") + MD5 = (4, "md5", "MD5") + SM3 = (5, "sm3", "SM3") + + +def get_hash_algorithm(algorithm: EnumHashAlgorithm) -> hashes.HashAlgorithm: + """For specified name return hashes algorithm instance. + + :param algorithm: Algorithm type enum + :return: instance of algorithm class + :raises SPSDKError: If algorithm not found + """ + algo_cls = getattr( + hashes, algorithm.label.upper(), None + ) # hack: get class object by name + if algo_cls is None: + raise SPSDKError(f"Unsupported algorithm: hashes.{algorithm.label.upper()}") + + algo = algo_cls() + assert isinstance(algo, hashes.HashAlgorithm) + return algo + + +def get_hash_length(algorithm: EnumHashAlgorithm) -> int: + """For specified name return hash binary length. + + :param algorithm: Algorithm type enum + :return: Hash length + :raises SPSDKError: If algorithm not found + """ + return get_hash_algorithm(algorithm).digest_size + + +class Hash: + """SPSDK Hash Class.""" + + def __init__(self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256) -> None: + """Initialize hash object. + + :param algorithm: Algorithm type enum, defaults to EnumHashAlgorithm.SHA256 + """ + self.hash_obj = hashes.Hash(get_hash_algorithm(algorithm)) + + def update(self, data: bytes) -> None: + """Update the hash by new data. + + :param data: Data to be hashed + """ + self.hash_obj.update(data) + + def update_int(self, value: int) -> None: + """Update the hash by new integer value as is. + + :param value: Integer value to be hashed + """ + data = value.to_bytes( + length=ceil(value.bit_length() / 8), byteorder=Endianness.BIG.value + ) + self.update(data) + + def finalize(self) -> bytes: + """Finalize the hash and return the hash value. + + :returns: Computed hash + """ + return self.hash_obj.finalize() + + +def get_hash( + data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 +) -> bytes: + """Return a HASH from input data with specified algorithm. + + :param data: Input data in bytes + :param algorithm: Algorithm type enum + :return: Hash-ed bytes + :raises SPSDKError: If algorithm not found + """ + hash_obj = hashes.Hash(get_hash_algorithm(algorithm)) + hash_obj.update(data) + return hash_obj.finalize() diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/hmac.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/hmac.py new file mode 100644 index 0000000..338b7ee --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/hmac.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""OpenSSL implementation for HMAC packet authentication.""" + +from cryptography.exceptions import InvalidSignature + +# Used security modules +from cryptography.hazmat.primitives import hmac as hmac_cls + +from .hash import EnumHashAlgorithm, get_hash_algorithm + + +def hmac( + key: bytes, data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 +) -> bytes: + """Return a HMAC from data with specified key and algorithm. + + :param key: The key in bytes format + :param data: Input data in bytes format + :param algorithm: Algorithm type for HASH function (sha256, sha384, sha512, ...) + :return: HMAC bytes + """ + hmac_obj = hmac_cls.HMAC(key, get_hash_algorithm(algorithm)) + hmac_obj.update(data) + return hmac_obj.finalize() + + +def hmac_validate( + key: bytes, + data: bytes, + signature: bytes, + algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, +) -> bool: + """Return a HMAC from data with specified key and algorithm. + + :param key: The key in bytes format + :param data: Input data in bytes format + :param signature: HMAC signature to validate + :param algorithm: Algorithm type for HASH function (sha256, sha384, sha512, ...) + :return: HMAC bytes + """ + hmac_obj = hmac_cls.HMAC(key=key, algorithm=get_hash_algorithm(algorithm)) + hmac_obj.update(data) + try: + hmac_obj.verify(signature=signature) + return True + except InvalidSignature: + return False diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/keys.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/keys.py new file mode 100644 index 0000000..be5e688 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/keys.py @@ -0,0 +1,1225 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause +"""Module for key generation and saving keys to file.""" + +import abc +import getpass +import math +from enum import Enum +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Union + +from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm +from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa, utils +from cryptography.hazmat.primitives.serialization import ( + BestAvailableEncryption, + NoEncryption, + PrivateFormat, + PublicFormat, +) +from cryptography.hazmat.primitives.serialization import ( + load_der_private_key as crypto_load_der_private_key, +) +from cryptography.hazmat.primitives.serialization import ( + load_der_public_key as crypto_load_der_public_key, +) +from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key as crypto_load_pem_private_key, +) +from cryptography.hazmat.primitives.serialization import ( + load_pem_public_key as crypto_load_pem_public_key, +) + +from ..exceptions import SPSDKError, SPSDKValueError +from ..utils.abstract import BaseClass +from ..utils.misc import Endianness, load_binary, write_file +from .hash import EnumHashAlgorithm, get_hash, get_hash_algorithm +from .types import SPSDKEncoding + +if TYPE_CHECKING: + from typing_extensions import Self + + +def _load_pem_private_key(data: bytes, password: Optional[bytes]) -> Any: + """Load PEM Private key. + + :param data: key data + :param password: optional password + :raises SPSDKError: if the key cannot be decoded + :return: Key + """ + last_error: Exception + try: + return _crypto_load_private_key(SPSDKEncoding.PEM, data, password) + except (UnsupportedAlgorithm, ValueError) as exc: + last_error = exc + raise SPSDKError(f"Cannot load PEM private key: {last_error}") + + +def _load_der_private_key(data: bytes, password: Optional[bytes]) -> Any: + """Load DER Private key. + + :param data: key data + :param password: optional password + :raises SPSDKError: if the key cannot be decoded + :return: Key + """ + last_error: Exception + try: + return _crypto_load_private_key(SPSDKEncoding.DER, data, password) + except (UnsupportedAlgorithm, ValueError) as exc: + last_error = exc + raise SPSDKError(f"Cannot load DER private key: {last_error}") + + +def _crypto_load_private_key( + encoding: SPSDKEncoding, data: bytes, password: Optional[bytes] +) -> Union[ec.EllipticCurvePrivateKey, rsa.RSAPrivateKey]: + """Load Private key. + + :param encoding: Encoding of input data + :param data: Key data + :param password: Optional password + :raises SPSDKValueError: Unsupported encoding + :raises SPSDKWrongKeyPassphrase: Private key is encrypted and passphrase is incorrect + :raises SPSDKKeyPassphraseMissing: Private key is encrypted and passphrase is missing + :return: Key + """ + if encoding not in [SPSDKEncoding.DER, SPSDKEncoding.PEM]: + raise SPSDKValueError(f"Unsupported encoding: {encoding}") + crypto_load_function = { + SPSDKEncoding.DER: crypto_load_der_private_key, + SPSDKEncoding.PEM: crypto_load_pem_private_key, + }[encoding] + try: + private_key = crypto_load_function(data, password) + assert isinstance(private_key, (ec.EllipticCurvePrivateKey, rsa.RSAPrivateKey)) + return private_key + except ValueError as exc: + if "Incorrect password" in exc.args[0]: + raise SPSDKWrongKeyPassphrase("Provided password was incorrect.") from exc + raise exc + except TypeError as exc: + if "Password was not given but private key is encrypted" in str(exc): + raise SPSDKKeyPassphraseMissing(str(exc)) from exc + raise exc + + +def _load_pem_public_key(data: bytes) -> Any: + """Load PEM Public key. + + :param data: key data + :raises SPSDKError: if the key cannot be decoded + :return: PublicKey + """ + last_error: Exception + try: + return crypto_load_pem_public_key(data) + except (UnsupportedAlgorithm, ValueError) as exc: + last_error = exc + raise SPSDKError(f"Cannot load PEM public key: {last_error}") + + +def _load_der_public_key(data: bytes) -> Any: + """Load DER Public key. + + :param data: key data + :raises SPSDKError: if the key cannot be decoded + :return: PublicKey + """ + last_error: Exception + try: + return crypto_load_der_public_key(data) + except (UnsupportedAlgorithm, ValueError) as exc: + last_error = exc + raise SPSDKError(f"Cannot load DER private key: {last_error}") + + +class SPSDKInvalidKeyType(SPSDKError): + """Invalid Key Type.""" + + +class SPSDKKeyPassphraseMissing(SPSDKError): + """Passphrase for decryption of private key is missing.""" + + +class SPSDKWrongKeyPassphrase(SPSDKError): + """Passphrase for decryption of private key is wrong.""" + + +class PrivateKey(BaseClass, abc.ABC): + """SPSDK Private Key.""" + + key: Any + + @classmethod + @abc.abstractmethod + def generate_key(cls) -> "Self": + """Generate SPSDK Key (private key). + + :return: SPSDK private key + """ + + @property + @abc.abstractmethod + def signature_size(self) -> int: + """Size of signature data.""" + + @property + @abc.abstractmethod + def key_size(self) -> int: + """Key size in bits. + + :return: Key Size + """ + + @abc.abstractmethod + def get_public_key(self) -> "PublicKey": + """Generate public key. + + :return: Public key + """ + + @abc.abstractmethod + def verify_public_key(self, public_key: "PublicKey") -> bool: + """Verify public key. + + :param public_key: Public key to verify + :return: True if is in pair, False otherwise + """ + + def __eq__(self, obj: Any) -> bool: + """Check object equality.""" + return ( + isinstance(obj, self.__class__) + and self.get_public_key() == obj.get_public_key() + ) + + def save( + self, + file_path: str, + password: Optional[str] = None, + encoding: SPSDKEncoding = SPSDKEncoding.PEM, + ) -> None: + """Save the Private key to the given file. + + :param file_path: path to the file, where the key will be stored + :param password: password to private key; None to store without password + :param encoding: encoding type, default is PEM + """ + write_file( + self.export(password=password, encoding=encoding), file_path, mode="wb" + ) + + @classmethod + def load(cls, file_path: str, password: Optional[str] = None) -> "PrivateKey": + """Load the Private key from the given file. + + :param file_path: path to the file, where the key is stored + :param password: password to private key; None to load without password + """ + data = load_binary(file_path) + return cls.parse(data=data, password=password) + + @abc.abstractmethod + def sign(self, data: bytes) -> bytes: + """Sign input data. + + :param data: Input data + :return: Signed data + """ + + @abc.abstractmethod + def export( + self, + password: Optional[str] = None, + encoding: SPSDKEncoding = SPSDKEncoding.DER, + ) -> bytes: + """Export key into bytes in requested format. + + :param password: password to private key; None to store without password + :param encoding: encoding type, default is DER + :return: Byte representation of key + """ + + @classmethod + def parse(cls, data: bytes, password: Optional[str] = None) -> "PrivateKey": + """Deserialize object from bytes array. + + :param data: Data to be parsed + :param password: password to private key; None to store without password + :returns: Recreated key + """ + try: + private_key = { + SPSDKEncoding.PEM: _load_pem_private_key, + SPSDKEncoding.DER: _load_der_private_key, + }[SPSDKEncoding.get_file_encodings(data)]( + data, password.encode("utf-8") if password else None + ) + if isinstance(private_key, (ec.EllipticCurvePrivateKey, rsa.RSAPrivateKey)): + return cls.create(private_key) + except (ValueError, SPSDKInvalidKeyType) as exc: + raise SPSDKError(f"Cannot load private key: ({str(exc)})") from exc + raise SPSDKError(f"Unsupported private key: ({str(private_key)})") + + @classmethod + def create(cls, key: Any) -> "PrivateKey": + """Create Private Key object. + + :param key: Supported private key. + :raises SPSDKInvalidKeyType: Unsupported private key given + :return: SPSDK Private Kye object + """ + if isinstance(key, ec.EllipticCurvePrivateKey): + return PrivateKeyEcc(key) + if isinstance(key, rsa.RSAPrivateKey): + return PrivateKeyRsa(key) + + raise SPSDKInvalidKeyType(f"Unsupported key type: {str(key)}") + + +class PublicKey(BaseClass, abc.ABC): + """SPSDK Public Key.""" + + key: Any + + @property + @abc.abstractmethod + def signature_size(self) -> int: + """Size of signature data.""" + + @property + @abc.abstractmethod + def public_numbers(self) -> Any: + """Public numbers.""" + + def save(self, file_path: str, encoding: SPSDKEncoding = SPSDKEncoding.PEM) -> None: + """Save the public key to the file. + + :param file_path: path to the file, where the key will be stored + :param encoding: encoding type, default is PEM + """ + write_file(data=self.export(encoding=encoding), path=file_path, mode="wb") + + @classmethod + def load(cls, file_path: str) -> "PublicKey": + """Load the Public key from the given file. + + :param file_path: path to the file, where the key is stored + """ + data = load_binary(file_path) + return cls.parse(data=data) + + @abc.abstractmethod + def verify_signature( + self, + signature: bytes, + data: bytes, + algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, + ) -> bool: + """Verify input data. + + :param signature: The signature of input data + :param data: Input data + :param algorithm: Used algorithm + :return: True if signature is valid, False otherwise + """ + + @abc.abstractmethod + def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes: + """Export key into bytes to requested format. + + :param encoding: encoding type, default is NXP + :return: Byte representation of key + """ + + @classmethod + def parse(cls, data: bytes) -> "PublicKey": + """Deserialize object from bytes array. + + :param data: Data to be parsed + :returns: Recreated key + """ + try: + public_key = { + SPSDKEncoding.PEM: _load_pem_public_key, + SPSDKEncoding.DER: _load_der_public_key, + }[SPSDKEncoding.get_file_encodings(data)](data) + if isinstance(public_key, (ec.EllipticCurvePublicKey, rsa.RSAPublicKey)): + return cls.create(public_key) + except (ValueError, SPSDKInvalidKeyType) as exc: + raise SPSDKError(f"Cannot load public key: ({str(exc)})") from exc + raise SPSDKError(f"Unsupported public key: ({str(public_key)})") + + def key_hash( + self, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 + ) -> bytes: + """Get key hash. + + :param algorithm: Used hash algorithm, defaults to sha256 + :return: Key Hash + """ + return get_hash(self.export(), algorithm) + + def __eq__(self, obj: Any) -> bool: + """Check object equality.""" + return ( + isinstance(obj, self.__class__) + and self.public_numbers == obj.public_numbers + ) + + @classmethod + def create(cls, key: Any) -> "PublicKey": + """Create Public Key object. + + :param key: Supported public key. + :raises SPSDKInvalidKeyType: Unsupported public key given + :return: SPSDK Public Kye object + """ + if isinstance(key, ec.EllipticCurvePublicKey): + return PublicKeyEcc(key) + if isinstance(key, rsa.RSAPublicKey): + return PublicKeyRsa(key) + + raise SPSDKInvalidKeyType(f"Unsupported key type: {str(key)}") + + +# =================================================================================================== +# =================================================================================================== +# +# RSA Keys +# +# =================================================================================================== +# =================================================================================================== + + +class PrivateKeyRsa(PrivateKey): + """SPSDK Private Key.""" + + SUPPORTED_KEY_SIZES = [2048, 3072, 4096] + + key: rsa.RSAPrivateKey + + def __init__(self, key: rsa.RSAPrivateKey) -> None: + """Create SPSDK Key. + + :param key: Only RSA key is accepted + """ + self.key = key + + @classmethod + def generate_key(cls, key_size: int = 2048, exponent: int = 65537) -> "Self": + """Generate SPSDK Key (private key). + + :param key_size: key size in bits; must be >= 512 + :param exponent: public exponent; must be >= 3 and odd + :return: SPSDK private key + """ + return cls( + rsa.generate_private_key( + public_exponent=exponent, + key_size=key_size, + ) + ) + + @property + def signature_size(self) -> int: + """Size of signature data.""" + return self.key.key_size // 8 + + @property + def key_size(self) -> int: + """Key size in bits. + + :return: Key Size + """ + return self.key.key_size + + def get_public_key(self) -> "PublicKeyRsa": + """Generate public key. + + :return: Public key + """ + return PublicKeyRsa(self.key.public_key()) + + def verify_public_key(self, public_key: PublicKey) -> bool: + """Verify public key. + + :param public_key: Public key to verify + :return: True if is in pair, False otherwise + """ + return self.get_public_key() == public_key + + def export( + self, + password: Optional[str] = None, + encoding: SPSDKEncoding = SPSDKEncoding.DER, + ) -> bytes: + """Export the Private key to the bytes in requested encoding. + + :param password: password to private key; None to store without password + :param encoding: encoding type, default is DER + :returns: Private key in bytes + """ + enc = ( + BestAvailableEncryption(password=password.encode("utf-8")) + if password + else NoEncryption() + ) + return self.key.private_bytes( + SPSDKEncoding.get_cryptography_encodings(encoding), PrivateFormat.PKCS8, enc + ) + + def sign( + self, data: bytes, algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256 + ) -> bytes: + """Sign input data. + + :param data: Input data + :param algorithm: Used algorithm + :return: Signed data + """ + signature = self.key.sign( + data=data, + padding=padding.PKCS1v15(), + algorithm=get_hash_algorithm(algorithm), + ) + return signature + + @classmethod + def parse(cls, data: bytes, password: Optional[str] = None) -> "PrivateKeyRsa": + """Deserialize object from bytes array. + + :param data: Data to be parsed + :param password: password to private key; None to store without password + :returns: Recreated key + """ + key = super().parse(data=data, password=password) + if isinstance(key, PrivateKeyRsa): + return key + + raise SPSDKInvalidKeyType("Can't parse Rsa private key from given data") + + def __repr__(self) -> str: + return f"RSA{self.key_size} Private Key" + + def __str__(self) -> str: + """Object description in string format.""" + ret = ( + f"RSA{self.key_size} Private key: \nd({hex(self.key.private_numbers().d)})" + ) + return ret + + +class PublicKeyRsa(PublicKey): + """SPSDK Public Key.""" + + key: rsa.RSAPublicKey + + def __init__(self, key: rsa.RSAPublicKey) -> None: + """Create SPSDK Public Key. + + :param key: SPSDK Public Key data or file path + """ + self.key = key + + @property + def signature_size(self) -> int: + """Size of signature data.""" + return self.key.key_size // 8 + + @property + def key_size(self) -> int: + """Key size in bits. + + :return: Key Size + """ + return self.key.key_size + + @property + def public_numbers(self) -> rsa.RSAPublicNumbers: + """Public numbers of key. + + :return: Public numbers + """ + return self.key.public_numbers() + + @property + def e(self) -> int: + """Public number E. + + :return: E + """ + return self.public_numbers.e + + @property + def n(self) -> int: + """Public number N. + + :return: N + """ + return self.public_numbers.n + + def export( + self, + encoding: SPSDKEncoding = SPSDKEncoding.NXP, + exp_length: Optional[int] = None, + modulus_length: Optional[int] = None, + ) -> bytes: + """Save the public key to the bytes in NXP or DER format. + + :param encoding: encoding type, default is NXP + :param exp_length: Optional specific exponent length in bytes + :param modulus_length: Optional specific modulus length in bytes + :returns: Public key in bytes + """ + if encoding == SPSDKEncoding.NXP: + exp_rotk = self.e + mod_rotk = self.n + exp_length = exp_length or math.ceil(exp_rotk.bit_length() / 8) + modulus_length = modulus_length or math.ceil(mod_rotk.bit_length() / 8) + exp_rotk_bytes = exp_rotk.to_bytes(exp_length, Endianness.BIG.value) + mod_rotk_bytes = mod_rotk.to_bytes(modulus_length, Endianness.BIG.value) + return mod_rotk_bytes + exp_rotk_bytes + + return self.key.public_bytes( + SPSDKEncoding.get_cryptography_encodings(encoding), PublicFormat.PKCS1 + ) + + def verify_signature( + self, + signature: bytes, + data: bytes, + algorithm: EnumHashAlgorithm = EnumHashAlgorithm.SHA256, + ) -> bool: + """Verify input data. + + :param signature: The signature of input data + :param data: Input data + :param algorithm: Used algorithm + :return: True if signature is valid, False otherwise + """ + try: + self.key.verify( + signature=signature, + data=data, + padding=padding.PKCS1v15(), + algorithm=get_hash_algorithm(algorithm), + ) + except InvalidSignature: + return False + + return True + + def __eq__(self, obj: Any) -> bool: + """Check object equality.""" + return ( + isinstance(obj, self.__class__) + and self.public_numbers == obj.public_numbers + ) + + def __repr__(self) -> str: + return f"RSA{self.key_size} Public Key" + + def __str__(self) -> str: + """Object description in string format.""" + ret = f"RSA{self.key_size} Public key: \ne({hex(self.e)}) \nn({hex(self.n)})" + return ret + + @classmethod + def recreate(cls, exponent: int, modulus: int) -> "Self": + """Recreate RSA public key from Exponent and modulus. + + :param exponent: Exponent of RSA key. + :param modulus: Modulus of RSA key. + :return: RSA public key. + """ + public_numbers = rsa.RSAPublicNumbers(e=exponent, n=modulus) + return cls(public_numbers.public_key()) + + @staticmethod + def recreate_public_numbers(data: bytes) -> rsa.RSAPublicNumbers: + """Recreate public numbers from data. + + :param data: Dat with raw key. + :raises SPSDKError: Un recognized data. + :return: RAS public numbers. + """ + data_len = len(data) + for key_size in PrivateKeyRsa.SUPPORTED_KEY_SIZES: + key_size_bytes = key_size // 8 + if key_size_bytes + 3 <= data_len <= key_size_bytes + 4: + n = int.from_bytes(data[:key_size_bytes], Endianness.BIG.value) + e = int.from_bytes(data[key_size_bytes:], Endianness.BIG.value) + return rsa.RSAPublicNumbers(e=e, n=n) + + raise SPSDKError(f"Unsupported RSA key to recreate with data size {data_len}") + + @classmethod + def parse(cls, data: bytes) -> "PublicKeyRsa": + """Deserialize object from bytes array. + + :param data: Data to be parsed + :returns: Recreated key + """ + try: + key = super().parse(data=data) + if isinstance(key, PublicKeyRsa): + return key + except SPSDKError: + public_numbers = PublicKeyRsa.recreate_public_numbers(data) + return PublicKeyRsa(public_numbers.public_key()) + + raise SPSDKInvalidKeyType("Can't parse RSA public key from given data") + + +# =================================================================================================== +# =================================================================================================== +# +# Elliptic Curves Keys +# +# =================================================================================================== +# =================================================================================================== + + +class EccCurve(str, Enum): + """Supported ecc key types.""" + + SECP256R1 = "secp256r1" + SECP384R1 = "secp384r1" + SECP521R1 = "secp521r1" + + +class SPSDKUnsupportedEccCurve(SPSDKValueError): + """Unsupported Ecc curve error.""" + + +class KeyEccCommon: + """SPSDK Common Key.""" + + key: Union[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey] + + @property + def coordinate_size(self) -> int: + """Size of signature data.""" + return math.ceil(self.key.key_size / 8) + + @property + def signature_size(self) -> int: + """Size of signature data.""" + return self.coordinate_size * 2 + + @property + def curve(self) -> EccCurve: + """Curve type.""" + return EccCurve(self.key.curve.name) + + @property + def key_size(self) -> int: + """Key size in bits.""" + return self.key.key_size + + @staticmethod + def _get_ec_curve_object(name: EccCurve) -> ec.EllipticCurve: + """Get the EC curve object by its name. + + :param name: Name of EC curve. + :return: EC curve object. + :raises SPSDKValueError: Invalid EC curve name. + """ + # pylint: disable=protected-access + for key_object in ec._CURVE_TYPES: + if key_object.lower() == name.lower(): + # pylint: disable=protected-access + return ec._CURVE_TYPES[key_object] + + raise SPSDKValueError(f"The EC curve with name '{name}' is not supported.") + + @staticmethod + def serialize_signature(signature: bytes, coordinate_length: int) -> bytes: + """Re-format ECC ANS.1 DER signature into the format used by ROM code.""" + r, s = utils.decode_dss_signature(signature) + + r_bytes = r.to_bytes(coordinate_length, Endianness.BIG.value) + s_bytes = s.to_bytes(coordinate_length, Endianness.BIG.value) + return r_bytes + s_bytes + + +class PrivateKeyEcc(KeyEccCommon, PrivateKey): + """SPSDK Private Key.""" + + key: ec.EllipticCurvePrivateKey + + def __init__(self, key: ec.EllipticCurvePrivateKey) -> None: + """Create SPSDK Ecc Private Key. + + :param key: Only Ecc key is accepted + """ + self.key = key + + @classmethod + def generate_key(cls, curve_name: EccCurve = EccCurve.SECP256R1) -> "Self": + """Generate SPSDK Key (private key). + + :param curve_name: Name of curve + :return: SPSDK private key + """ + curve_obj = cls._get_ec_curve_object(curve_name) + prv = ec.generate_private_key(curve_obj) + return cls(prv) + + def exchange(self, peer_public_key: "PublicKeyEcc") -> bytes: + """Exchange key using ECDH algorithm with provided peer public key. + + :param peer_public_key: Peer public key + :return: Shared key + """ + return self.key.exchange( + algorithm=ec.ECDH(), peer_public_key=peer_public_key.key + ) + + def get_public_key(self) -> "PublicKeyEcc": + """Generate public key. + + :return: Public key + """ + return PublicKeyEcc(self.key.public_key()) + + def verify_public_key(self, public_key: PublicKey) -> bool: + """Verify public key. + + :param public_key: Public key to verify + :return: True if is in pair, False otherwise + """ + return self.get_public_key() == public_key + + def export( + self, + password: Optional[str] = None, + encoding: SPSDKEncoding = SPSDKEncoding.DER, + ) -> bytes: + """Export the Private key to the bytes in requested format. + + :param password: password to private key; None to store without password + :param encoding: encoding type, default is DER + :returns: Private key in bytes + """ + return self.key.private_bytes( + encoding=SPSDKEncoding.get_cryptography_encodings(encoding), + format=PrivateFormat.PKCS8, + encryption_algorithm=( + BestAvailableEncryption(password.encode("utf-8")) + if password + else NoEncryption() + ), + ) + + def sign( + self, + data: bytes, + algorithm: Optional[EnumHashAlgorithm] = None, + der_format: bool = False, + prehashed: bool = False, + ) -> bytes: + """Sign input data. + + :param data: Input data + :param algorithm: Used algorithm + :param der_format: Use DER format as a output + :param prehashed: Use pre hashed value as input + :return: Signed data + """ + hash_name = ( + algorithm + or { + 256: EnumHashAlgorithm.SHA256, + 384: EnumHashAlgorithm.SHA384, + 521: EnumHashAlgorithm.SHA512, + }[self.key.key_size] + ) + if prehashed: + signature_algorithm = ec.ECDSA( + utils.Prehashed(get_hash_algorithm(hash_name)) + ) + else: + signature_algorithm = ec.ECDSA(get_hash_algorithm(hash_name)) + signature = self.key.sign(data, signature_algorithm) + + if der_format: + return signature + + return self.serialize_signature(signature, self.coordinate_size) + + @property + def d(self) -> int: + """Private number D.""" + return self.key.private_numbers().private_value + + @classmethod + def parse(cls, data: bytes, password: Optional[str] = None) -> "PrivateKeyEcc": + """Deserialize object from bytes array. + + :param data: Data to be parsed + :param password: password to private key; None to store without password + :returns: Recreated key + """ + key = super().parse(data=data, password=password) + if isinstance(key, PrivateKeyEcc): + return key + + raise SPSDKInvalidKeyType("Can't parse Ecc private key from given data") + + @classmethod + def recreate(cls, d: int, curve: EccCurve) -> "Self": + """Recreate ECC private key from private key number. + + :param d: Private number D. + :param curve: ECC curve. + + :return: ECC private key. + """ + key = ec.derive_private_key(d, cls._get_ec_curve_object(curve)) + return cls(key) + + def __repr__(self) -> str: + return f"ECC {self.curve} Private Key" + + def __str__(self) -> str: + """Object description in string format.""" + return f"ECC ({self.curve}) Private key: \nd({hex(self.d)})" + + +class PublicKeyEcc(KeyEccCommon, PublicKey): + """SPSDK Public Key.""" + + key: ec.EllipticCurvePublicKey + + def __init__(self, key: ec.EllipticCurvePublicKey) -> None: + """Create SPSDK Public Key. + + :param key: SPSDK Public Key data or file path + """ + self.key = key + + def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes: + """Export the public key to the bytes in requested format. + + :param encoding: encoding type, default is NXP + :returns: Public key in bytes + """ + if encoding == SPSDKEncoding.NXP: + x_bytes = self.x.to_bytes(self.coordinate_size, Endianness.BIG.value) + y_bytes = self.y.to_bytes(self.coordinate_size, Endianness.BIG.value) + return x_bytes + y_bytes + + return self.key.public_bytes( + SPSDKEncoding.get_cryptography_encodings(encoding), + PublicFormat.SubjectPublicKeyInfo, + ) + + def verify_signature( + self, + signature: bytes, + data: bytes, + algorithm: Optional[EnumHashAlgorithm] = None, + prehashed: bool = False, + ) -> bool: + """Verify input data. + + :param signature: The signature of input data + :param data: Input data + :param algorithm: Used algorithm + :param prehashed: Use pre hashed value as input + :return: True if signature is valid, False otherwise + """ + coordinate_size = math.ceil(self.key.key_size / 8) + hash_name = ( + algorithm + or { + 256: EnumHashAlgorithm.SHA256, + 384: EnumHashAlgorithm.SHA384, + 521: EnumHashAlgorithm.SHA512, + }[self.key.key_size] + ) + + if prehashed: + signature_algorithm = ec.ECDSA( + utils.Prehashed(get_hash_algorithm(hash_name)) + ) + else: + signature_algorithm = ec.ECDSA(get_hash_algorithm(hash_name)) + + if len(signature) == self.signature_size: + der_signature = utils.encode_dss_signature( + int.from_bytes( + signature[:coordinate_size], byteorder=Endianness.BIG.value + ), + int.from_bytes( + signature[coordinate_size:], byteorder=Endianness.BIG.value + ), + ) + else: + der_signature = signature + try: + # pylint: disable=no-value-for-parameter # pylint is mixing RSA and ECC verify methods + self.key.verify(der_signature, data, signature_algorithm) + return True + except InvalidSignature: + return False + + @property + def public_numbers(self) -> ec.EllipticCurvePublicNumbers: + """Public numbers of key. + + :return: Public numbers + """ + return self.key.public_numbers() + + @property + def x(self) -> int: + """Public number X. + + :return: X + """ + return self.public_numbers.x + + @property + def y(self) -> int: + """Public number Y. + + :return: Y + """ + return self.public_numbers.y + + @classmethod + def recreate(cls, coor_x: int, coor_y: int, curve: EccCurve) -> "Self": + """Recreate ECC public key from coordinates. + + :param coor_x: X coordinate of point on curve. + :param coor_y: Y coordinate of point on curve. + :param curve: ECC curve. + :return: ECC public key. + """ + pub_numbers = ec.EllipticCurvePublicNumbers( + x=coor_x, y=coor_y, curve=PrivateKeyEcc._get_ec_curve_object(curve) + ) + key = pub_numbers.public_key() + return cls(key) + + @classmethod + def recreate_from_data( + cls, data: bytes, curve: Optional[EccCurve] = None + ) -> "Self": + """Recreate ECC public key from coordinates in data blob. + + :param data: Data blob of coordinates in bytes (X,Y in Big Endian) + :param curve: ECC curve. + :return: ECC public key. + """ + + def get_curve( + data_length: int, curve: Optional[EccCurve] = None + ) -> Tuple[EccCurve, bool]: + curve_list = [curve] if curve else list(EccCurve) + for cur in curve_list: + curve_obj = KeyEccCommon._get_ec_curve_object(EccCurve(cur)) + curve_sign_size = math.ceil(curve_obj.key_size / 8) * 2 + # Check raw binary format + if curve_sign_size == data_length: + return (cur, False) + # Check DER binary format + curve_sign_size += 7 + if curve_sign_size <= data_length <= curve_sign_size + 2: + return (cur, True) + raise SPSDKUnsupportedEccCurve( + f"Cannot recreate ECC curve with {data_length} length" + ) + + data_length = len(data) + (curve, der_format) = get_curve(data_length, curve) + + if der_format: + der = _load_der_public_key(data) + assert isinstance(der, ec.EllipticCurvePublicKey) + return cls(der) + + coordinate_length = data_length // 2 + coor_x = int.from_bytes( + data[:coordinate_length], byteorder=Endianness.BIG.value + ) + coor_y = int.from_bytes( + data[coordinate_length:], byteorder=Endianness.BIG.value + ) + return cls.recreate(coor_x=coor_x, coor_y=coor_y, curve=curve) + + @classmethod + def parse(cls, data: bytes) -> "PublicKeyEcc": + """Deserialize object from bytes array. + + :param data: Data to be parsed + :returns: Recreated key + """ + try: + key = super().parse(data=data) + if isinstance(key, PublicKeyEcc): + return key + except SPSDKError: + return cls.recreate_from_data(data=data) + + raise SPSDKInvalidKeyType("Can't parse ECC public key from given data") + + def __repr__(self) -> str: + return f"ECC {self.curve} Public Key" + + def __str__(self) -> str: + """Object description in string format.""" + return f"ECC ({self.curve}) Public key: \nx({hex(self.x)}) \ny({hex(self.y)})" + + +class ECDSASignature: + """ECDSA Signature.""" + + COORDINATE_LENGTHS = { + EccCurve.SECP256R1: 32, + EccCurve.SECP384R1: 48, + EccCurve.SECP521R1: 66, + } + + def __init__(self, r: int, s: int, ecc_curve: EccCurve) -> None: + """ECDSA Signature constructor. + + :param r: r value of signature + :param s: s value of signature + :param ecc_curve: ECC Curve enum + """ + self.r = r + self.s = s + self.ecc_curve = ecc_curve + + @classmethod + def parse(cls, signature: bytes) -> "Self": + """Parse signature in DER or NXP format. + + :param signature: Signature binary + """ + encoding = cls.get_encoding(signature) + if encoding == SPSDKEncoding.DER: + r, s = utils.decode_dss_signature(signature) + ecc_curve = cls.get_ecc_curve(len(signature)) + return cls(r, s, ecc_curve) + if encoding == SPSDKEncoding.NXP: + r = int.from_bytes(signature[: len(signature) // 2], Endianness.BIG.value) + s = int.from_bytes(signature[len(signature) // 2 :], Endianness.BIG.value) + ecc_curve = cls.get_ecc_curve(len(signature)) + return cls(r, s, ecc_curve) + raise SPSDKValueError(f"Invalid signature encoding {encoding.value}") + + def export(self, encoding: SPSDKEncoding = SPSDKEncoding.NXP) -> bytes: + """Export signature in DER or NXP format. + + :param encoding: Signature encoding + :return: Signature as bytes + """ + if encoding == SPSDKEncoding.NXP: + r_bytes = self.r.to_bytes( + self.COORDINATE_LENGTHS[self.ecc_curve], Endianness.BIG.value + ) + s_bytes = self.s.to_bytes( + self.COORDINATE_LENGTHS[self.ecc_curve], Endianness.BIG.value + ) + return r_bytes + s_bytes + if encoding == SPSDKEncoding.DER: + return utils.encode_dss_signature(self.r, self.s) + raise SPSDKValueError(f"Invalid signature encoding {encoding.value}") + + @classmethod + def get_encoding(cls, signature: bytes) -> SPSDKEncoding: + """Get encoding of signature. + + :param signature: Signature + """ + signature_length = len(signature) + # Try detect the NXP format by data length + if signature_length // 2 in cls.COORDINATE_LENGTHS.values(): + return SPSDKEncoding.NXP + # Try detect the DER format by decode of header + try: + utils.decode_dss_signature(signature) + return SPSDKEncoding.DER + except ValueError: + pass + raise SPSDKValueError( + f"The given signature with length {signature_length} does not match any encoding" + ) + + @classmethod + def get_ecc_curve(cls, signature_length: int) -> EccCurve: + """Get the Elliptic Curve of signature. + + :param signature_length: Signature length + """ + for curve, coord_len in cls.COORDINATE_LENGTHS.items(): + if signature_length == coord_len * 2: + return curve + if signature_length in range(coord_len * 2 + 3, coord_len * 2 + 9): + return curve + raise SPSDKValueError( + f"The given signature with length {signature_length} does not match any ecc curve" + ) + + +# # =================================================================================================== +# # =================================================================================================== +# # +# # General section +# # +# # =================================================================================================== +# # =================================================================================================== + +GeneratorParams = Dict[str, Union[int, str, bool]] +KeyGeneratorInfo = Dict[str, Tuple[Callable[..., PrivateKey], GeneratorParams]] + + +def get_supported_keys_generators() -> KeyGeneratorInfo: + """Generate list with list of supported key types. + + :return: `KeyGeneratorInfo` dictionary of supported key types. + """ + ret: KeyGeneratorInfo = { + # RSA keys + "rsa2048": (PrivateKeyRsa.generate_key, {"key_size": 2048}), + "rsa3072": (PrivateKeyRsa.generate_key, {"key_size": 3072}), + "rsa4096": (PrivateKeyRsa.generate_key, {"key_size": 4096}), + # ECC keys + "secp256r1": (PrivateKeyEcc.generate_key, {"curve_name": "secp256r1"}), + "secp384r1": (PrivateKeyEcc.generate_key, {"curve_name": "secp384r1"}), + "secp521r1": (PrivateKeyEcc.generate_key, {"curve_name": "secp521r1"}), + } + + return ret + + +def get_ecc_curve(key_length: int) -> EccCurve: + """Get curve name for Crypto library. + + :param key_length: Length of ecc key in bytes + """ + if key_length <= 32 or key_length == 64: + return EccCurve.SECP256R1 + if key_length <= 48 or key_length == 96: + return EccCurve.SECP384R1 + if key_length <= 66: + return EccCurve.SECP521R1 + raise SPSDKError(f"Not sure what curve corresponds to {key_length} data") + + +def prompt_for_passphrase() -> str: + """Prompt interactively for private key passphrase.""" + password = getpass.getpass( + prompt="Private key is encrypted. Enter password: ", stream=None + ) + return password diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/rng.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/rng.py new file mode 100644 index 0000000..7cf2a23 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/rng.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Implementation for getting random numbers.""" + +# Used security modules + + +from secrets import token_bytes + + +def random_bytes(length: int) -> bytes: + """Return a random byte string with specified length. + + :param length: The length in bytes + :return: Random bytes + """ + return token_bytes(length) diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/symmetric.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/symmetric.py new file mode 100644 index 0000000..2f3324f --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/symmetric.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""OpenSSL implementation for symmetric key encryption.""" + + +# Used security modules +from typing import Optional + +from cryptography.hazmat.primitives import keywrap +from cryptography.hazmat.primitives.ciphers import Cipher, aead, algorithms, modes + +from ..exceptions import SPSDKError +from ..utils.misc import Endianness, align_block + + +class Counter: + """AES counter with specified counter byte ordering and customizable increment.""" + + @property + def value(self) -> bytes: + """Initial vector for AES encryption.""" + return self._nonce + self._ctr.to_bytes(4, self._ctr_byteorder_encoding.value) + + def __init__( + self, + nonce: bytes, + ctr_value: Optional[int] = None, + ctr_byteorder_encoding: Endianness = Endianness.LITTLE, + ): + """Constructor. + + :param nonce: last four bytes are used as initial value for counter + :param ctr_value: counter initial value; it is added to counter value retrieved from nonce + :param ctr_byteorder_encoding: way how the counter is encoded into output value + :raises SPSDKError: When invalid byteorder is provided + """ + assert isinstance(nonce, bytes) and len(nonce) == 16 + self._nonce = nonce[:-4] + self._ctr_byteorder_encoding = ctr_byteorder_encoding + self._ctr = int.from_bytes(nonce[-4:], ctr_byteorder_encoding.value) + if ctr_value is not None: + self._ctr += ctr_value + + def increment(self, value: int = 1) -> None: + """Increment counter by specified value. + + :param value: to add to counter + """ + self._ctr += value + + +def aes_key_wrap(kek: bytes, key_to_wrap: bytes) -> bytes: + """Wraps a key using a key-encrypting key (KEK). + + :param kek: The key-encrypting key + :param key_to_wrap: Plain data + :return: Wrapped key + """ + return keywrap.aes_key_wrap(kek, key_to_wrap) + + +def aes_key_unwrap(kek: bytes, wrapped_key: bytes) -> bytes: + """Unwraps a key using a key-encrypting key (KEK). + + :param kek: The key-encrypting key + :param wrapped_key: Encrypted data + :return: Un-wrapped key + """ + return keywrap.aes_key_unwrap(kek, wrapped_key) + + +def aes_ecb_encrypt(key: bytes, plain_data: bytes) -> bytes: + """Encrypt plain data with AES in ECB mode. + + :param key: The key for data encryption + :param plain_data: Input data + :return: Encrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.ECB()) + enc = cipher.encryptor() + return enc.update(plain_data) + enc.finalize() + + +def aes_ecb_decrypt(key: bytes, encrypted_data: bytes) -> bytes: + """Decrypt encrypted data with AES in ECB mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :return: Decrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.ECB()) + enc = cipher.decryptor() + return enc.update(encrypted_data) + enc.finalize() + + +def aes_cbc_encrypt( + key: bytes, plain_data: bytes, iv_data: Optional[bytes] = None +) -> bytes: + """Encrypt plain data with AES in CBC mode. + + :param key: The key for data encryption + :param plain_data: Input data + :param iv_data: Initialization vector data + :raises SPSDKError: Invalid Key or IV + :return: Encrypted image + """ + if len(key) * 8 not in algorithms.AES.key_sizes: + raise SPSDKError( + "The key must be a valid AES key length: " + f"{', '.join([str(k) for k in algorithms.AES.key_sizes])}" + ) + init_vector = iv_data or bytes(algorithms.AES.block_size // 8) + if len(init_vector) * 8 != algorithms.AES.block_size: + raise SPSDKError( + f"The initial vector length must be {algorithms.AES.block_size // 8}" + ) + cipher = Cipher(algorithms.AES(key), modes.CBC(init_vector)) + enc = cipher.encryptor() + return ( + enc.update(align_block(plain_data, alignment=algorithms.AES.block_size // 8)) + + enc.finalize() + ) + + +def aes_cbc_decrypt( + key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] = None +) -> bytes: + """Decrypt encrypted data with AES in CBC mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :param iv_data: Initialization vector data + :raises SPSDKError: Invalid Key or IV + :return: Decrypted image + """ + if len(key) * 8 not in algorithms.AES.key_sizes: + raise SPSDKError( + "The key must be a valid AES key length: " + f"{', '.join([str(k) for k in algorithms.AES.key_sizes])}" + ) + init_vector = iv_data or bytes(algorithms.AES.block_size) + if len(init_vector) * 8 != algorithms.AES.block_size: + raise SPSDKError( + f"The initial vector length must be {algorithms.AES.block_size}" + ) + cipher = Cipher(algorithms.AES(key), modes.CBC(init_vector)) + dec = cipher.decryptor() + return dec.update(encrypted_data) + dec.finalize() + + +def aes_ctr_encrypt(key: bytes, plain_data: bytes, nonce: bytes) -> bytes: + """Encrypt plain data with AES in CTR mode. + + :param key: The key for data encryption + :param plain_data: Input data + :param nonce: Nonce data with counter value + :return: Encrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.CTR(nonce)) + enc = cipher.encryptor() + return enc.update(plain_data) + enc.finalize() + + +def aes_ctr_decrypt(key: bytes, encrypted_data: bytes, nonce: bytes) -> bytes: + """Decrypt encrypted data with AES in CTR mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :param nonce: Nonce data with counter value + :return: Decrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.CTR(nonce)) + enc = cipher.decryptor() + return enc.update(encrypted_data) + enc.finalize() + + +def aes_xts_encrypt(key: bytes, plain_data: bytes, tweak: bytes) -> bytes: + """Encrypt plain data with AES in XTS mode. + + :param key: The key for data encryption + :param plain_data: Input data + :param tweak: The tweak is a 16 byte value + :return: Encrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.XTS(tweak)) + enc = cipher.encryptor() + return enc.update(plain_data) + enc.finalize() + + +def aes_xts_decrypt(key: bytes, encrypted_data: bytes, tweak: bytes) -> bytes: + """Decrypt encrypted data with AES in XTS mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :param tweak: The tweak is a 16 byte value + :return: Decrypted data + """ + cipher = Cipher(algorithms.AES(key), modes.XTS(tweak)) + enc = cipher.decryptor() + return enc.update(encrypted_data) + enc.finalize() + + +def aes_ccm_encrypt( + key: bytes, + plain_data: bytes, + nonce: bytes, + associated_data: bytes = b"", + tag_len: int = 16, +) -> bytes: + """Encrypt plain data with AES in CCM mode (Counter with CBC). + + :param key: The key for data encryption + :param plain_data: Input data + :param nonce: Nonce data with counter value + :param associated_data: Associated data - Unencrypted but authenticated + :param tag_len: Length of encryption tag + :return: Encrypted data + """ + aesccm = aead.AESCCM(key, tag_length=tag_len) + return aesccm.encrypt(nonce, plain_data, associated_data) + + +def aes_ccm_decrypt( + key: bytes, + encrypted_data: bytes, + nonce: bytes, + associated_data: bytes, + tag_len: int = 16, +) -> bytes: + """Decrypt encrypted data with AES in CCM mode (Counter with CBC). + + :param key: The key for data decryption + :param encrypted_data: Input data + :param nonce: Nonce data with counter value + :param associated_data: Associated data - Unencrypted but authenticated + :param tag_len: Length of encryption tag + :return: Decrypted data + """ + aesccm = aead.AESCCM(key, tag_length=tag_len) + return aesccm.decrypt(nonce, encrypted_data, associated_data) + + +def sm4_cbc_encrypt( + key: bytes, plain_data: bytes, iv_data: Optional[bytes] = None +) -> bytes: + """Encrypt plain data with SM4 in CBC mode. + + :param key: The key for data encryption + :param plain_data: Input data + :param iv_data: Initialization vector data + :raises SPSDKError: Invalid Key or IV + :return: Encrypted image + """ + if len(key) * 8 not in algorithms.SM4.key_sizes: + raise SPSDKError( + "The key must be a valid SM4 key length: " + f"{', '.join([str(k) for k in algorithms.SM4.key_sizes])}" + ) + init_vector = iv_data or bytes(algorithms.SM4.block_size // 8) + if len(init_vector) * 8 != algorithms.SM4.block_size: + raise SPSDKError( + f"The initial vector length must be {algorithms.SM4.block_size // 8}" + ) + cipher = Cipher(algorithms.SM4(key), modes.CBC(init_vector)) + enc = cipher.encryptor() + return ( + enc.update(align_block(plain_data, alignment=algorithms.SM4.block_size // 8)) + + enc.finalize() + ) + + +def sm4_cbc_decrypt( + key: bytes, encrypted_data: bytes, iv_data: Optional[bytes] = None +) -> bytes: + """Decrypt encrypted data with SM4 in CBC mode. + + :param key: The key for data decryption + :param encrypted_data: Input data + :param iv_data: Initialization vector data + :raises SPSDKError: Invalid Key or IV + :return: Decrypted image + """ + if len(key) * 8 not in algorithms.SM4.key_sizes: + raise SPSDKError( + "The key must be a valid SM4 key length: " + f"{', '.join([str(k) for k in algorithms.AES.key_sizes])}" + ) + init_vector = iv_data or bytes(algorithms.SM4.block_size) + if len(init_vector) * 8 != algorithms.SM4.block_size: + raise SPSDKError( + f"The initial vector length must be {algorithms.SM4.block_size}" + ) + cipher = Cipher(algorithms.SM4(key), modes.CBC(init_vector)) + dec = cipher.decryptor() + return dec.update(encrypted_data) + dec.finalize() diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/types.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/types.py new file mode 100644 index 0000000..ec5dd5b --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/types.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Based crypto classes.""" +from typing import Dict + +from cryptography import utils +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.x509.base import Version +from cryptography.x509.extensions import Extensions, KeyUsage +from cryptography.x509.name import Name +from cryptography.x509.oid import ExtensionOID, NameOID, ObjectIdentifier + +from ..exceptions import SPSDKError + + +class SPSDKEncoding(utils.Enum): + """Extension of cryptography Encoders class.""" + + NXP = "NXP" + PEM = "PEM" + DER = "DER" + + @staticmethod + def get_cryptography_encodings(encoding: "SPSDKEncoding") -> Encoding: + """Get Encoding in cryptography class.""" + cryptography_encoding = { + SPSDKEncoding.PEM: Encoding.PEM, + SPSDKEncoding.DER: Encoding.DER, + }.get(encoding) + if cryptography_encoding is None: + raise SPSDKError(f"{encoding} format is not supported by cryptography.") + return cryptography_encoding + + @staticmethod + def get_file_encodings(data: bytes) -> "SPSDKEncoding": + """Get the encoding type out of given item from the data. + + :param data: Already loaded data file to determine the encoding style + :return: encoding type (Encoding.PEM, Encoding.DER) + """ + encoding = SPSDKEncoding.PEM + try: + decoded = data.decode("utf-8") + except UnicodeDecodeError: + encoding = SPSDKEncoding.DER + else: + if decoded.find("----") == -1: + encoding = SPSDKEncoding.DER + return encoding + + @staticmethod + def all() -> Dict[str, "SPSDKEncoding"]: + """Get all supported encodings.""" + return { + "NXP": SPSDKEncoding.NXP, + "PEM": SPSDKEncoding.PEM, + "DER": SPSDKEncoding.DER, + } + + +SPSDKExtensions = Extensions +SPSDKExtensionOID = ExtensionOID +SPSDKNameOID = NameOID +SPSDKKeyUsage = KeyUsage +SPSDKName = Name +SPSDKVersion = Version +SPSDKObjectIdentifier = ObjectIdentifier diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/utils.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/utils.py new file mode 100644 index 0000000..72cd5f0 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/crypto/utils.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""OpenSSL implementation for security backend.""" + +from typing import Iterable, List, Optional + +from ..crypto.certificate import Certificate +from ..crypto.keys import PrivateKey, PublicKey +from ..exceptions import SPSDKError +from ..utils.misc import load_binary + + +def extract_public_key_from_data( + object_data: bytes, password: Optional[str] = None +) -> PublicKey: + """Extract any kind of public key from a data that contains Certificate, Private Key or Public Key. + + :raises SPSDKError: Raised when file can not be loaded + :return: private key of any type + """ + try: + return Certificate.parse(object_data).get_public_key() + except SPSDKError: + pass + + try: + return PrivateKey.parse( + object_data, password=password if password else None + ).get_public_key() + except SPSDKError: + pass + + try: + return PublicKey.parse(object_data) + except SPSDKError as exc: + raise SPSDKError("Unable to load secret data.") from exc + + +def extract_public_key( + file_path: str, + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, +) -> PublicKey: + """Extract any kind of public key from a file that contains Certificate, Private Key or Public Key. + + :param file_path: File path to public key file. + :param password: Optional password for encrypted Private file source. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Raised when file can not be loaded + :return: Public key of any type + """ + try: + object_data = load_binary(file_path, search_paths=search_paths) + return extract_public_key_from_data(object_data, password) + except SPSDKError as exc: + raise SPSDKError(f"Unable to load secret file '{file_path}'.") from exc + + +def extract_public_keys( + secret_files: Iterable[str], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, +) -> List[PublicKey]: + """Extract any kind of public key from files that contain Certificate, Private Key or Public Key. + + :param secret_files: List of file paths to public key files. + :param password: Optional password for encrypted Private file source. + :param search_paths: List of paths where to search for the file, defaults to None + :return: List of public keys of any type + """ + return [ + extract_public_key( + file_path=source, password=password, search_paths=search_paths + ) + for source in secret_files + ] diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/exceptions.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/exceptions.py new file mode 100644 index 0000000..99a0e96 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/exceptions.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Base for SPSDK exceptions.""" +from typing import Optional + +####################################################################### +# # Secure Provisioning SDK Exceptions +####################################################################### + + +class SPSDKError(Exception): + """Secure Provisioning SDK Base Exception.""" + + fmt = "SPSDK: {description}" + + def __init__(self, desc: Optional[str] = None) -> None: + """Initialize the base SPSDK Exception.""" + super().__init__() + self.description = desc + + def __str__(self) -> str: + return self.fmt.format(description=self.description or "Unknown Error") + + +class SPSDKKeyError(SPSDKError, KeyError): + """SPSDK standard key error.""" + + +class SPSDKValueError(SPSDKError, ValueError): + """SPSDK standard value error.""" + + +class SPSDKTypeError(SPSDKError, TypeError): + """SPSDK standard type error.""" + + +class SPSDKIOError(SPSDKError, IOError): + """SPSDK standard IO error.""" + + +class SPSDKNotImplementedError(SPSDKError, NotImplementedError): + """SPSDK standard not implemented error.""" + + +class SPSDKLengthError(SPSDKError, ValueError): + """SPSDK parsing error of any AHAB containers. + + Input/output data must be of at least container declared length bytes long. + """ + + +class SPSDKOverlapError(SPSDKError, ValueError): + """Data overlap error.""" + + +class SPSDKAlignmentError(SPSDKError, ValueError): + """Data improperly aligned.""" + + +class SPSDKParsingError(SPSDKError): + """Cannot parse binary data.""" + + +class SPSDKCorruptedException(SPSDKError): + """Corrupted Exception.""" + + +class SPSDKUnsupportedOperation(SPSDKError): + """SPSDK unsupported operation error.""" + + +class SPSDKSyntaxError(SyntaxError, SPSDKError): + """SPSDK syntax error.""" + + +class SPSDKFileNotFoundError(FileNotFoundError, SPSDKError): + """SPSDK file not found error.""" + + +class SPSDKAttributeError(SPSDKError, AttributeError): + """SPSDK standard attribute error.""" + + +class SPSDKConnectionError(SPSDKError, ConnectionError): + """SPSDK standard connection error.""" + + +class SPSDKIndexError(SPSDKError, IndexError): + """SPSDK standard index error.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/__init__.py new file mode 100644 index 0000000..b74e48f --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module implementing communication with the MCU Bootloader.""" +# diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/commands.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/commands.py new file mode 100644 index 0000000..0a5b86d --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/commands.py @@ -0,0 +1,525 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Commands and responses used by MBOOT module.""" + +from struct import pack, unpack, unpack_from +from typing import Dict, List, Optional, Type + +from ..utils.interfaces.commands import CmdPacketBase, CmdResponseBase +from ..utils.spsdk_enum import SpsdkEnum +from .error_codes import StatusCode +from .exceptions import McuBootError + +######################################################################################################################## +# McuBoot Commands and Responses Tags +######################################################################################################################## + + +# fmt: off +class CommandTag(SpsdkEnum): + """McuBoot Commands.""" + + NO_COMMAND = (0x00, "NoCommand", "No Command") + FLASH_ERASE_ALL = (0x01, "FlashEraseAll", "Erase Complete Flash") + FLASH_ERASE_REGION = (0x02, "FlashEraseRegion", "Erase Flash Region") + READ_MEMORY = (0x03, "ReadMemory", "Read Memory") + WRITE_MEMORY = (0x04, "WriteMemory", "Write Memory") + FILL_MEMORY = (0x05, "FillMemory", "Fill Memory") + FLASH_SECURITY_DISABLE = (0x06, "FlashSecurityDisable", "Disable Flash Security") + GET_PROPERTY = (0x07, "GetProperty", "Get Property") + RECEIVE_SB_FILE = (0x08, "ReceiveSBFile", "Receive SB File") + EXECUTE = (0x09, "Execute", "Execute") + CALL = (0x0A, "Call", "Call") + RESET = (0x0B, "Reset", "Reset MCU") + SET_PROPERTY = (0x0C, "SetProperty", "Set Property") + FLASH_ERASE_ALL_UNSECURE = (0x0D, "FlashEraseAllUnsecure", "Erase Complete Flash and Unlock") + FLASH_PROGRAM_ONCE = (0x0E, "FlashProgramOnce", "Flash Program Once") + FLASH_READ_ONCE = (0x0F, "FlashReadOnce", "Flash Read Once") + FLASH_READ_RESOURCE = (0x10, "FlashReadResource", "Flash Read Resource") + CONFIGURE_MEMORY = (0x11, "ConfigureMemory", "Configure Quad-SPI Memory") + RELIABLE_UPDATE = (0x12, "ReliableUpdate", "Reliable Update") + GENERATE_KEY_BLOB = (0x13, "GenerateKeyBlob", "Generate Key Blob") + FUSE_PROGRAM = (0x14, "ProgramFuse", "Program Fuse") + KEY_PROVISIONING = (0x15, "KeyProvisioning", "Key Provisioning") + TRUST_PROVISIONING = (0x16, "TrustProvisioning", "Trust Provisioning") + FUSE_READ = (0x17, "ReadFuse", "Read Fuse") + UPDATE_LIFE_CYCLE = (0x18, "UpdateLifeCycle", "Update Life Cycle") + ELE_MESSAGE = (0x19, "EleMessage", "Send EdgeLock Enclave Message") + + # reserved commands + CONFIGURE_I2C = (0xC1, "ConfigureI2c", "Configure I2C") + CONFIGURE_SPI = (0xC2, "ConfigureSpi", "Configure SPI") + CONFIGURE_CAN = (0xC3, "ConfigureCan", "Configure CAN") + + +class CommandFlag(SpsdkEnum): + """Flags for McuBoot commands.""" + + NONE = (0, "NoFlags", "No flags specified") + HAS_DATA_PHASE = (1, "DataPhase", "Command has a data phase") + + +class ResponseTag(SpsdkEnum): + """McuBoot Responses to Commands.""" + + GENERIC = (0xA0, "GenericResponse", "Generic Response") + READ_MEMORY = (0xA3, "ReadMemoryResponse", "Read Memory Response") + GET_PROPERTY = (0xA7, "GetPropertyResponse", "Get Property Response") + FLASH_READ_ONCE = (0xAF, "FlashReadOnceResponse", "Flash Read Once Response") + FLASH_READ_RESOURCE = (0xB0, "FlashReadResourceResponse", "Flash Read Resource Response") + KEY_BLOB_RESPONSE = (0xB3, "CreateKeyBlobResponse", "Create Key Blob") + KEY_PROVISIONING_RESPONSE = (0xB5, "KeyProvisioningResponse", "Key Provisioning Response") + TRUST_PROVISIONING_RESPONSE = (0xB6, "TrustProvisioningResponse", "Trust Provisioning Response") + + +class KeyProvOperation(SpsdkEnum): + """Type of key provisioning operation.""" + + ENROLL = (0, "Enroll", "Enroll Operation") + SET_USER_KEY = (1, "SetUserKey", "Set User Key Operation") + SET_INTRINSIC_KEY = (2, "SetIntrinsicKey", "Set Intrinsic Key Operation") + WRITE_NON_VOLATILE = (3, "WriteNonVolatile", "Write Non Volatile Operation") + READ_NON_VOLATILE = (4, "ReadNonVolatile", "Read Non Volatile Operation") + WRITE_KEY_STORE = (5, "WriteKeyStore", "Write Key Store Operation") + READ_KEY_STORE = (6, "ReadKeyStore", "Read Key Store Operation") + + +class KeyProvUserKeyType(SpsdkEnum): + """Enumeration of supported user keys in PUF. Keys are SoC specific, not all will be supported for the processor.""" + + OTFADKEK = (2, "OTFADKEK", "Key for OTFAD encryption") + SBKEK = (3, "SBKEK", "Key for SB file encryption") + PRINCE_REGION_0 = (7, "PRINCE0", "Key for Prince region 0") + PRINCE_REGION_1 = (8, "PRINCE1", "Key for Prince region 1") + PRINCE_REGION_2 = (9, "PRINCE2", "Key for Prince region 2") + PRINCE_REGION_3 = (10, "PRINCE3", "Key for Prince region 3") + + USERKEK = (11, "USERKEK", "Encrypted boot image key") + UDS = (12, "UDS", "Universal Device Secret for DICE") + + +class GenerateKeyBlobSelect(SpsdkEnum): + """Key selector for the generate-key-blob function. + + For devices with SNVS, valid options of [key_sel] are + 0, 1 or OTPMK: OTPMK from FUSE or OTP(default), + 2 or ZMK: ZMK from SNVS, + 3 or CMK: CMK from SNVS, + For devices without SNVS, this option will be ignored. + """ + + OPTMK = (0, "OPTMK", "OTPMK from FUSE or OTP(default)") + ZMK = (2, "ZMK", "ZMK from SNVS") + CMK = (3, "CMK", "CMK from SNVS") + + +class TrustProvOperation(SpsdkEnum): + """Operations supported by Trust Provisioning flow.""" + + PROVE_GENUINITY = (0xF4, "ProveGenuinity", "Start the proving genuinity process") + ISP_SET_WRAPPED_DATA = (0xF0, "SetWrappedData", "Start processing Wrapped data") + """Type of trust provisioning operation.""" + + OEM_GEN_MASTER_SHARE = (0, "OemGenMasterShare", "Enroll Operation") + OEM_SET_MASTER_SHARE = (1, "SetUserKey", "Set User Key Operation") + OEM_GET_CUST_CERT_DICE_PUK = (2, "SetIntrinsicKey", "Set Intrinsic Key Operation") + HSM_GEN_KEY = (3, "HsmGenKey", "HSM gen key") + HSM_STORE_KEY = (4, "HsmStoreKey", "HSM store key") + HSM_ENC_BLOCK = (5, "HsmEncBlock", "HSM Enc block") + HSM_ENC_SIGN = (6, "HsnEncSign", "HSM enc sign") + + +class TrustProvOemKeyType(SpsdkEnum): + """Type of oem key type definition.""" + + MFWISK = (0xC3A5, "MFWISK", "ECDSA Manufacturing Firmware Signing Key") + MFWENCK = (0xA5C3, "MFWENCK", "CKDF Master Key for Manufacturing Firmware Encryption Key") + GENSIGNK = (0x5A3C, "GENSIGNK", "Generic ECDSA Signing Key") + GETCUSTMKSK = (0x3C5A, "GETCUSTMKSK", "CKDF Master Key for Production Firmware Encryption Key") + + +class TrustProvKeyType(SpsdkEnum): + """Type of key type definition.""" + + CKDFK = (1, "CKDFK", "CKDF Master Key") + HKDFK = (2, "HKDFK", "HKDF Master Key") + HMACK = (3, "HMACK", "HMAC Key") + CMACK = (4, "CMACK", "CMAC Key") + AESK = (5, "AESK", "AES Key") + KUOK = (6, "KUOK", "Key Unwrap Only Key") + + +class TrustProvWrappingKeyType(SpsdkEnum): + """Type of wrapping key type definition.""" + + INT_SK = (0x10, "INT_SK", "The wrapping key for wrapping of MFG_CUST_MK_SK0_BLOB") + EXT_SK = (0x11, "EXT_SK", "The wrapping key for wrapping of MFG_CUST_MK_SK0_BLOB") + + +class TrustProvWpc(SpsdkEnum): + """Type of WPC trusted facility commands for DSC.""" + + WPC_GET_ID = (0x5000000, "wpc_get_id", "WPC get ID") + NXP_GET_ID = (0x5000001, "nxp_get_id", "NXP get ID") + WPC_INSERT_CERT = (0x5000002, "wpc_insert_cert", "WPC insert certificate") + WPC_SIGN_CSR = (0x5000003, "wpc_sign_csr", "WPC sign CSR") + + +class TrustProvDevHsmDsc(SpsdkEnum): + """Type of DSC Device HSM.""" + + DSC_HSM_CREATE_SESSION = (0x6000000, "dsc_hsm_create_session", "DSC HSM create session") + DSC_HSM_ENC_BLK = (0x6000001, "dsc_hsm_enc_blk", "DSC HSM encrypt bulk") + DSC_HSM_ENC_SIGN = (0x6000002, "dsc_hsm_enc_sign", "DSC HSM sign") + +# fmt: on + +######################################################################################################################## +# McuBoot Command and Response packet classes +######################################################################################################################## + + +class CmdHeader: + """McuBoot command/response header.""" + + SIZE = 4 + + def __init__(self, tag: int, flags: int, reserved: int, params_count: int) -> None: + """Initialize the Command Header. + + :param tag: Tag indicating the command, see: `CommandTag` class + :param flags: Flags for the command + :param reserved: Reserved? + :param params_count: Number of parameter for the command + """ + self.tag = tag + self.flags = flags + self.reserved = reserved + self.params_count = params_count + + def __eq__(self, obj: object) -> bool: + return isinstance(obj, CmdHeader) and vars(obj) == vars(self) + + def __ne__(self, obj: object) -> bool: + return not self.__eq__(obj) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return ( + f"CmdHeader(tag=0x{self.tag:02X}, flags=0x{self.flags:02X}, " + f"reserved={self.reserved}, params_count={self.params_count})" + ) + + def to_bytes(self) -> bytes: + """Serialize header into bytes.""" + return pack("4B", self.tag, self.flags, self.reserved, self.params_count) + + @classmethod + def from_bytes(cls, data: bytes, offset: int = 0) -> "CmdHeader": + """Deserialize header from bytes. + + :param data: Input data in bytes + :param offset: The offset of input data + :return: De-serialized CmdHeader object + :raises McuBootError: Invalid data format + """ + if len(data) < 4: + raise McuBootError( + f"Invalid format of RX packet (data length is {len(data)} bytes)" + ) + return cls(*unpack_from("4B", data, offset)) + + +class CmdPacket(CmdPacketBase): + """McuBoot command packet format class.""" + + SIZE = 32 + EMPTY_VALUE = 0x00 + + def __init__( + self, tag: CommandTag, flags: int, *args: int, data: Optional[bytes] = None + ) -> None: + """Initialize the Command Packet object. + + :param tag: Tag identifying the command + :param flags: Flags used by the command + :param args: Arguments used by the command + :param data: Additional data, defaults to None + """ + self.header = CmdHeader(tag.tag, flags, 0, len(args)) + self.params = list(args) + if data is not None: + if len(data) % 4: + data += b"\0" * (4 - len(data) % 4) + self.params.extend(unpack_from(f"<{len(data) // 4}I", data)) + self.header.params_count = len(self.params) + + def __eq__(self, obj: object) -> bool: + return isinstance(obj, CmdPacket) and vars(obj) == vars(self) + + def __ne__(self, obj: object) -> bool: + return not self.__eq__(obj) + + def __str__(self) -> str: + """Get object info.""" + tag = ( + CommandTag.get_label(self.header.tag) + if self.header.tag in CommandTag.tags() + else f"0x{self.header.tag:02X}" + ) + return f"Tag={tag}, Flags=0x{self.header.flags:02X}" + "".join( + f", P[{n}]=0x{param:08X}" for n, param in enumerate(self.params) + ) + + def to_bytes(self, padding: bool = True) -> bytes: + """Serialize CmdPacket into bytes. + + :param padding: If True, add padding to specific size + :return: Serialized object into bytes + """ + self.header.params_count = len(self.params) + data = self.header.to_bytes() + data += pack(f"<{self.header.params_count}I", *self.params) + if padding and len(data) < self.SIZE: + data += bytes([self.EMPTY_VALUE] * (self.SIZE - len(data))) + return data + + +class CmdResponse(CmdResponseBase): + """McuBoot response base format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Command Response object. + + :param header: Header for the response + :param raw_data: Response data + """ + assert isinstance(header, CmdHeader) + assert isinstance(raw_data, (bytes, bytearray)) + self.header = header + self.raw_data = raw_data + (status,) = unpack_from(" int: + """Return a integer representation of the response.""" + value = unpack_from(">I", self.raw_data)[0] + assert isinstance(value, int) + return value + + def _get_status_label(self) -> str: + return ( + StatusCode.get_label(self.status) + if self.status in StatusCode.tags() + else f"Unknown[0x{self.status:08X}]" + ) + + def __eq__(self, obj: object) -> bool: + return isinstance(obj, CmdResponse) and vars(obj) == vars(self) + + def __ne__(self, obj: object) -> bool: + return not self.__eq__(obj) + + def __str__(self) -> str: + """Get object info.""" + return ( + f"Tag=0x{self.header.tag:02X}, Flags=0x{self.header.flags:02X}" + + " [" + + ", ".join(f"{b:02X}" for b in self.raw_data) + + "]" + ) + + +class GenericResponse(CmdResponse): + """McuBoot generic response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Generic response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, tag = unpack_from("<2I", raw_data) + self.cmd_tag: int = tag + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + cmd = ( + CommandTag.get_label(self.cmd_tag) + if self.cmd_tag in CommandTag.tags() + else f"Unknown[0x{self.cmd_tag:02X}]" + ) + return f"Tag={tag}, Status={status}, Cmd={cmd}" + + +class GetPropertyResponse(CmdResponse): + """McuBoot get property response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Get-Property response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, *values = unpack_from(f"<{self.header.params_count}I", raw_data) + self.values: List[int] = list(values) + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}" + "".join( + f", v{n}=0x{value:08X}" for n, value in enumerate(self.values) + ) + + +class ReadMemoryResponse(CmdResponse): + """McuBoot read memory response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Read-Memory response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, length = unpack_from("<2I", raw_data) + self.length: int = length + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}, Length={self.length}" + + +class FlashReadOnceResponse(CmdResponse): + """McuBoot flash read once response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Flash-Read-Once response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, length, *values = unpack_from(f"<{self.header.params_count}I", raw_data) + self.length: int = length + self.values: List[int] = list(values) + self.data = raw_data[8 : 8 + self.length] if self.length > 0 else b"" + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}, Length={self.length}" + + +class FlashReadResourceResponse(CmdResponse): + """McuBoot flash read resource response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Flash-Read-Resource response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, length = unpack_from("<2I", raw_data) + self.length: int = length + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}, Length={self.length}" + + +class KeyProvisioningResponse(CmdResponse): + """McuBoot Key Provisioning response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Key-Provisioning response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, length = unpack_from("<2I", raw_data) + self.length: int = length + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}, Length={self.length}" + + +class TrustProvisioningResponse(CmdResponse): + """McuBoot Trust Provisioning response format class.""" + + def __init__(self, header: CmdHeader, raw_data: bytes) -> None: + """Initialize the Trust-Provisioning response object. + + :param header: Header for the response + :param raw_data: Response data + """ + super().__init__(header, raw_data) + _, *values = unpack(f"<{self.header.params_count}I", raw_data) + self.values: List[int] = list(values) + + def __str__(self) -> str: + """Get object info.""" + tag = ResponseTag.get_label(self.header.tag) + status = self._get_status_label() + return f"Tag={tag}, Status={status}" + + +class NoResponse(CmdResponse): + """Special internal case when no response is provided by the target.""" + + def __init__(self, cmd_tag: int) -> None: + """Create a NoResponse to an command that was issued, indicated by its tag. + + :param cmd_tag: Tag of the command that preceded the no-response from target + """ + header = CmdHeader(tag=cmd_tag, flags=0, reserved=0, params_count=0) + raw_data = pack(" CmdResponse: + """Parse command response. + + :param data: Input data in bytes + :param offset: The offset of input data + :return: De-serialized object from data + """ + known_response: Dict[int, Type[CmdResponse]] = { + ResponseTag.GENERIC.tag: GenericResponse, + ResponseTag.GET_PROPERTY.tag: GetPropertyResponse, + ResponseTag.READ_MEMORY.tag: ReadMemoryResponse, + ResponseTag.FLASH_READ_RESOURCE.tag: FlashReadResourceResponse, + ResponseTag.FLASH_READ_ONCE.tag: FlashReadOnceResponse, + ResponseTag.KEY_BLOB_RESPONSE.tag: ReadMemoryResponse, + ResponseTag.KEY_PROVISIONING_RESPONSE.tag: KeyProvisioningResponse, + ResponseTag.TRUST_PROVISIONING_RESPONSE.tag: TrustProvisioningResponse, + } + header = CmdHeader.from_bytes(data, offset) + if header.tag in known_response: + return known_response[header.tag](header, data[CmdHeader.SIZE :]) + + return CmdResponse(header, data[CmdHeader.SIZE :]) diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/error_codes.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/error_codes.py new file mode 100644 index 0000000..9afe9b6 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/error_codes.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Status and error codes used by the MBoot protocol.""" + +from ..utils.spsdk_enum import SpsdkEnum + +######################################################################################################################## +# McuBoot Status Codes (Errors) +######################################################################################################################## + + +# pylint: disable=line-too-long +# fmt: off +class StatusCode(SpsdkEnum): + """McuBoot status codes.""" + + SUCCESS = (0, "Success", "Success") + FAIL = (1, "Fail", "Fail") + READ_ONLY = (2, "ReadOnly", "Read Only Error") + OUT_OF_RANGE = (3, "OutOfRange", "Out Of Range Error") + INVALID_ARGUMENT = (4, "InvalidArgument", "Invalid Argument Error") + TIMEOUT = (5, "TimeoutError", "Timeout Error") + NO_TRANSFER_IN_PROGRESS = (6, "NoTransferInProgress", "No Transfer In Progress Error") + + # Flash driver errors. + FLASH_SIZE_ERROR = (100, "FlashSizeError", "FLASH Driver: Size Error") + FLASH_ALIGNMENT_ERROR = (101, "FlashAlignmentError", "FLASH Driver: Alignment Error") + FLASH_ADDRESS_ERROR = (102, "FlashAddressError", "FLASH Driver: Address Error") + FLASH_ACCESS_ERROR = (103, "FlashAccessError", "FLASH Driver: Access Error") + FLASH_PROTECTION_VIOLATION = (104, "FlashProtectionViolation", "FLASH Driver: Protection Violation") + FLASH_COMMAND_FAILURE = (105, "FlashCommandFailure", "FLASH Driver: Command Failure") + FLASH_UNKNOWN_PROPERTY = (106, "FlashUnknownProperty", "FLASH Driver: Unknown Property") + FLASH_ERASE_KEY_ERROR = (107, "FlashEraseKeyError", "FLASH Driver: Provided Key Does Not Match Programmed Flash Memory Key") + FLASH_REGION_EXECUTE_ONLY = (108, "FlashRegionExecuteOnly", "FLASH Driver: Region Execute Only") + FLASH_EXEC_IN_RAM_NOT_READY = (109, "FlashExecuteInRamFunctionNotReady", "FLASH Driver: Execute In RAM Function Not Ready") + FLASH_COMMAND_NOT_SUPPORTED = (111, "FlashCommandNotSupported", "FLASH Driver: Command Not Supported") + FLASH_READ_ONLY_PROPERTY = (112, "FlashReadOnlyProperty", "FLASH Driver: Flash Memory Property Is Read-Only") + FLASH_INVALID_PROPERTY_VALUE = (113, "FlashInvalidPropertyValue", "FLASH Driver: Flash Memory Property Value Out Of Range") + FLASH_INVALID_SPECULATION_OPTION = (114, "FlashInvalidSpeculationOption", "FLASH Driver: Flash Memory Prefetch Speculation Option Is Invalid") + FLASH_ECC_ERROR = (116, "FlashEccError", "FLASH Driver: ECC Error") + FLASH_COMPARE_ERROR = (117, "FlashCompareError", "FLASH Driver: Destination And Source Memory Contents Do Not Match") + FLASH_REGULATION_LOSS = (118, "FlashRegulationLoss", "FLASH Driver: Loss Of Regulation During Read") + FLASH_INVALID_WAIT_STATE_CYCLES = (119, "FlashInvalidWaitStateCycles", "FLASH Driver: Wait State Cycle Set To Read/Write Mode Is Invalid") + FLASH_OUT_OF_DATE_CFPA_PAGE = (132, "FlashOutOfDateCfpaPage", "FLASH Driver: Out Of Date CFPA Page") + FLASH_BLANK_IFR_PAGE_DATA = (133, "FlashBlankIfrPageData", "FLASH Driver: Blank IFR Page Data") + FLASH_ENCRYPTED_REGIONS_ERASE_NOT_DONE_AT_ONCE = (134, "FlashEncryptedRegionsEraseNotDoneAtOnce", "FLASH Driver: Encrypted Regions Erase Not Done At Once") + FLASH_PROGRAM_VERIFICATION_NOT_ALLOWED = (135, "FlashProgramVerificationNotAllowed", "FLASH Driver: Program Verification Not Allowed") + FLASH_HASH_CHECK_ERROR = (136, "FlashHashCheckError", "FLASH Driver: Hash Check Error") + FLASH_SEALED_PFR_REGION = (137, "FlashSealedPfrRegion", "FLASH Driver: Sealed PFR Region") + FLASH_PFR_REGION_WRITE_BROKEN = (138, "FlashPfrRegionWriteBroken", "FLASH Driver: PFR Region Write Broken") + FLASH_NMPA_UPDATE_NOT_ALLOWED = (139, "FlashNmpaUpdateNotAllowed", "FLASH Driver: NMPA Update Not Allowed") + FLASH_CMPA_CFG_DIRECT_ERASE_NOT_ALLOWED = (140, "FlashCmpaCfgDirectEraseNotAllowed", "FLASH Driver: CMPA Cfg Direct Erase Not Allowed") + FLASH_PFR_BANK_IS_LOCKED = (141, "FlashPfrBankIsLocked", "FLASH Driver: PFR Bank Is Locked") + FLASH_CFPA_SCRATCH_PAGE_INVALID = (148, "FlashCfpaScratchPageInvalid", "FLASH Driver: CFPA Scratch Page Invalid") + FLASH_CFPA_VERSION_ROLLBACK_DISALLOWED = (149, "FlashCfpaVersionRollbackDisallowed", "FLASH Driver: CFPA Version Rollback Disallowed") + FLASH_READ_HIDING_AREA_DISALLOWED = (150, "FlashReadHidingAreaDisallowed", "FLASH Driver: Flash Memory Hiding Read Not Allowed") + FLASH_MODIFY_PROTECTED_AREA_DISALLOWED = (151, "FlashModifyProtectedAreaDisallowed", "FLASH Driver: Flash Firewall Page Locked Erase And Program Are Not Allowed") + FLASH_COMMAND_OPERATION_IN_PROGRESS = (152, "FlashCommandOperationInProgress", "FLASH Driver: Flash Memory State Busy Flash Memory Command Is In Progress") + + # I2C driver errors. + I2C_SLAVE_TX_UNDERRUN = (200, "I2cSlaveTxUnderrun", "I2C Driver: Slave Tx Underrun") + I2C_SLAVE_RX_OVERRUN = (201, "I2cSlaveRxOverrun", "I2C Driver: Slave Rx Overrun") + I2C_ARBITRATION_LOST = (202, "I2cArbitrationLost", "I2C Driver: Arbitration Lost") + + # SPI driver errors. + SPI_SLAVE_TX_UNDERRUN = (300, "SpiSlaveTxUnderrun", "SPI Driver: Slave Tx Underrun") + SPI_SLAVE_RX_OVERRUN = (301, "SpiSlaveRxOverrun", "SPI Driver: Slave Rx Overrun") + + # QuadSPI driver errors. + QSPI_FLASH_SIZE_ERROR = (400, "QspiFlashSizeError", "QSPI Driver: Flash Size Error") + QSPI_FLASH_ALIGNMENT_ERROR = (401, "QspiFlashAlignmentError", "QSPI Driver: Flash Alignment Error") + QSPI_FLASH_ADDRESS_ERROR = (402, "QspiFlashAddressError", "QSPI Driver: Flash Address Error") + QSPI_FLASH_COMMAND_FAILURE = (403, "QspiFlashCommandFailure", "QSPI Driver: Flash Command Failure") + QSPI_FLASH_UNKNOWN_PROPERTY = (404, "QspiFlashUnknownProperty", "QSPI Driver: Flash Unknown Property") + QSPI_NOT_CONFIGURED = (405, "QspiNotConfigured", "QSPI Driver: Not Configured") + QSPI_COMMAND_NOT_SUPPORTED = (406, "QspiCommandNotSupported", "QSPI Driver: Command Not Supported") + QSPI_COMMAND_TIMEOUT = (407, "QspiCommandTimeout", "QSPI Driver: Command Timeout") + QSPI_WRITE_FAILURE = (408, "QspiWriteFailure", "QSPI Driver: Write Failure") + + # OTFAD driver errors. + OTFAD_SECURITY_VIOLATION = (500, "OtfadSecurityViolation", "OTFAD Driver: Security Violation") + OTFAD_LOGICALLY_DISABLED = (501, "OtfadLogicallyDisabled", "OTFAD Driver: Logically Disabled") + OTFAD_INVALID_KEY = (502, "OtfadInvalidKey", "OTFAD Driver: Invalid Key") + OTFAD_INVALID_KEY_BLOB = (503, "OtfadInvalidKeyBlob", "OTFAD Driver: Invalid Key Blob") + + # Sending errors. + SENDING_OPERATION_CONDITION_ERROR = (1812, "SendOperationConditionError", "Send Operation Condition failed") + + # SDMMC driver errors. + + # FlexSPI statuses. + FLEXSPI_SEQUENCE_EXECUTION_TIMEOUT_RT5xx = (6000, "FLEXSPI_SequenceExecutionTimeout_RT5xx", "FLEXSPI: Sequence Execution Timeout") + FLEXSPI_INVALID_SEQUENCE_RT5xx = (6001, "FLEXSPI_InvalidSequence_RT5xx", "FLEXSPI: Invalid Sequence") + FLEXSPI_DEVICE_TIMEOUT_RT5xx = (6002, "FLEXSPI_DeviceTimeout_RT5xx", "FLEXSPI: Device Timeout") + FLEXSPI_SEQUENCE_EXECUTION_TIMEOUT = (7000, "FLEXSPI_SequenceExecutionTimeout", "FLEXSPI: Sequence Execution Timeout") + FLEXSPI_INVALID_SEQUENCE = (7001, "FLEXSPI_InvalidSequence", "FLEXSPI: Invalid Sequence") + FLEXSPI_DEVICE_TIMEOUT = (7002, "FLEXSPI_DeviceTimeout", "FLEXSPI: Device Timeout") + + # Bootloader errors. + UNKNOWN_COMMAND = (10000, "UnknownCommand", "Unknown Command") + SECURITY_VIOLATION = (10001, "SecurityViolation", "Security Violation") + ABORT_DATA_PHASE = (10002, "AbortDataPhase", "Abort Data Phase") + PING_ERROR = (10003, "PingError", "Ping Error") + NO_RESPONSE = (10004, "NoResponse", "No response packet from target device") + NO_RESPONSE_EXPECTED = (10005, "NoResponseExpected", "No Response Expected") + UNSUPPORTED_COMMAND = (10006, "UnsupportedCommand", "Unsupported Command") + + # SB loader errors. + ROMLDR_SECTION_OVERRUN = (10100, "RomLdrSectionOverrun", "ROM Loader: Section Overrun") + ROMLDR_SIGNATURE = (10101, "RomLdrSignature", "ROM Loader: Signature Error") + ROMLDR_SECTION_LENGTH = (10102, "RomLdrSectionLength", "ROM Loader: Section Length Error") + ROMLDR_UNENCRYPTED_ONLY = (10103, "RomLdrUnencryptedOnly", "ROM Loader: Unencrypted Only") + ROMLDR_EOF_REACHED = (10104, "RomLdrEOFReached", "ROM Loader: EOF Reached") + ROMLDR_CHECKSUM = (10105, "RomLdrChecksum", "ROM Loader: Checksum Error") + ROMLDR_CRC32_ERROR = (10106, "RomLdrCrc32Error", "ROM Loader: CRC32 Error") + ROMLDR_UNKNOWN_COMMAND = (10107, "RomLdrUnknownCommand", "ROM Loader: Unknown Command") + ROMLDR_ID_NOT_FOUND = (10108, "RomLdrIdNotFound", "ROM Loader: ID Not Found") + ROMLDR_DATA_UNDERRUN = (10109, "RomLdrDataUnderrun", "ROM Loader: Data Underrun") + ROMLDR_JUMP_RETURNED = (10110, "RomLdrJumpReturned", "ROM Loader: Jump Returned") + ROMLDR_CALL_FAILED = (10111, "RomLdrCallFailed", "ROM Loader: Call Failed") + ROMLDR_KEY_NOT_FOUND = (10112, "RomLdrKeyNotFound", "ROM Loader: Key Not Found") + ROMLDR_SECURE_ONLY = (10113, "RomLdrSecureOnly", "ROM Loader: Secure Only") + ROMLDR_RESET_RETURNED = (10114, "RomLdrResetReturned", "ROM Loader: Reset Returned") + ROMLDR_ROLLBACK_BLOCKED = (10115, "RomLdrRollbackBlocked", "ROM Loader: Rollback Blocked") + ROMLDR_INVALID_SECTION_MAC_COUNT = (10116, "RomLdrInvalidSectionMacCount", "ROM Loader: Invalid Section Mac Count") + ROMLDR_UNEXPECTED_COMMAND = (10117, "RomLdrUnexpectedCommand", "ROM Loader: Unexpected Command") + ROMLDR_BAD_SBKEK = (10118, "RomLdrBadSBKEK", "ROM Loader: Bad SBKEK Detected") + ROMLDR_PENDING_JUMP_COMMAND = (10119, "RomLdrPendingJumpCommand", "ROM Loader: Pending Jump Command") + + # Memory interface errors. + MEMORY_RANGE_INVALID = (10200, "MemoryRangeInvalid", "Memory Range Invalid") + MEMORY_READ_FAILED = (10201, "MemoryReadFailed", "Memory Read Failed") + MEMORY_WRITE_FAILED = (10202, "MemoryWriteFailed", "Memory Write Failed") + MEMORY_CUMULATIVE_WRITE = (10203, "MemoryCumulativeWrite", "Memory Cumulative Write") + MEMORY_APP_OVERLAP_WITH_EXECUTE_ONLY_REGION = (10204, "MemoryAppOverlapWithExecuteOnlyRegion", "Memory App Overlap with exec region") + MEMORY_NOT_CONFIGURED = (10205, "MemoryNotConfigured", "Memory Not Configured") + MEMORY_ALIGNMENT_ERROR = (10206, "MemoryAlignmentError", "Memory Alignment Error") + MEMORY_VERIFY_FAILED = (10207, "MemoryVerifyFailed", "Memory Verify Failed") + MEMORY_WRITE_PROTECTED = (10208, "MemoryWriteProtected", "Memory Write Protected") + MEMORY_ADDRESS_ERROR = (10209, "MemoryAddressError", "Memory Address Error") + MEMORY_BLANK_CHECK_FAILED = (10210, "MemoryBlankCheckFailed", "Memory Black Check Failed") + MEMORY_BLANK_PAGE_READ_DISALLOWED = (10211, "MemoryBlankPageReadDisallowed", "Memory Blank Page Read Disallowed") + MEMORY_PROTECTED_PAGE_READ_DISALLOWED = (10212, "MemoryProtectedPageReadDisallowed", "Memory Protected Page Read Disallowed") + MEMORY_PFR_SPEC_REGION_WRITE_BROKEN = (10213, "MemoryPfrSpecRegionWriteBroken", "Memory PFR Spec Region Write Broken") + MEMORY_UNSUPPORTED_COMMAND = (10214, "MemoryUnsupportedCommand", "Memory Unsupported Command") + + # Property store errors. + UNKNOWN_PROPERTY = (10300, "UnknownProperty", "Unknown Property") + READ_ONLY_PROPERTY = (10301, "ReadOnlyProperty", "Read Only Property") + INVALID_PROPERTY_VALUE = (10302, "InvalidPropertyValue", "Invalid Property Value") + + # Property store errors. + APP_CRC_CHECK_PASSED = (10400, "AppCrcCheckPassed", "Application CRC Check: Passed") + APP_CRC_CHECK_FAILED = (10401, "AppCrcCheckFailed", "Application: CRC Check: Failed") + APP_CRC_CHECK_INACTIVE = (10402, "AppCrcCheckInactive", "Application CRC Check: Inactive") + APP_CRC_CHECK_INVALID = (10403, "AppCrcCheckInvalid", "Application CRC Check: Invalid") + APP_CRC_CHECK_OUT_OF_RANGE = (10404, "AppCrcCheckOutOfRange", "Application CRC Check: Out Of Range") + + # Packetizer errors. + PACKETIZER_NO_PING_RESPONSE = (10500, "NoPingResponse", "Packetizer Error: No Ping Response") + PACKETIZER_INVALID_PACKET_TYPE = (10501, "InvalidPacketType", "Packetizer Error: No response received for ping command") + PACKETIZER_INVALID_CRC = (10502, "InvalidCRC", "Packetizer Error: Invalid packet type") + PACKETIZER_NO_COMMAND_RESPONSE = (10503, "NoCommandResponse", "Packetizer Error: No response received for command") + + # Reliable Update statuses. + RELIABLE_UPDATE_SUCCESS = (10600, "ReliableUpdateSuccess", "Reliable Update: Success") + RELIABLE_UPDATE_FAIL = (10601, "ReliableUpdateFail", "Reliable Update: Fail") + RELIABLE_UPDATE_INACTIVE = (10602, "ReliableUpdateInactive", "Reliable Update: Inactive") + RELIABLE_UPDATE_BACKUPAPPLICATIONINVALID = (10603, "ReliableUpdateBackupApplicationInvalid", "Reliable Update: Backup Application Invalid") + RELIABLE_UPDATE_STILLINMAINAPPLICATION = (10604, "ReliableUpdateStillInMainApplication", "Reliable Update: Still In Main Application") + RELIABLE_UPDATE_SWAPSYSTEMNOTREADY = (10605, "ReliableUpdateSwapSystemNotReady", "Reliable Update: Swap System Not Ready") + RELIABLE_UPDATE_BACKUPBOOTLOADERNOTREADY = (10606, "ReliableUpdateBackupBootloaderNotReady", "Reliable Update: Backup Bootloader Not Ready") + RELIABLE_UPDATE_SWAPINDICATORADDRESSINVALID = (10607, "ReliableUpdateSwapIndicatorAddressInvalid", "Reliable Update: Swap Indicator Address Invalid") + RELIABLE_UPDATE_SWAPSYSTEMNOTAVAILABLE = (10608, "ReliableUpdateSwapSystemNotAvailable", "Reliable Update: Swap System Not Available") + RELIABLE_UPDATE_SWAPTEST = (10609, "ReliableUpdateSwapTest", "Reliable Update: Swap Test") + + # Serial NOR/EEPROM statuses. + SERIAL_NOR_EEPROM_ADDRESS_INVALID = (10700, "SerialNorEepromAddressInvalid", "SerialNorEeprom: Address Invalid") + SERIAL_NOR_EEPROM_TRANSFER_ERROR = (10701, "SerialNorEepromTransferError", "SerialNorEeprom: Transfer Error") + SERIAL_NOR_EEPROM_TYPE_INVALID = (10702, "SerialNorEepromTypeInvalid", "SerialNorEeprom: Type Invalid") + SERIAL_NOR_EEPROM_SIZE_INVALID = (10703, "SerialNorEepromSizeInvalid", "SerialNorEeprom: Size Invalid") + SERIAL_NOR_EEPROM_COMMAND_INVALID = (10704, "SerialNorEepromCommandInvalid", "SerialNorEeprom: Command Invalid") + + # ROM API statuses. + ROM_API_NEED_MORE_DATA = (10800, "RomApiNeedMoreData", "RomApi: Need More Data") + ROM_API_BUFFER_SIZE_NOT_ENOUGH = (10801, "RomApiBufferSizeNotEnough", "RomApi: Buffer Size Not Enough") + ROM_API_INVALID_BUFFER = (10802, "RomApiInvalidBuffer", "RomApi: Invalid Buffer") + + # FlexSPI NAND statuses. + FLEXSPINAND_READ_PAGE_FAIL = (20000, "FlexSPINANDReadPageFail", "FlexSPINAND: Read Page Fail") + FLEXSPINAND_READ_CACHE_FAIL = (20001, "FlexSPINANDReadCacheFail", "FlexSPINAND: Read Cache Fail") + FLEXSPINAND_ECC_CHECK_FAIL = (20002, "FlexSPINANDEccCheckFail", "FlexSPINAND: Ecc Check Fail") + FLEXSPINAND_PAGE_LOAD_FAIL = (20003, "FlexSPINANDPageLoadFail", "FlexSPINAND: Page Load Fail") + FLEXSPINAND_PAGE_EXECUTE_FAIL = (20004, "FlexSPINANDPageExecuteFail", "FlexSPINAND: Page Execute Fail") + FLEXSPINAND_ERASE_BLOCK_FAIL = (20005, "FlexSPINANDEraseBlockFail", "FlexSPINAND: Erase Block Fail") + FLEXSPINAND_WAIT_TIMEOUT = (20006, "FlexSPINANDWaitTimeout", "FlexSPINAND: Wait Timeout") + FlexSPINAND_NOT_SUPPORTED = (20007, "SPINANDPageSizeOverTheMaxSupportedSize", "SPI NAND: PageSize over the max supported size") + FlexSPINAND_FCB_UPDATE_FAIL = (20008, "FailedToUpdateFlashConfigBlockToSPINAND", "SPI NAND: Failed to update Flash config block to SPI NAND") + FlexSPINAND_DBBT_UPDATE_FAIL = (20009, "Failed to update discovered bad block table to SPI NAND", "SPI NAND: Failed to update discovered bad block table to SPI NAND") + FLEXSPINAND_WRITEALIGNMENTERROR = (20010, "FlexSPINANDWriteAlignmentError", "FlexSPINAND: Write Alignment Error") + FLEXSPINAND_NOT_FOUND = (20011, "FlexSPINANDNotFound", "FlexSPINAND: Not Found") + + # FlexSPI NOR statuses. + FLEXSPINOR_PROGRAM_FAIL = (20100, "FLEXSPINORProgramFail", "FLEXSPINOR: Program Fail") + FLEXSPINOR_ERASE_SECTOR_FAIL = (20101, "FLEXSPINOREraseSectorFail", "FLEXSPINOR: Erase Sector Fail") + FLEXSPINOR_ERASE_ALL_FAIL = (20102, "FLEXSPINOREraseAllFail", "FLEXSPINOR: Erase All Fail") + FLEXSPINOR_WAIT_TIMEOUT = (20103, "FLEXSPINORWaitTimeout", "FLEXSPINOR:Wait Timeout") + FLEXSPINOR_NOT_SUPPORTED = (20104, "FLEXSPINORPageSizeOverTheMaxSupportedSize", "FlexSPINOR: PageSize over the max supported size") + FLEXSPINOR_WRITE_ALIGNMENT_ERROR = (20105, "FlexSPINORWriteAlignmentError", "FlexSPINOR:Write Alignment Error") + FLEXSPINOR_COMMANDFAILURE = (20106, "FlexSPINORCommandFailure", "FlexSPINOR: Command Failure") + FLEXSPINOR_SFDP_NOTFOUND = (20107, "FlexSPINORSFDPNotFound", "FlexSPINOR: SFDP Not Found") + FLEXSPINOR_UNSUPPORTED_SFDP_VERSION = (20108, "FLEXSPINORUnsupportedSFDPVersion", "FLEXSPINOR: Unsupported SFDP Version") + FLEXSPINOR_FLASH_NOTFOUND = (20109, "FLEXSPINORFlashNotFound", "FLEXSPINOR Flash Not Found") + FLEXSPINOR_DTR_READ_DUMMYPROBEFAILED = (20110, "FLEXSPINORDTRReadDummyProbeFailed", "FLEXSPINOR: DTR Read Dummy Probe Failed") + + # OCOTP statuses. + OCOTP_READ_FAILURE = (20200, "OCOTPReadFailure", "OCOTP: Read Failure") + OCOTP_PROGRAM_FAILURE = (20201, "OCOTPProgramFailure", "OCOTP: Program Failure") + OCOTP_RELOAD_FAILURE = (20202, "OCOTPReloadFailure", "OCOTP: Reload Failure") + OCOTP_WAIT_TIMEOUT = (20203, "OCOTPWaitTimeout", "OCOTP: Wait Timeout") + + # SEMC NOR statuses. + SEMCNOR_DEVICE_TIMEOUT = (21100, "SemcNOR_DeviceTimeout", "SemcNOR: Device Timeout") + SEMCNOR_INVALID_MEMORY_ADDRESS = (21101, "SemcNOR_InvalidMemoryAddress", "SemcNOR: Invalid Memory Address") + SEMCNOR_UNMATCHED_COMMAND_SET = (21102, "SemcNOR_unmatchedCommandSet", "SemcNOR: unmatched Command Set") + SEMCNOR_ADDRESS_ALIGNMENT_ERROR = (21103, "SemcNOR_AddressAlignmentError", "SemcNOR: Address Alignment Error") + SEMCNOR_INVALID_CFI_SIGNATURE = (21104, "SemcNOR_InvalidCfiSignature", "SemcNOR: Invalid Cfi Signature") + SEMCNOR_COMMAND_ERROR_NO_OP_TO_SUSPEND = (21105, "SemcNOR_CommandErrorNoOpToSuspend", "SemcNOR: Command Error No Op To Suspend") + SEMCNOR_COMMAND_ERROR_NO_INFO_AVAILABLE = (21106, "SemcNOR_CommandErrorNoInfoAvailable", "SemcNOR: Command Error No Info Available") + SEMCNOR_BLOCK_ERASE_COMMAND_FAILURE = (21107, "SemcNOR_BlockEraseCommandFailure", "SemcNOR: Block Erase Command Failure") + SEMCNOR_BUFFER_PROGRAM_COMMAND_FAILURE = (21108, "SemcNOR_BufferProgramCommandFailure", "SemcNOR: Buffer Program Command Failure") + SEMCNOR_PROGRAM_VERIFY_FAILURE = (21109, "SemcNOR_ProgramVerifyFailure", "SemcNOR: Program Verify Failure") + SEMCNOR_ERASE_VERIFY_FAILURE = (21110, "SemcNOR_EraseVerifyFailure", "SemcNOR: Erase Verify Failure") + SEMCNOR_INVALID_CFG_TAG = (21116, "SemcNOR_InvalidCfgTag", "SemcNOR: Invalid Cfg Tag") + + # SEMC NAND statuses. + SEMCNAND_DEVICE_TIMEOUT = (21200, "SemcNAND_DeviceTimeout", "SemcNAND: Device Timeout") + SEMCNAND_INVALID_MEMORY_ADDRESS = (21201, "SemcNAND_InvalidMemoryAddress", "SemcNAND: Invalid Memory Address") + SEMCNAND_NOT_EQUAL_TO_ONE_PAGE_SIZE = (21202, "SemcNAND_NotEqualToOnePageSize", "SemcNAND: Not Equal To One Page Size") + SEMCNAND_MORE_THAN_ONE_PAGE_SIZE = (21203, "SemcNAND_MoreThanOnePageSize", "SemcNAND: More Than One Page Size") + SEMCNAND_ECC_CHECK_FAIL = (21204, "SemcNAND_EccCheckFail", "SemcNAND: Ecc Check Fail") + SEMCNAND_INVALID_ONFI_PARAMETER = (21205, "SemcNAND_InvalidOnfiParameter", "SemcNAND: Invalid Onfi Parameter") + SEMCNAND_CANNOT_ENABLE_DEVICE_ECC = (21206, "SemcNAND_CannotEnableDeviceEcc", "SemcNAND: Cannot Enable Device Ecc") + SEMCNAND_SWITCH_TIMING_MODE_FAILURE = (21207, "SemcNAND_SwitchTimingModeFailure", "SemcNAND: Switch Timing Mode Failure") + SEMCNAND_PROGRAM_VERIFY_FAILURE = (21208, "SemcNAND_ProgramVerifyFailure", "SemcNAND: Program Verify Failure") + SEMCNAND_ERASE_VERIFY_FAILURE = (21209, "SemcNAND_EraseVerifyFailure", "SemcNAND: Erase Verify Failure") + SEMCNAND_INVALID_READBACK_BUFFER = (21210, "SemcNAND_InvalidReadbackBuffer", "SemcNAND: Invalid Readback Buffer") + SEMCNAND_INVALID_CFG_TAG = (21216, "SemcNAND_InvalidCfgTag", "SemcNAND: Invalid Cfg Tag") + SEMCNAND_FAIL_TO_UPDATE_FCB = (21217, "SemcNAND_FailToUpdateFcb", "SemcNAND: Fail To Update Fcb") + SEMCNAND_FAIL_TO_UPDATE_DBBT = (21218, "SemcNAND_FailToUpdateDbbt", "SemcNAND: Fail To Update Dbbt") + SEMCNAND_DISALLOW_OVERWRITE_BCB = (21219, "SemcNAND_DisallowOverwriteBcb", "SemcNAND: Disallow Overwrite Bcb") + SEMCNAND_ONLY_SUPPORT_ONFI_DEVICE = (21220, "SemcNAND_OnlySupportOnfiDevice", "SemcNAND: Only Support Onfi Device") + SEMCNAND_MORE_THAN_MAX_IMAGE_COPY = (21221, "SemcNAND_MoreThanMaxImageCopy", "SemcNAND: More Than Max Image Copy") + SEMCNAND_DISORDERED_IMAGE_COPIES = (21222, "SemcNAND_DisorderedImageCopies", "SemcNAND: Disordered Image Copies") + + # SPIFI NOR statuses. + SPIFINOR_PROGRAM_FAIL = (22000, "SPIFINOR_ProgramFail", "SPIFINOR: Program Fail") + SPIFINOR_ERASE_SECTORFAIL = (22001, "SPIFINOR_EraseSectorFail", "SPIFINOR: Erase Sector Fail") + SPIFINOR_ERASE_ALL_FAIL = (22002, "SPIFINOR_EraseAllFail", "SPIFINOR: Erase All Fail") + SPIFINOR_WAIT_TIMEOUT = (22003, "SPIFINOR_WaitTimeout", "SPIFINOR: Wait Timeout") + SPIFINOR_NOT_SUPPORTED = (22004, "SPIFINOR_NotSupported", "SPIFINOR: Not Supported") + SPIFINOR_WRITE_ALIGNMENTERROR = (22005, "SPIFINOR_WriteAlignmentError", "SPIFINOR: Write Alignment Error") + SPIFINOR_COMMAND_FAILURE = (22006, "SPIFINOR_CommandFailure", "SPIFINOR: Command Failure") + SPIFINOR_SFDP_NOT_FOUND = (22007, "SPIFINOR_SFDP_NotFound", "SPIFINOR: SFDP Not Found") + + # EDGELOCK ENCLAVE statuses. + EDGELOCK_INVALID_RESPONSE = (30000, "EDGELOCK_InvalidResponse", "EDGELOCK: Invalid Response") + EDGELOCK_RESPONSE_ERROR = (30001, "EDGELOCK_ResponseError", "EDGELOCK: Response Error") + EDGELOCK_ABORT = (30002, "EDGELOCK_Abort", "EDGELOCK: Abort") + EDGELOCK_OPERATION_FAILED = (30003, "EDGELOCK_OperationFailed", "EDGELOCK: Operation Failed") + EDGELOCK_OTP_PROGRAM_FAILURE = (30004, "EDGELOCK_OTPProgramFailure", "EDGELOCK: OTP Program Failure") + EDGELOCK_OTP_LOCKED = (30005, "EDGELOCK_OTPLocked", "EDGELOCK: OTP Locked") + EDGELOCK_OTP_INVALID_IDX = (30006, "EDGELOCK_OTPInvalidIDX", "EDGELOCK: OTP Invalid IDX") + EDGELOCK_INVALID_LIFECYCLE = (30007, "EDGELOCK_InvalidLifecycle", "EDGELOCK: Invalid Lifecycle") + + # OTP statuses. + OTP_INVALID_ADDRESS = (52801, "OTP_InvalidAddress", "OTD: Invalid OTP address") + OTP_PROGRAM_FAIL = (52802, "OTP_ProgrammingFail", "OTD: Programming failed") + OTP_CRC_FAIL = (52803, "OTP_CRCFail", "OTP: CRC check failed") + OTP_ERROR = (52804, "OTP_Error", "OTP: Error happened during OTP operation") + OTP_ECC_CRC_FAIL = (52805, "OTP_EccCheckFail", "OTP: ECC check failed during OTP operation") + OTP_LOCKED = (52806, "OTP_FieldLocked", "OTP: Field is locked when programming") + OTP_TIMEOUT = (52807, "OTP_Timeout", "OTP: Operation timed out") + OTP_CRC_CHECK_PASS = (52808, "OTP_CRCCheckPass", "OTP: CRC check passed") + OTP_VERIFY_FAIL = (52009, "OPT_VerifyFail", "OTP: Failed to verify OTP write") + + # Security subsystem statuses. + SECURITY_SUBSYSTEM_ERROR = (1515890085, "SecuritySubSystemError", "Security SubSystem Error") + + # TrustProvisioning statuses. + TP_SUCCESS = (0, "TP_SUCCESS", "TP: SUCCESS") + TP_GENERAL_ERROR = (80000, "TP_GENERAL_ERROR", "TP: General error") + TP_CRYPTO_ERROR = (80001, "TP_CRYPTO_ERROR", "TP: Error during cryptographic operation") + TP_NULLPTR_ERROR = (80002, "TP_NULLPTR_ERROR", "TP: NULL pointer dereference or when buffer could not be allocated") + TP_ALREADYINITIALIZED = (80003, "TP_ALREADYINITIALIZED", "TP: Already initialized") + TP_BUFFERSMALL = (80004, "TP_BUFFERSMALL", "TP: Buffer is too small") + TP_ADDRESS_ERROR = (80005, "TP_ADDRESS_ERROR", "TP: Address out of allowed range or buffer could not be allocated") + TP_CONTAINERINVALID = (80006, "TP_CONTAINERINVALID", "TP: Container header or size is invalid") + TP_CONTAINERENTRYINVALID = (80007, "TP_CONTAINERENTRYINVALID", "TP: Container entry invalid") + TP_CONTAINERENTRYNOTFOUND = (80008, "TP_CONTAINERENTRYNOTFOUND", "TP: Container entry not found in container") + TP_INVALIDSTATEOPERATION = (80009, "TP_INVALIDSTATEOPERATION", "TP: Attempt to process command in disallowed state") + TP_COMMAND_ERROR = (80010, "TP_COMMAND_ERROR", "TP: ISP command arguments are invalid") + TP_PUF_ERROR = (80011, "TP_PUF_ERROR", "TP: PUF operation error") + TP_FLASH_ERROR = (80012, "TP_FLASH_ERROR", "TP: Flash erase/program/verify_erase failed") + TP_SECRETBOX_ERROR = (80013, "TP_SECRETBOX_ERROR", "TP: SBKEK or USER KEK cannot be stored in secret box") + TP_PFR_ERROR = (80014, "TP_PFR_ERROR", "TP: Protected Flash Region operation failed") + TP_VERIFICATION_ERROR = (80015, "TP_VERIFICATION_ERROR", "TP: Container signature verification failed") + TP_CFPA_ERROR = (80016, "TP_CFPA_ERROR", "TP: CFPA page cannot be stored") + TP_CMPA_ERROR = (80017, "TP_CMPA_ERROR", "TP: CMPA page cannot be stored or ROTKH or SECU registers are invalid") + TP_ADDR_OUT_OF_RANGE = (80018, "TP_ADDR_OUT_OF_RANGE", "TP: Address is out of range") + TP_CONTAINER_ADDR_ERROR = (80019, "TP_CONTAINER_ADDR_ERROR", "TP: Container address in write context is invalid or there is no memory for entry storage") + TP_CONTAINER_ADDR_UNALIGNED = (80020, "TP_CONTAINER_ADDR_UNALIGNED", "TP: Container address in read context is unaligned") + TP_CONTAINER_BUFF_SMALL = (80021, "TP_CONTAINER_BUFF_SMALL", "TP: There is not enough memory to store the container") + TP_CONTAINER_NO_ENTRY = (80022, "TP_CONTAINER_NO_ENTRY", "TP: Attempt to sign an empty container") + TP_CERT_ADDR_ERROR = (80023, "TP_CERT_ADDR_ERROR", "TP: Destination address of OEM certificate is invalid") + TP_CERT_ADDR_UNALIGNED = (80024, "TP_CERT_ADDR_UNALIGNED", "TP: Destination address of certificate is unaligned") + TP_CERT_OVERLAPPING = (80025, "TP_CERT_OVERLAPPING", "TP: OEM certificates are overlapping due to wrong destination addresses") + TP_PACKET_ERROR = (80026, "TP_PACKET_ERROR", "TP: Error during packet sending/receiving") + TP_PACKET_DATA_ERROR = (80027, "TP_PACKET_DATA_ERROR", "TP: Data in packet handle are invalid") + TP_UNKNOWN_COMMAND = (80028, "TP_UNKNOWN_COMMAND", "TP: Unknown command was received") + TP_SB3_FILE_ERROR = (80029, "TP_SB3_FILE_ERROR", "TP: Error during processing SB3 file") + # TP_CRITICAL_ERROR_START (80100) + TP_GENERAL_CRITICAL_ERROR = (80101, "TP_GENERAL_CRITICAL_ERROR", "TP: Critical error") + TP_CRYPTO_CRITICAL_ERROR = (80102, "TP_CRYPTO_CRITICAL_ERROR", "TP: Error of crypto module which prevents proper functionality") + TP_PUF_CRITICAL_ERROR = (80103, "TP_PUF_CRITICAL_ERROR", "TP: Initialization or start of the PUF periphery failed") + TP_PFR_CRITICAL_ERROR = (80104, "TP_PFR_CRITICAL_ERROR", "TP: Initialization of PFR or reading of activation code failed") + TP_PERIPHERAL_CRITICAL_ERROR = (80105, "TP_PERIPHERAL_CRITICAL_ERROR", "TP: Peripheral failure") + TP_PRINCE_CRITICAL_ERROR = (80106, "TP_PRINCE_CRITICAL_ERROR", "TP: Error during PRINCE encryption/decryption") + TP_SHA_CHECK_CRITICAL_ERROR = (80107, "TP_SHA_CHECK_CRITICAL_ERROR", "TP: SHA check verification failed") + + # IAP statuses. + IAP_INVALID_ARGUMENT = (100001, "IAP_InvalidArgument", "IAP: Invalid Argument Detected During API Execution") + IAP_OUT_OF_MEMORY = (100002, "IAP_OutOfMemory", "IAP: Heap Size Not Large Enough During API Execution") + IAP_READ_DISALLOWED = (100003, "IAP_ReadDisallowed ", "IAP: Read Memory Operation Disallowed During API Execution") + IAP_CUMULATIVE_WRITE = (100004, "IAP_CumulativeWrite", "IAP: Flash Memory Region To Be Programmed Is Not Empty") + IAP_ERASE_FAILUIRE = (100005, "IAP_EraseFailuire", "IAP: Erase Operation Failed") + IAP_COMMAND_NOT_SUPPORTED = (100006, "IAP_CommandNotSupported", "IAP: Specific Command Not Supported") + IAP_MEMORY_ACCESS_DISABLED = (100007, "IAP_MemoryAccessDisabled", "IAP: Memory Access Disabled") +# fmt: on + + +def stringify_status_code(status_code: int) -> str: + """Stringifies the MBoot status code.""" + return ( + f"{status_code} ({status_code:#x}) " + f"{StatusCode.get_description(status_code) if status_code in StatusCode.tags() else f'Unknown error code ({status_code})'}." + ) diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/exceptions.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/exceptions.py new file mode 100644 index 0000000..ea7320d --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/exceptions.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Exceptions used in the MBoot module.""" + +from ..exceptions import SPSDKError +from .error_codes import StatusCode + +######################################################################################################################## +# McuBoot Exceptions +######################################################################################################################## + + +class McuBootError(SPSDKError): + """MBoot Module: Base Exception.""" + + fmt = "MBoot: {description}" + + +class McuBootCommandError(McuBootError): + """MBoot Module: Command Exception.""" + + fmt = "MBoot: {cmd_name} interrupted -> {description}" + + def __init__(self, cmd: str, value: int) -> None: + """Initialize the Command Error exception. + + :param cmd: Name of the command causing the exception + :param value: Response value causing the exception + """ + super().__init__() + self.cmd_name = cmd + self.error_value = value + self.description = ( + StatusCode.get_description(value) + if value in StatusCode.tags() + else f"Unknown Error 0x{value:08X}" + ) + + def __str__(self) -> str: + return self.fmt.format(cmd_name=self.cmd_name, description=self.description) + + +class McuBootDataAbortError(McuBootError): + """MBoot Module: Data phase aborted by sender.""" + + fmt = "Mboot: Data aborted by sender" + + +class McuBootConnectionError(McuBootError): + """MBoot Module: Connection Exception.""" + + fmt = "MBoot: Connection issue -> {description}" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/interfaces/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/interfaces/__init__.py new file mode 100644 index 0000000..eff4a84 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/interfaces/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright (c) 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module implementing the Mboot communication protocol.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/interfaces/usb.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/interfaces/usb.py new file mode 100644 index 0000000..b73a092 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/interfaces/usb.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""USB Mboot interface implementation.""" + + +from dataclasses import dataclass +from typing import TYPE_CHECKING, List, Optional + +from ...mboot.protocol.bulk_protocol import MbootBulkProtocol +from ...utils.interfaces.device.usb_device import UsbDevice + +if TYPE_CHECKING: + from typing_extensions import Self + + +@dataclass +class ScanArgs: + """Scan arguments dataclass.""" + + device_id: str + + @classmethod + def parse(cls, params: str) -> "Self": + """Parse given scanning parameters into ScanArgs class. + + :param params: Parameters as a string + """ + return cls(device_id=params.replace(",", ":")) + + +USB_DEVICES = { + # NAME | VID | PID + "MKL27": (0x15A2, 0x0073), + "LPC55": (0x1FC9, 0x0021), + "IMXRT": (0x1FC9, 0x0135), + "MXRT10": (0x15A2, 0x0073), # this is ID of flash-loader for RT101x + "MXRT20": (0x15A2, 0x0073), # this is ID of flash-loader for RT102x + "MXRT50": (0x15A2, 0x0073), # this is ID of flash-loader for RT105x + "MXRT60": (0x15A2, 0x0073), # this is ID of flash-loader for RT106x + "LPC55xx": (0x1FC9, 0x0020), + "LPC551x": (0x1FC9, 0x0022), + "RT6xx": (0x1FC9, 0x0021), + "RT5xx_A": (0x1FC9, 0x0020), + "RT5xx_B": (0x1FC9, 0x0023), + "RT5xx_C": (0x1FC9, 0x0023), + "RT5xx": (0x1FC9, 0x0023), + "RT6xxM": (0x1FC9, 0x0024), + "LPC553x": (0x1FC9, 0x0025), + "MCXN9xx": (0x1FC9, 0x014F), + "MCXA1xx": (0x1FC9, 0x0155), + "MCXN23x": (0x1FC9, 0x0158), +} + + +class MbootUSBInterface(MbootBulkProtocol): + """USB interface.""" + + identifier = "usb" + device: UsbDevice + usb_devices = USB_DEVICES + + def __init__(self, device: UsbDevice) -> None: + """Initialize the MbootUSBInterface object. + + :param device: The device instance + """ + assert isinstance(device, UsbDevice) + super().__init__(device=device) + + @property + def name(self) -> str: + """Get the name of the device.""" + assert isinstance(self.device, UsbDevice) + for name, value in self.usb_devices.items(): + if value[0] == self.device.vid and value[1] == self.device.pid: + return name + return "Unknown" + + @classmethod + def scan_from_args( + cls, + params: str, + timeout: int, + extra_params: Optional[str] = None, + ) -> List["Self"]: + """Scan connected USB devices. + + :param params: Params as a configuration string + :param extra_params: Extra params configuration string + :param timeout: Timeout for the scan + :return: list of matching RawHid devices + """ + scan_args = ScanArgs.parse(params=params) + devices = cls.scan(device_id=scan_args.device_id, timeout=timeout) + return devices + + @classmethod + def scan( + cls, + device_id: Optional[str] = None, + timeout: Optional[int] = None, + ) -> List["Self"]: + """Scan connected USB devices. + + :param device_id: Device identifier , , device/instance path, device name are supported + :param timeout: Read/write timeout + :return: list of matching RawHid devices + """ + devices = UsbDevice.scan( + device_id=device_id, usb_devices_filter=cls.usb_devices, timeout=timeout + ) + return [cls(device) for device in devices] diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/mcuboot.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/mcuboot.py new file mode 100644 index 0000000..8f918b5 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/mcuboot.py @@ -0,0 +1,1779 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module for communication with the bootloader.""" + +import logging +import struct +import time +from types import TracebackType +from typing import Callable, Dict, List, Optional, Sequence, Type + +from ..exceptions import SPSDKError +from ..mboot.protocol.base import MbootProtocolBase +from ..utils.interfaces.device.usb_device import UsbDevice +from .commands import ( + CmdPacket, + CmdResponse, + CommandFlag, + CommandTag, + FlashReadOnceResponse, + FlashReadResourceResponse, + GenerateKeyBlobSelect, + GenericResponse, + GetPropertyResponse, + KeyProvisioningResponse, + KeyProvOperation, + NoResponse, + ReadMemoryResponse, + TrustProvDevHsmDsc, + TrustProvisioningResponse, + TrustProvOperation, + TrustProvWpc, +) +from .error_codes import StatusCode, stringify_status_code +from .exceptions import ( + McuBootCommandError, + McuBootConnectionError, + McuBootDataAbortError, + McuBootError, +) +from .memories import ExtMemId, ExtMemRegion, FlashRegion, MemoryRegion, RamRegion +from .properties import PropertyTag, PropertyValueBase, Version, parse_property_value + +logger = logging.getLogger(__name__) + + +######################################################################################################################## +# McuBoot Class +######################################################################################################################## +class McuBoot: # pylint: disable=too-many-public-methods + """Class for communication with the bootloader.""" + + DEFAULT_MAX_PACKET_SIZE = 32 + + @property + def status_code(self) -> int: + """Return status code of the last operation.""" + return self._status_code + + @property + def status_string(self) -> str: + """Return status string.""" + return stringify_status_code(self._status_code) + + @property + def is_opened(self) -> bool: + """Return True if the device is open.""" + return self._interface.is_opened + + def __init__( + self, interface: MbootProtocolBase, cmd_exception: bool = False + ) -> None: + """Initialize the McuBoot object. + + :param interface: The instance of communication interface class + :param cmd_exception: True to throw McuBootCommandError on any error; + False to set status code only + Note: some operation might raise McuBootCommandError is all cases + + """ + self._cmd_exception = cmd_exception + self._status_code = StatusCode.SUCCESS.tag + self._interface = interface + self.reopen = False + self.enable_data_abort = False + self._pause_point: Optional[int] = None + + def __enter__(self) -> "McuBoot": + self.reopen = True + self.open() + return self + + def __exit__( + self, + exception_type: Optional[Type[Exception]] = None, + exception_value: Optional[Exception] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() + + def _process_cmd(self, cmd_packet: CmdPacket) -> CmdResponse: + """Process Command. + + :param cmd_packet: Command Packet + :return: command response derived from the CmdResponse + :raises McuBootConnectionError: Timeout Error + :raises McuBootCommandError: Error during command execution on the target + """ + if not self.is_opened: + logger.info("TX: Device not opened") + raise McuBootConnectionError("Device not opened") + + logger.debug(f"TX-PACKET: {str(cmd_packet)}") + + try: + self._interface.write_command(cmd_packet) + response = self._interface.read() + except TimeoutError: + self._status_code = StatusCode.NO_RESPONSE.tag + logger.debug("RX-PACKET: No Response, Timeout Error !") + response = NoResponse(cmd_tag=cmd_packet.header.tag) + + assert isinstance(response, CmdResponse) + logger.debug(f"RX-PACKET: {str(response)}") + self._status_code = response.status + + if self._cmd_exception and self._status_code != StatusCode.SUCCESS: + raise McuBootCommandError( + CommandTag.get_label(cmd_packet.header.tag), response.status + ) + logger.info(f"CMD: Status: {self.status_string}") + return response + + def _read_data( + self, + cmd_tag: CommandTag, + length: int, + progress_callback: Optional[Callable[[int, int], None]] = None, + ) -> bytes: + """Read data from device. + + :param cmd_tag: Tag indicating the read command. + :param length: Length of data to read + :param progress_callback: Callback for updating the caller about the progress + :raises McuBootConnectionError: Timeout error or a problem opening the interface + :raises McuBootCommandError: Error during command execution on the target + :return: Data read from the device + """ + data = b"" + + if not self.is_opened: + logger.error("RX: Device not opened") + raise McuBootConnectionError("Device not opened") + while True: + try: + response = self._interface.read() + except McuBootDataAbortError as e: + logger.error(f"RX: {e}") + logger.info("Try increasing the timeout value") + response = self._interface.read() + except TimeoutError: + self._status_code = StatusCode.NO_RESPONSE.tag + logger.error("RX: No Response, Timeout Error !") + response = NoResponse(cmd_tag=cmd_tag.tag) + break + + if isinstance(response, bytes): + data += response + if progress_callback: + progress_callback(len(data), length) + + elif isinstance(response, GenericResponse): + logger.debug(f"RX-PACKET: {str(response)}") + self._status_code = response.status + if response.cmd_tag == cmd_tag: + break + + if len(data) < length or self.status_code != StatusCode.SUCCESS: + status_info = ( + StatusCode.get_label(self._status_code) + if self._status_code in StatusCode.tags() + else f"0x{self._status_code:08X}" + ) + logger.debug( + f"CMD: Received {len(data)} from {length} Bytes, {status_info}" + ) + if self._cmd_exception: + assert isinstance(response, CmdResponse) + raise McuBootCommandError(cmd_tag.label, response.status) + else: + logger.info(f"CMD: Successfully Received {len(data)} from {length} Bytes") + + return data[:length] if len(data) > length else data + + def _send_data( + self, + cmd_tag: CommandTag, + data: List[bytes], + progress_callback: Optional[Callable[[int, int], None]] = None, + ) -> bool: + """Send Data part of specific command. + + :param cmd_tag: Tag indicating the command + :param data: List of data chunks to send + :param progress_callback: Callback for updating the caller about the progress + :raises McuBootConnectionError: Timeout error + :raises McuBootCommandError: Error during command execution on the target + :return: True if the operation is successful + """ + if not self.is_opened: + logger.info("TX: Device Disconnected") + raise McuBootConnectionError("Device Disconnected !") + + total_sent = 0 + total_to_send = sum(len(chunk) for chunk in data) + # this difference is applicable for load-image and program-aeskey commands + expect_response = cmd_tag != CommandTag.NO_COMMAND + self._interface.allow_abort = self.enable_data_abort + try: + for data_chunk in data: + self._interface.write_data(data_chunk) + total_sent += len(data_chunk) + if progress_callback: + progress_callback(total_sent, total_to_send) + if self._pause_point and total_sent > self._pause_point: + time.sleep(0.1) + self._pause_point = None + + if expect_response: + response = self._interface.read() + except TimeoutError as e: + self._status_code = StatusCode.NO_RESPONSE.tag + logger.error("RX: No Response, Timeout Error !") + raise McuBootConnectionError("No Response from Device") from e + except SPSDKError as e: + logger.error(f"RX: {e}") + if expect_response: + response = self._interface.read() + else: + self._status_code = StatusCode.SENDING_OPERATION_CONDITION_ERROR.tag + + if expect_response: + assert isinstance(response, CmdResponse) + logger.debug(f"RX-PACKET: {str(response)}") + self._status_code = response.status + if response.status != StatusCode.SUCCESS: + status_info = ( + StatusCode.get_label(self._status_code) + if self._status_code in StatusCode.tags() + else f"0x{self._status_code:08X}" + ) + logger.debug(f"CMD: Send Error, {status_info}") + if self._cmd_exception: + raise McuBootCommandError(cmd_tag.label, response.status) + return False + + logger.info(f"CMD: Successfully Send {total_sent} out of {total_to_send} Bytes") + return total_sent == total_to_send + + def _get_max_packet_size(self) -> int: + """Get max packet size. + + :return int: max packet size in B + """ + packet_size_property = None + try: + packet_size_property = self.get_property( + prop_tag=PropertyTag.MAX_PACKET_SIZE + ) + except McuBootError: + pass + if packet_size_property is None: + packet_size_property = [self.DEFAULT_MAX_PACKET_SIZE] + logger.warning( + f"CMD: Unable to get MAX PACKET SIZE, using: {self.DEFAULT_MAX_PACKET_SIZE}" + ) + return packet_size_property[0] + + def _split_data(self, data: bytes) -> List[bytes]: + """Split data to send if necessary. + + :param data: Data to send + :return: List of data splices + """ + if not self._interface.need_data_split: + return [data] + max_packet_size = self._get_max_packet_size() + logger.info(f"CMD: Max Packet Size = {max_packet_size}") + return [ + data[i : i + max_packet_size] for i in range(0, len(data), max_packet_size) + ] + + def open(self) -> None: + """Connect to the device.""" + logger.info(f"Connect: {str(self._interface)}") + self._interface.open() + + def close(self) -> None: + """Disconnect from the device.""" + logger.info(f"Closing: {str(self._interface)}") + self._interface.close() + + def get_property_list(self) -> List[PropertyValueBase]: + """Get a list of available properties. + + :return: List of available properties. + :raises McuBootCommandError: Failure to read properties list + """ + property_list: List[PropertyValueBase] = [] + for property_tag in PropertyTag: + try: + values = self.get_property(property_tag) + except McuBootCommandError: + continue + + if values: + prop = parse_property_value(property_tag.tag, values) + assert prop is not None, "Property values cannot be parsed" + property_list.append(prop) + + self._status_code = StatusCode.SUCCESS.tag + if not property_list: + self._status_code = StatusCode.FAIL.tag + if self._cmd_exception: + raise McuBootCommandError("GetPropertyList", self.status_code) + + return property_list + + def _get_internal_flash(self) -> List[FlashRegion]: + """Get information about the internal flash. + + :return: list of FlashRegion objects + """ + index = 0 + mdata: List[FlashRegion] = [] + start_address = 0 + while True: + try: + values = self.get_property(PropertyTag.FLASH_START_ADDRESS, index) + if not values: + break + if index == 0: + start_address = values[0] + elif start_address == values[0]: + break + region_start = values[0] + values = self.get_property(PropertyTag.FLASH_SIZE, index) + if not values: + break + region_size = values[0] + values = self.get_property(PropertyTag.FLASH_SECTOR_SIZE, index) + if not values: + break + region_sector_size = values[0] + mdata.append( + FlashRegion( + index=index, + start=region_start, + size=region_size, + sector_size=region_sector_size, + ) + ) + index += 1 + except McuBootCommandError: + break + + return mdata + + def _get_internal_ram(self) -> List[RamRegion]: + """Get information about the internal RAM. + + :return: list of RamRegion objects + """ + index = 0 + mdata: List[RamRegion] = [] + start_address = 0 + while True: + try: + values = self.get_property(PropertyTag.RAM_START_ADDRESS, index) + if not values: + break + if index == 0: + start_address = values[0] + elif start_address == values[0]: + break + start = values[0] + values = self.get_property(PropertyTag.RAM_SIZE, index) + if not values: + break + size = values[0] + mdata.append(RamRegion(index=index, start=start, size=size)) + index += 1 + except McuBootCommandError: + break + + return mdata + + def _get_ext_memories(self) -> List[ExtMemRegion]: + """Get information about the external memories. + + :return: list of ExtMemRegion objects supported by the device + :raises SPSDKError: If no response to get property command + :raises SPSDKError: Other Error + """ + ext_mem_list: List[ExtMemRegion] = [] + ext_mem_ids: Sequence[int] = ExtMemId.tags() + try: + values = self.get_property(PropertyTag.CURRENT_VERSION) + except McuBootCommandError: + values = None + + if not values and self._status_code == StatusCode.UNKNOWN_PROPERTY: + self._status_code = StatusCode.SUCCESS.tag + return ext_mem_list + + if not values: + raise SPSDKError("No response to get property command") + + if Version(values[0]) <= Version("2.0.0"): + # old versions mboot support only Quad SPI memory + ext_mem_ids = [ExtMemId.QUAD_SPI0.tag] + + for mem_id in ext_mem_ids: + try: + values = self.get_property( + PropertyTag.EXTERNAL_MEMORY_ATTRIBUTES, mem_id + ) + except McuBootCommandError: + values = None + + if ( + not values + ): # pragma: no cover # corner-cases are currently untestable without HW + if self._status_code == StatusCode.UNKNOWN_PROPERTY: + break + + if self._status_code in [ + StatusCode.QSPI_NOT_CONFIGURED, + StatusCode.INVALID_ARGUMENT, + ]: + continue + + if self._status_code == StatusCode.MEMORY_NOT_CONFIGURED: + ext_mem_list.append(ExtMemRegion(mem_id=mem_id)) + + if self._status_code == StatusCode.SUCCESS: + raise SPSDKError("Other Error") + + else: + ext_mem_list.append(ExtMemRegion(mem_id=mem_id, raw_values=values)) + return ext_mem_list + + def get_memory_list(self) -> dict[str, Sequence[MemoryRegion]]: + """Get list of embedded memories. + + :return: dict, with the following keys: internal_flash (optional) - list , + internal_ram (optional) - list, external_mems (optional) - list + :raises McuBootCommandError: Error reading the memory list + """ + memory_list: Dict[str, Sequence[MemoryRegion]] = {} + + # Internal FLASH + mdata = self._get_internal_flash() + if mdata: + memory_list["internal_flash"] = mdata + + # Internal RAM + ram_data = self._get_internal_ram() + if mdata: + memory_list["internal_ram"] = ram_data + + # External Memories + ext_mem_list = self._get_ext_memories() + if ext_mem_list: + memory_list["external_mems"] = ext_mem_list + + self._status_code = StatusCode.SUCCESS.tag + if not memory_list: + self._status_code = StatusCode.FAIL.tag + if self._cmd_exception: + raise McuBootCommandError("GetMemoryList", self.status_code) + + return memory_list + + def flash_erase_all(self, mem_id: int = 0) -> bool: + """Erase complete flash memory without recovering flash security section. + + :param mem_id: Memory ID + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: FlashEraseAll(mem_id={mem_id})") + cmd_packet = CmdPacket(CommandTag.FLASH_ERASE_ALL, CommandFlag.NONE.tag, mem_id) + response = self._process_cmd(cmd_packet) + return response.status == StatusCode.SUCCESS + + def flash_erase_region(self, address: int, length: int, mem_id: int = 0) -> bool: + """Erase specified range of flash. + + :param address: Start address + :param length: Count of bytes + :param mem_id: Memory ID + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: FlashEraseRegion(address=0x{address:08X}, length={length}, mem_id={mem_id})" + ) + mem_id = _clamp_down_memory_id(memory_id=mem_id) + cmd_packet = CmdPacket( + CommandTag.FLASH_ERASE_REGION, CommandFlag.NONE.tag, address, length, mem_id + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def read_memory( + self, + address: int, + length: int, + mem_id: int = 0, + progress_callback: Optional[Callable[[int, int], None]] = None, + fast_mode: bool = False, + ) -> Optional[bytes]: + """Read data from MCU memory. + + :param address: Start address + :param length: Count of bytes + :param mem_id: Memory ID + :param fast_mode: Fast mode for USB-HID data transfer, not reliable !!! + :param progress_callback: Callback for updating the caller about the progress + :return: Data read from the memory; None in case of a failure + """ + logger.info( + f"CMD: ReadMemory(address=0x{address:08X}, length={length}, mem_id={mem_id})" + ) + mem_id = _clamp_down_memory_id(memory_id=mem_id) + + # workaround for better USB-HID reliability + if isinstance(self._interface.device, UsbDevice) and not fast_mode: + payload_size = self._get_max_packet_size() + packets = length // payload_size + remainder = length % payload_size + if remainder: + packets += 1 + + data = b"" + + for idx in range(packets): + if idx == packets - 1 and remainder: + data_len = remainder + else: + data_len = payload_size + + cmd_packet = CmdPacket( + CommandTag.READ_MEMORY, + CommandFlag.NONE.tag, + address + idx * payload_size, + data_len, + mem_id, + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + data += self._read_data(CommandTag.READ_MEMORY, data_len) + if progress_callback: + progress_callback(len(data), length) + if self._status_code == StatusCode.NO_RESPONSE: + logger.warning( + f"CMD: NO RESPONSE, received {len(data)}/{length} B" + ) + return data + else: + return b"" + + return data + + cmd_packet = CmdPacket( + CommandTag.READ_MEMORY, CommandFlag.NONE.tag, address, length, mem_id + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, ReadMemoryResponse) + return self._read_data( + CommandTag.READ_MEMORY, cmd_response.length, progress_callback + ) + return None + + def write_memory( + self, + address: int, + data: bytes, + mem_id: int = 0, + progress_callback: Optional[Callable[[int, int], None]] = None, + ) -> bool: + """Write data into MCU memory. + + :param address: Start address + :param data: List of bytes + :param progress_callback: Callback for updating the caller about the progress + :param mem_id: Memory ID, see ExtMemId; additionally use `0` for internal memory + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: WriteMemory(address=0x{address:08X}, length={len(data)}, mem_id={mem_id})" + ) + data_chunks = self._split_data(data=data) + mem_id = _clamp_down_memory_id(memory_id=mem_id) + cmd_packet = CmdPacket( + CommandTag.WRITE_MEMORY, + CommandFlag.HAS_DATA_PHASE.tag, + address, + len(data), + mem_id, + ) + if self._process_cmd(cmd_packet).status == StatusCode.SUCCESS: + return self._send_data( + CommandTag.WRITE_MEMORY, data_chunks, progress_callback + ) + return False + + def fill_memory(self, address: int, length: int, pattern: int = 0xFFFFFFFF) -> bool: + """Fill MCU memory with specified pattern. + + :param address: Start address (must be word aligned) + :param length: Count of words (must be word aligned) + :param pattern: Count of wrote bytes + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: FillMemory(address=0x{address:08X}, length={length}, pattern=0x{pattern:08X})" + ) + cmd_packet = CmdPacket( + CommandTag.FILL_MEMORY, CommandFlag.NONE.tag, address, length, pattern + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def flash_security_disable(self, backdoor_key: bytes) -> bool: + """Disable flash security by using of backdoor key. + + :param backdoor_key: The key value as array of 8 bytes + :return: False in case of any problem; True otherwise + :raises McuBootError: If the backdoor_key is not 8 bytes long + """ + if len(backdoor_key) != 8: + raise McuBootError("Backdoor key must by 8 bytes long") + logger.info(f"CMD: FlashSecurityDisable(backdoor_key={backdoor_key!r})") + key_high = backdoor_key[0:4][::-1] + key_low = backdoor_key[4:8][::-1] + cmd_packet = CmdPacket( + CommandTag.FLASH_SECURITY_DISABLE, + CommandFlag.NONE.tag, + data=key_high + key_low, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def get_property( + self, prop_tag: PropertyTag, index: int = 0 + ) -> Optional[List[int]]: + """Get specified property value. + + :param prop_tag: Property TAG (see Properties Enum) + :param index: External memory ID or internal memory region index (depends on property type) + :return: list integers representing the property; None in case no response from device + :raises McuBootError: If received invalid get-property response + """ + logger.info(f"CMD: GetProperty({prop_tag.label}, index={index!r})") + cmd_packet = CmdPacket( + CommandTag.GET_PROPERTY, CommandFlag.NONE.tag, prop_tag.tag, index + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + if isinstance(cmd_response, GetPropertyResponse): + return cmd_response.values + raise McuBootError( + f"Received invalid get-property response: {str(cmd_response)}" + ) + return None + + def set_property(self, prop_tag: PropertyTag, value: int) -> bool: + """Set value of specified property. + + :param prop_tag: Property TAG (see Property enumerator) + :param value: The value of selected property + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: SetProperty({prop_tag.label}, value=0x{value:08X})") + cmd_packet = CmdPacket( + CommandTag.SET_PROPERTY, CommandFlag.NONE.tag, prop_tag.tag, value + ) + cmd_response = self._process_cmd(cmd_packet) + return cmd_response.status == StatusCode.SUCCESS + + def receive_sb_file( + self, + data: bytes, + progress_callback: Optional[Callable[[int, int], None]] = None, + check_errors: bool = False, + ) -> bool: + """Receive SB file. + + :param data: SB file data + :param progress_callback: Callback for updating the caller about the progress + :param check_errors: Check for ABORT_FRAME (and related errors) on USB interface between data packets. + When this parameter is set to `False` significantly improves USB transfer speed (cca 20x) + However, the final status code might be misleading (original root cause may get overridden) + In case `receive-sb-file` fails, re-run the operation with this flag set to `True` + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: ReceiveSBfile(data_length={len(data)})") + data_chunks = self._split_data(data=data) + cmd_packet = CmdPacket( + CommandTag.RECEIVE_SB_FILE, CommandFlag.HAS_DATA_PHASE.tag, len(data) + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + self.enable_data_abort = check_errors + if isinstance(self._interface.device, UsbDevice): + try: + # pylint: disable=import-outside-toplevel # import only if needed to save time + from ..sbfile.sb2.headers import ImageHeaderV2 + + sb2_header = ImageHeaderV2.parse(data=data) + self._pause_point = sb2_header.first_boot_tag_block * 16 + except SPSDKError: + pass + # Deactivated for pynitrokey + # try: + # # pylint: disable=import-outside-toplevel # import only if needed to save time + # from spsdk.sbfile.sb31.images import SecureBinary31Header + + # sb3_header = SecureBinary31Header.parse(data=data) + # self._pause_point = sb3_header.image_total_length + # except SPSDKError: + # pass + result = self._send_data( + CommandTag.RECEIVE_SB_FILE, data_chunks, progress_callback + ) + self.enable_data_abort = False + return result + return False + + def execute( + self, address: int, argument: int, sp: int + ) -> bool: # pylint: disable=invalid-name + """Execute program on a given address using the stack pointer. + + :param address: Jump address (must be word aligned) + :param argument: Function arguments address + :param sp: Stack pointer address + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: Execute(address=0x{address:08X}, argument=0x{argument:08X}, SP=0x{sp:08X})" + ) + cmd_packet = CmdPacket( + CommandTag.EXECUTE, CommandFlag.NONE.tag, address, argument, sp + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def call(self, address: int, argument: int) -> bool: + """Fill MCU memory with specified pattern. + + :param address: Call address (must be word aligned) + :param argument: Function arguments address + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: Call(address=0x{address:08X}, argument=0x{argument:08X})") + cmd_packet = CmdPacket(CommandTag.CALL, CommandFlag.NONE.tag, address, argument) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def reset(self, timeout: int = 2000, reopen: bool = True) -> bool: + """Reset MCU and reconnect if enabled. + + :param timeout: The maximal waiting time in [ms] for reopen connection + :param reopen: True for reopen connection after HW reset else False + :return: False in case of any problem; True otherwise + :raises McuBootError: if reopen is not supported + :raises McuBootConnectionError: Failure to reopen the device + """ + logger.info("CMD: Reset MCU") + cmd_packet = CmdPacket(CommandTag.RESET, CommandFlag.NONE.tag) + ret_val = False + status = self._process_cmd(cmd_packet).status + self.close() + ret_val = True + + if status not in [StatusCode.NO_RESPONSE, StatusCode.SUCCESS]: + ret_val = False + if self._cmd_exception: + raise McuBootConnectionError("Reset command failed") + + if status == StatusCode.NO_RESPONSE: + logger.warning("Did not receive response from reset command, ignoring it") + self._status_code = StatusCode.SUCCESS.tag + + if reopen: + if not self.reopen: + raise McuBootError("reopen is not supported") + time.sleep(timeout / 1000) + try: + self.open() + except SPSDKError as e: + ret_val = False + if self._cmd_exception: + raise McuBootConnectionError("reopen failed") from e + + return ret_val + + def flash_erase_all_unsecure(self) -> bool: + """Erase complete flash memory and recover flash security section. + + :return: False in case of any problem; True otherwise + """ + logger.info("CMD: FlashEraseAllUnsecure") + cmd_packet = CmdPacket( + CommandTag.FLASH_ERASE_ALL_UNSECURE, CommandFlag.NONE.tag + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def efuse_read_once(self, index: int) -> Optional[int]: + """Read from MCU flash program once region. + + :param index: Start index + :return: read value (32-bit int); None if operation failed + """ + logger.info(f"CMD: FlashReadOnce(index={index})") + cmd_packet = CmdPacket( + CommandTag.FLASH_READ_ONCE, CommandFlag.NONE.tag, index, 4 + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, FlashReadOnceResponse) + return cmd_response.values[0] + return None + + def efuse_program_once(self, index: int, value: int, verify: bool = False) -> bool: + """Write into MCU once program region (OCOTP). + + :param index: Start index + :param value: Int value (4 bytes long) + :param verify: Verify that data were written (by comparing value as bitmask) + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: FlashProgramOnce(index={index}, value=0x{value:X}) " + f"with{'' if verify else 'out'} verification." + ) + cmd_packet = CmdPacket( + CommandTag.FLASH_PROGRAM_ONCE, CommandFlag.NONE.tag, index, 4, value + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status != StatusCode.SUCCESS: + return False + if verify: + read_value = self.efuse_read_once(index=index & ((1 << 24) - 1)) + if read_value is None: + return False + # We check only a bitmask, because OTP allows to burn individual bits separately + # Some other bits may have been already written + if read_value & value == value: + return True + # It may happen that ROM will not report error when attempting to write into locked OTP + # In such case we substitute the original SUCCESS code with custom-made OTP_VERIFY_FAIL + self._status_code = StatusCode.OTP_VERIFY_FAIL.tag + return False + return cmd_response.status == StatusCode.SUCCESS + + def flash_read_once(self, index: int, count: int = 4) -> Optional[bytes]: + """Read from MCU flash program once region (max 8 bytes). + + :param index: Start index + :param count: Count of bytes + :return: Data read; None in case of an failure + :raises SPSDKError: When invalid count of bytes. Must be 4 or 8 + """ + if count not in (4, 8): + raise SPSDKError("Invalid count of bytes. Must be 4 or 8") + logger.info(f"CMD: FlashReadOnce(index={index}, bytes={count})") + cmd_packet = CmdPacket( + CommandTag.FLASH_READ_ONCE, CommandFlag.NONE.tag, index, count + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, FlashReadOnceResponse) + return cmd_response.data + return None + + def flash_program_once(self, index: int, data: bytes) -> bool: + """Write into MCU flash program once region (max 8 bytes). + + :param index: Start index + :param data: Input data aligned to 4 or 8 bytes + :return: False in case of any problem; True otherwise + :raises SPSDKError: When invalid length of data. Must be aligned to 4 or 8 bytes + """ + if len(data) not in (4, 8): + raise SPSDKError("Invalid length of data. Must be aligned to 4 or 8 bytes") + logger.info(f"CMD: FlashProgramOnce(index={index!r}, data={data!r})") + cmd_packet = CmdPacket( + CommandTag.FLASH_PROGRAM_ONCE, + CommandFlag.NONE.tag, + index, + len(data), + data=data, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def flash_read_resource( + self, address: int, length: int, option: int = 1 + ) -> Optional[bytes]: + """Read resource of flash module. + + :param address: Start address + :param length: Number of bytes + :param option: Area to be read. 0 means Flash IFR, 1 means Flash Firmware ID + :raises McuBootError: when the length is not aligned to 4 bytes + :return: Data from the resource; None in case of an failure + """ + if length % 4: + raise McuBootError( + "The number of bytes to read is not aligned to the 4 bytes" + ) + logger.info( + f"CMD: FlashReadResource(address=0x{address:08X}, length={length}, option={option})" + ) + cmd_packet = CmdPacket( + CommandTag.FLASH_READ_RESOURCE, + CommandFlag.NONE.tag, + address, + length, + option, + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, FlashReadResourceResponse) + return self._read_data(CommandTag.FLASH_READ_RESOURCE, cmd_response.length) + return None + + def configure_memory(self, address: int, mem_id: int) -> bool: + """Configure memory. + + :param address: The address in memory where are locating configuration data + :param mem_id: Memory ID + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: ConfigureMemory({mem_id}, address=0x{address:08X})") + cmd_packet = CmdPacket( + CommandTag.CONFIGURE_MEMORY, CommandFlag.NONE.tag, mem_id, address + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def reliable_update(self, address: int) -> bool: + """Reliable Update. + + :param address: Address where new the firmware is stored + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: ReliableUpdate(address=0x{address:08X})") + cmd_packet = CmdPacket( + CommandTag.RELIABLE_UPDATE, CommandFlag.NONE.tag, address + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def generate_key_blob( + self, + dek_data: bytes, + key_sel: int = GenerateKeyBlobSelect.OPTMK.tag, + count: int = 72, + ) -> Optional[bytes]: + """Generate Key Blob. + + :param dek_data: Data Encryption Key as bytes + :param key_sel: select the BKEK used to wrap the BK (default: OPTMK/FUSES) + :param count: Key blob count (default: 72 - AES128bit) + :return: Key blob; None in case of an failure + """ + logger.info( + f"CMD: GenerateKeyBlob(dek_len={len(dek_data)}, key_sel={key_sel}, count={count})" + ) + data_chunks = self._split_data(data=dek_data) + cmd_response = self._process_cmd( + CmdPacket( + CommandTag.GENERATE_KEY_BLOB, + CommandFlag.HAS_DATA_PHASE.tag, + key_sel, + len(dek_data), + 0, + ) + ) + if cmd_response.status != StatusCode.SUCCESS: + return None + if not self._send_data(CommandTag.GENERATE_KEY_BLOB, data_chunks): + return None + cmd_response = self._process_cmd( + CmdPacket( + CommandTag.GENERATE_KEY_BLOB, CommandFlag.NONE.tag, key_sel, count, 1 + ) + ) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, ReadMemoryResponse) + return self._read_data(CommandTag.GENERATE_KEY_BLOB, cmd_response.length) + return None + + def kp_enroll(self) -> bool: + """Key provisioning: Enroll Command (start PUF). + + :return: False in case of any problem; True otherwise + """ + logger.info("CMD: [KeyProvisioning] Enroll") + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.ENROLL.tag, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def kp_set_intrinsic_key(self, key_type: int, key_size: int) -> bool: + """Key provisioning: Generate Intrinsic Key. + + :param key_type: Type of the key + :param key_size: Size of the key + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: [KeyProvisioning] SetIntrinsicKey(type={key_type}, key_size={key_size})" + ) + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.SET_INTRINSIC_KEY.tag, + key_type, + key_size, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def kp_write_nonvolatile(self, mem_id: int = 0) -> bool: + """Key provisioning: Write the key to a nonvolatile memory. + + :param mem_id: The memory ID (default: 0) + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: [KeyProvisioning] WriteNonVolatileMemory(mem_id={mem_id})") + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.WRITE_NON_VOLATILE.tag, + mem_id, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def kp_read_nonvolatile(self, mem_id: int = 0) -> bool: + """Key provisioning: Load the key from a nonvolatile memory to bootloader. + + :param mem_id: The memory ID (default: 0) + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: [KeyProvisioning] ReadNonVolatileMemory(mem_id={mem_id})") + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.READ_NON_VOLATILE.tag, + mem_id, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def kp_set_user_key(self, key_type: int, key_data: bytes) -> bool: + """Key provisioning: Send the user key specified by to bootloader. + + :param key_type: type of the user key, see enumeration for details + :param key_data: binary content of the user key + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: [KeyProvisioning] SetUserKey(key_type={key_type}, " + f"key_len={len(key_data)})" + ) + data_chunks = self._split_data(data=key_data) + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.HAS_DATA_PHASE.tag, + KeyProvOperation.SET_USER_KEY.tag, + key_type, + len(key_data), + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + return self._send_data(CommandTag.KEY_PROVISIONING, data_chunks) + return False + + def kp_write_key_store(self, key_data: bytes) -> bool: + """Key provisioning: Write key data into key store area. + + :param key_data: key store binary content to be written to processor + :return: result of the operation; True means success + """ + logger.info(f"CMD: [KeyProvisioning] WriteKeyStore(key_len={len(key_data)})") + data_chunks = self._split_data(data=key_data) + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.HAS_DATA_PHASE.tag, + KeyProvOperation.WRITE_KEY_STORE.tag, + 0, + len(key_data), + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + return self._send_data(CommandTag.KEY_PROVISIONING, data_chunks) + return False + + def kp_read_key_store(self) -> Optional[bytes]: + """Key provisioning: Read key data from key store area.""" + logger.info("CMD: [KeyProvisioning] ReadKeyStore") + cmd_packet = CmdPacket( + CommandTag.KEY_PROVISIONING, + CommandFlag.NONE.tag, + KeyProvOperation.READ_KEY_STORE.tag, + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, KeyProvisioningResponse) + return self._read_data(CommandTag.KEY_PROVISIONING, cmd_response.length) + return None + + def load_image( + self, + data: bytes, + progress_callback: Optional[Callable[[int, int], None]] = None, + ) -> bool: + """Load a boot image to the device. + + :param data: boot image + :param progress_callback: Callback for updating the caller about the progress + :return: False in case of any problem; True otherwise + """ + logger.info(f"CMD: LoadImage(length={len(data)})") + data_chunks = self._split_data(data) + # there's no command in this case + self._status_code = StatusCode.SUCCESS.tag + return self._send_data(CommandTag.NO_COMMAND, data_chunks, progress_callback) + + def tp_prove_genuinity(self, address: int, buffer_size: int) -> Optional[int]: + """Start the process of proving genuinity. + + :param address: Address where to prove genuinity request (challenge) container + :param buffer_size: Maximum size of the response package (limit 0xFFFF) + :raises McuBootError: Invalid input parameters + :return: True if prove_genuinity operation is successfully completed + """ + logger.info( + f"CMD: [TrustProvisioning] ProveGenuinity(address={hex(address)}, " + f"buffer_size={buffer_size})" + ) + if buffer_size > 0xFFFF: + raise McuBootError("buffer_size must be less than 0xFFFF") + address_msb = (address >> 32) & 0xFFFF_FFFF + address_lsb = address & 0xFFFF_FFFF + sentinel_cmd = _tp_sentinel_frame( + TrustProvOperation.PROVE_GENUINITY.tag, + args=[address_msb, address_lsb, buffer_size], + ) + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, CommandFlag.NONE.tag, data=sentinel_cmd + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + assert isinstance(cmd_response, TrustProvisioningResponse) + return cmd_response.values[0] + return None + + def tp_set_wrapped_data( + self, address: int, stage: int = 0x4B, control: int = 1 + ) -> bool: + """Start the process of setting OEM data. + + :param address: Address where the wrapped data container on target + :param control: 1 - use the address, 2 - use container within the firmware, defaults to 1 + :param stage: Stage of TrustProvisioning flow, defaults to 0x4B + :return: True if set_wrapped_data operation is successfully completed + """ + logger.info(f"CMD: [TrustProvisioning] SetWrappedData(address={hex(address)})") + if address == 0: + control = 2 + + address_msb = (address >> 32) & 0xFFFF_FFFF + address_lsb = address & 0xFFFF_FFFF + stage_control = control << 8 | stage + sentinel_cmd = _tp_sentinel_frame( + TrustProvOperation.ISP_SET_WRAPPED_DATA.tag, + args=[stage_control, address_msb, address_lsb], + ) + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, CommandFlag.NONE.tag, data=sentinel_cmd + ) + cmd_response = self._process_cmd(cmd_packet) + return cmd_response.status == StatusCode.SUCCESS + + def fuse_program(self, address: int, data: bytes, mem_id: int = 0) -> bool: + """Program fuse. + + :param address: Start address + :param data: List of bytes + :param mem_id: Memory ID + :return: False in case of any problem; True otherwise + """ + logger.info( + f"CMD: FuseProgram(address=0x{address:08X}, length={len(data)}, mem_id={mem_id})" + ) + data_chunks = self._split_data(data=data) + mem_id = _clamp_down_memory_id(memory_id=mem_id) + cmd_packet = CmdPacket( + CommandTag.FUSE_PROGRAM, + CommandFlag.HAS_DATA_PHASE.tag, + address, + len(data), + mem_id, + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: # pragma: no cover + # command is not supported in any device, thus we can't measure coverage + return self._send_data(CommandTag.FUSE_PROGRAM, data_chunks) + return False + + def fuse_read(self, address: int, length: int, mem_id: int = 0) -> Optional[bytes]: + """Read fuse. + + :param address: Start address + :param length: Count of bytes + :param mem_id: Memory ID + :return: Data read from the fuse; None in case of a failure + """ + logger.info( + f"CMD: ReadFuse(address=0x{address:08X}, length={length}, mem_id={mem_id})" + ) + mem_id = _clamp_down_memory_id(memory_id=mem_id) + cmd_packet = CmdPacket( + CommandTag.FUSE_READ, CommandFlag.NONE.tag, address, length, mem_id + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: # pragma: no cover + # command is not supported in any device, thus we can't measure coverage + assert isinstance(cmd_response, ReadMemoryResponse) + return self._read_data(CommandTag.FUSE_READ, cmd_response.length) + return None + + def update_life_cycle(self, life_cycle: int) -> bool: + """Update device life cycle. + + :param life_cycle: New life cycle value. + :return: False in case of any problems, True otherwise. + """ + logger.info(f"CMD: UpdateLifeCycle (life cycle=0x{life_cycle:02X})") + cmd_packet = CmdPacket( + CommandTag.UPDATE_LIFE_CYCLE, CommandFlag.NONE.tag, life_cycle + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def ele_message( + self, cmdMsgAddr: int, cmdMsgCnt: int, respMsgAddr: int, respMsgCnt: int + ) -> bool: + """Send EdgeLock Enclave message. + + :param cmdMsgAddr: Address in RAM where is prepared the command message words + :param cmdMsgCnt: Count of 32bits command words + :param respMsgAddr: Address in RAM where the command store the response + :param respMsgCnt: Count of 32bits response words + + :return: False in case of any problems, True otherwise. + """ + logger.info( + f"CMD: EleMessage Command (cmdMsgAddr=0x{cmdMsgAddr:08X}, cmdMsgCnt={cmdMsgCnt})" + ) + if respMsgCnt: + logger.info( + f"CMD: EleMessage Response (respMsgAddr=0x{respMsgAddr:08X}, respMsgCnt={respMsgCnt})" + ) + cmd_packet = CmdPacket( + CommandTag.ELE_MESSAGE, + CommandFlag.NONE.tag, + 0, # reserved for future use as a sub command ID or anything else + cmdMsgAddr, + cmdMsgCnt, + respMsgAddr, + respMsgCnt, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def tp_hsm_gen_key( + self, + key_type: int, + reserved: int, + key_blob_output_addr: int, + key_blob_output_size: int, + ecdsa_puk_output_addr: int, + ecdsa_puk_output_size: int, + ) -> Optional[List[int]]: + """Trust provisioning: OEM generate common keys. + + :param key_type: Key to generate (MFW_ISK, MFW_ENCK, GEN_SIGNK, GET_CUST_MK_SK) + :param reserved: Reserved, must be zero + :param key_blob_output_addr: The output buffer address where ROM writes the key blob to + :param key_blob_output_size: The output buffer size in byte + :param ecdsa_puk_output_addr: The output buffer address where ROM writes the public key to + :param ecdsa_puk_output_size: The output buffer size in byte + :return: Return byte count of the key blob + byte count of the public key from the device; + None in case of an failure + """ + logger.info("CMD: [TrustProvisioning] OEM generate common keys") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.HSM_GEN_KEY.tag, + key_type, + reserved, + key_blob_output_addr, + key_blob_output_size, + ecdsa_puk_output_addr, + ecdsa_puk_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values + return None + + def tp_oem_gen_master_share( + self, + oem_share_input_addr: int, + oem_share_input_size: int, + oem_enc_share_output_addr: int, + oem_enc_share_output_size: int, + oem_enc_master_share_output_addr: int, + oem_enc_master_share_output_size: int, + oem_cust_cert_puk_output_addr: int, + oem_cust_cert_puk_output_size: int, + ) -> Optional[List[int]]: + """Takes the entropy seed provided by the OEM as input. + + :param oem_share_input_addr: The input buffer address + where the OEM Share(entropy seed) locates at + :param oem_share_input_size: The byte count of the OEM Share + :param oem_enc_share_output_addr: The output buffer address + where ROM writes the Encrypted OEM Share to + :param oem_enc_share_output_size: The output buffer size in byte + :param oem_enc_master_share_output_addr: The output buffer address + where ROM writes the Encrypted OEM Master Share to + :param oem_enc_master_share_output_size: The output buffer size in byte. + :param oem_cust_cert_puk_output_addr: The output buffer address where + ROM writes the OEM Customer Certificate Public Key to + :param oem_cust_cert_puk_output_size: The output buffer size in byte + :return: Sizes of two encrypted blobs(the Encrypted OEM Share and the Encrypted OEM Master Share) + and a public key(the OEM Customer Certificate Public Key). + """ + logger.info("CMD: [TrustProvisioning] OEM generate master share") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.OEM_GEN_MASTER_SHARE.tag, + oem_share_input_addr, + oem_share_input_size, + oem_enc_share_output_addr, + oem_enc_share_output_size, + oem_enc_master_share_output_addr, + oem_enc_master_share_output_size, + oem_cust_cert_puk_output_addr, + oem_cust_cert_puk_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values + return None + + def tp_oem_set_master_share( + self, + oem_share_input_addr: int, + oem_share_input_size: int, + oem_enc_master_share_input_addr: int, + oem_enc_master_share_input_size: int, + ) -> bool: + """Takes the entropy seed and the Encrypted OEM Master Share. + + :param oem_share_input_addr: The input buffer address + where the OEM Share(entropy seed) locates at + :param oem_share_input_size: The byte count of the OEM Share + :param oem_enc_master_share_input_addr: The input buffer address + where the Encrypted OEM Master Share locates at + :param oem_enc_master_share_input_size: The byte count of the Encrypted OEM Master Share + :return: False in case of any problem; True otherwise + """ + logger.info( + "CMD: [TrustProvisioning] Takes the entropy seed and the Encrypted OEM Master Share." + ) + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.OEM_SET_MASTER_SHARE.tag, + oem_share_input_addr, + oem_share_input_size, + oem_enc_master_share_input_addr, + oem_enc_master_share_input_size, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def tp_oem_get_cust_cert_dice_puk( + self, + oem_rkth_input_addr: int, + oem_rkth_input_size: int, + oem_cust_cert_dice_puk_output_addr: int, + oem_cust_cert_dice_puk_output_size: int, + ) -> Optional[int]: + """Creates the initial trust provisioning keys. + + :param oem_rkth_input_addr: The input buffer address where the OEM RKTH locates at + :param oem_rkth_input_size: The byte count of the OEM RKTH + :param oem_cust_cert_dice_puk_output_addr: The output buffer address where ROM writes the OEM Customer + Certificate Public Key for DICE to + :param oem_cust_cert_dice_puk_output_size: The output buffer size in byte + :return: The byte count of the OEM Customer Certificate Public Key for DICE + """ + logger.info( + "CMD: [TrustProvisioning] Creates the initial trust provisioning keys" + ) + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.OEM_GET_CUST_CERT_DICE_PUK.tag, + oem_rkth_input_addr, + oem_rkth_input_size, + oem_cust_cert_dice_puk_output_addr, + oem_cust_cert_dice_puk_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def tp_hsm_store_key( + self, + key_type: int, + key_property: int, + key_input_addr: int, + key_input_size: int, + key_blob_output_addr: int, + key_blob_output_size: int, + ) -> Optional[List[int]]: + """Trust provisioning: OEM generate common keys. + + :param key_type: Key to generate (CKDFK, HKDFK, HMACK, CMACK, AESK, KUOK) + :param key_property: Bit 0: Key Size, 0 for 128bit, 1 for 256bit. + Bits 30-31: set key protection CSS mode. + :param key_input_addr: The input buffer address where the key locates at + :param key_input_size: The byte count of the key + :param key_blob_output_addr: The output buffer address where ROM writes the key blob to + :param key_blob_output_size: The output buffer size in byte + :return: Return header of the key blob + byte count of the key blob + (header is not included) from the device; None in case of an failure + """ + logger.info("CMD: [TrustProvisioning] OEM generate common keys") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.HSM_STORE_KEY.tag, + key_type, + key_property, + key_input_addr, + key_input_size, + key_blob_output_addr, + key_blob_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values + return None + + def tp_hsm_enc_blk( + self, + mfg_cust_mk_sk_0_blob_input_addr: int, + mfg_cust_mk_sk_0_blob_input_size: int, + kek_id: int, + sb3_header_input_addr: int, + sb3_header_input_size: int, + block_num: int, + block_data_addr: int, + block_data_size: int, + ) -> bool: + """Trust provisioning: Encrypt the given SB3 data block. + + :param mfg_cust_mk_sk_0_blob_input_addr: The input buffer address + where the CKDF Master Key Blob locates at + :param mfg_cust_mk_sk_0_blob_input_size: The byte count of the CKDF Master Key Blob + :param kek_id: The CKDF Master Key Encryption Key ID + (0x10: NXP_CUST_KEK_INT_SK, 0x11: NXP_CUST_KEK_EXT_SK) + :param sb3_header_input_addr: The input buffer address, + where the SB3 Header(block0) locates at + :param sb3_header_input_size: The byte count of the SB3 Header + :param block_num: The index of the block. Due to SB3 Header(block 0) is always unencrypted, + the index starts from block1 + :param block_data_addr: The buffer address where the SB3 data block locates at + :param block_data_size: The byte count of the SB3 data block + :return: False in case of any problem; True otherwise + """ + logger.info("CMD: [TrustProvisioning] Encrypt the given SB3 data block") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.HSM_ENC_BLOCK.tag, + mfg_cust_mk_sk_0_blob_input_addr, + mfg_cust_mk_sk_0_blob_input_size, + kek_id, + sb3_header_input_addr, + sb3_header_input_size, + block_num, + block_data_addr, + block_data_size, + ) + return self._process_cmd(cmd_packet).status == StatusCode.SUCCESS + + def tp_hsm_enc_sign( + self, + key_blob_input_addr: int, + key_blob_input_size: int, + block_data_input_addr: int, + block_data_input_size: int, + signature_output_addr: int, + signature_output_size: int, + ) -> Optional[int]: + """Signs the given data. + + :param key_blob_input_addr: The input buffer address where signing key blob locates at + :param key_blob_input_size: The byte count of the signing key blob + :param block_data_input_addr: The input buffer address where the data locates at + :param block_data_input_size: The byte count of the data + :param signature_output_addr: The output buffer address where ROM writes the signature to + :param signature_output_size: The output buffer size in byte + :return: Return signature size; None in case of an failure + """ + logger.info("CMD: [TrustProvisioning] HSM ENC SIGN") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvOperation.HSM_ENC_SIGN.tag, + key_blob_input_addr, + key_blob_input_size, + block_data_input_addr, + block_data_input_size, + signature_output_addr, + signature_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def wpc_get_id( + self, + wpc_id_blob_addr: int, + wpc_id_blob_size: int, + ) -> Optional[int]: + """Command used for harvesting device ID blob. + + :param wpc_id_blob_addr: Buffer address + :param wpc_id_blob_size: Buffer size + """ + logger.info("CMD: [TrustProvisioning] WPC GET ID") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvWpc.WPC_GET_ID.tag, + wpc_id_blob_addr, + wpc_id_blob_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def nxp_get_id( + self, + id_blob_addr: int, + id_blob_size: int, + ) -> Optional[int]: + """Command used for harvesting device ID blob during wafer test as part of RTS flow. + + :param id_blob_addr: address of ID blob defined by Round-trip trust provisioning specification. + :param id_blob_size: length of buffer in bytes + """ + logger.info("CMD: [TrustProvisioning] NXP GET ID") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvWpc.NXP_GET_ID.tag, + id_blob_addr, + id_blob_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def wpc_insert_cert( + self, + wpc_cert_addr: int, + wpc_cert_len: int, + ec_id_offset: int, + wpc_puk_offset: int, + ) -> Optional[int]: + """Command used for certificate validation before it is written into flash. + + This command does following things: + Extracts ECID and WPC PUK from certificate + Validates ECID and WPC PUK. If both are OK it returns success. Otherwise returns fail + + :param wpc_cert_addr: address of inserted certificate + :param wpc_cert_len: length in bytes of inserted certificate + :param ec_id_offset: offset to 72-bit ECID + :param wpc_puk_offset: WPC PUK offset from beginning of inserted certificate + """ + logger.info("CMD: [TrustProvisioning] WPC INSERT CERT") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvWpc.WPC_INSERT_CERT.tag, + wpc_cert_addr, + wpc_cert_len, + ec_id_offset, + wpc_puk_offset, + ) + cmd_response = self._process_cmd(cmd_packet) + if cmd_response.status == StatusCode.SUCCESS: + return 0 + return None + + def wpc_sign_csr( + self, + csr_tbs_addr: int, + csr_tbs_len: int, + signature_addr: int, + signature_len: int, + ) -> Optional[int]: + """Command used sign CSR data (TBS portion). + + :param csr_tbs_addr: address of CSR-TBS data + :param csr_tbs_len: length in bytes of CSR-TBS data + :param signature_addr: address where to store signature + :param signature_len: expected length of signature + :return: actual signature length + """ + logger.info("CMD: [TrustProvisioning] WPC SIGN CSR-TBS DATA") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvWpc.WPC_SIGN_CSR.tag, + csr_tbs_addr, + csr_tbs_len, + signature_addr, + signature_len, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def dsc_hsm_create_session( + self, + oem_seed_input_addr: int, + oem_seed_input_size: int, + oem_share_output_addr: int, + oem_share_output_size: int, + ) -> Optional[int]: + """Command used by OEM to provide it share to create the initial trust provisioning keys. + + :param oem_seed_input_addr: address of 128-bit entropy seed value provided by the OEM. + :param oem_seed_input_size: OEM seed size in bytes + :param oem_share_output_addr: A 128-bit encrypted token. + :param oem_share_output_size: size in bytes + """ + logger.info("CMD: [TrustProvisioning] DSC HSM CREATE SESSION") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvDevHsmDsc.DSC_HSM_CREATE_SESSION.tag, + oem_seed_input_addr, + oem_seed_input_size, + oem_share_output_addr, + oem_share_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def dsc_hsm_enc_blk( + self, + sbx_header_input_addr: int, + sbx_header_input_size: int, + block_num: int, + block_data_addr: int, + block_data_size: int, + ) -> Optional[int]: + """Command used to encrypt the given block sliced by the nxpimage. + + This command is only supported after issuance of dsc_hsm_create_session. + + :param sbx_header_input_addr: SBx header containing file size, Firmware version and Timestamp data. + Except for hash digest of block 0, all other fields should be valid. + :param sbx_header_input_size: size of the header in bytes + :param block_num: Number of block + :param block_data_addr: Address of data block + :param block_data_size: Size of data block + """ + logger.info("CMD: [TrustProvisioning] DSC HSM ENC BLK") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvDevHsmDsc.DSC_HSM_ENC_BLK.tag, + sbx_header_input_addr, + sbx_header_input_size, + block_num, + block_data_addr, + block_data_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + def dsc_hsm_enc_sign( + self, + block_data_input_addr: int, + block_data_input_size: int, + signature_output_addr: int, + signature_output_size: int, + ) -> Optional[int]: + """Command used for signing the data buffer provided. + + This command is only supported after issuance of dsc_hsm_create_session. + + :param block_data_input_addr: Address of data buffer to be signed + :param block_data_input_size: Size of data buffer in bytes + :param signature_output_addr: Address to output signature data + :param signature_output_size: Size of the output signature data in bytes + """ + logger.info("CMD: [TrustProvisioning] DSC HSM ENC SIGN") + cmd_packet = CmdPacket( + CommandTag.TRUST_PROVISIONING, + CommandFlag.NONE.tag, + TrustProvDevHsmDsc.DSC_HSM_ENC_SIGN.tag, + block_data_input_addr, + block_data_input_size, + signature_output_addr, + signature_output_size, + ) + cmd_response = self._process_cmd(cmd_packet) + if isinstance(cmd_response, TrustProvisioningResponse): + return cmd_response.values[0] + return None + + +#################### +# Helper functions # +#################### + + +def _tp_sentinel_frame( + command: int, args: List[int], tag: int = 0x17, version: int = 0 +) -> bytes: + """Prepare frame used by sentinel.""" + data = struct.pack("<4B", command, len(args), version, tag) + for item in args: + data += struct.pack(" int: + if memory_id > 255 or memory_id == 0: + return memory_id + logger.warning( + "Note: memoryId is not required when accessing mapped external memory" + ) + return 0 diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/memories.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/memories.py new file mode 100644 index 0000000..eb00e60 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/memories.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Various types of memory identifiers used in the MBoot module.""" + +from typing import List, Optional + +from ..utils.misc import size_fmt +from ..utils.spsdk_enum import SpsdkEnum + +LEGACY_MEM_ID = { + "internal": "INTERNAL", + "qspi": "QSPI", + "fuse": "FUSE", + "ifr": "IFR0", + "semcnor": "SEMC_NOR", + "flexspinor": "FLEX-SPI-NOR", + "semcnand": "SEMC-NAND", + "spinand": "SPI-NAND", + "spieeprom": "SPI-MEM", + "i2ceeprom": "I2C-MEM", + "sdcard": "SD", + "mmccard": "MMC", +} + + +######################################################################################################################## +# McuBoot External Memory ID +######################################################################################################################## +class MemIdEnum(SpsdkEnum): + """McuBoot Memory Base class.""" + + @classmethod + def get_legacy_str(cls, key: str) -> Optional[int]: + """Converts legacy str to new enum key. + + :param key: str value of legacy enum + :return: new enum value + """ + new_key = LEGACY_MEM_ID.get(key) + return cls.get_tag(new_key) if new_key else None + + @classmethod + def get_legacy_int(cls, key: int) -> Optional[str]: + """Converts legacy int to new enum key. + + :param key: int value of legacy enum + :return: new enum value + """ + if isinstance(key, int): + new_value = cls.from_tag(key) + if new_value: + return [k for k, v in LEGACY_MEM_ID.items() if v == new_value.label][0] + + return None + + +class ExtMemId(MemIdEnum): + """McuBoot External Memory Property Tags.""" + + QUAD_SPI0 = (1, "QSPI", "Quad SPI Memory 0") + IFR = (4, "IFR0", "Nonvolatile information register 0 (only used by SB loader)") + FUSE = (4, "FUSE", "Nonvolatile information register 0 (only used by SB loader)") + SEMC_NOR = (8, "SEMC-NOR", "SEMC NOR Memory") + FLEX_SPI_NOR = (9, "FLEX-SPI-NOR", "Flex SPI NOR Memory") + SPIFI_NOR = (10, "SPIFI-NOR", "SPIFI NOR Memory") + FLASH_EXEC_ONLY = (16, "FLASH-EXEC", "Execute-Only region on internal Flash") + SEMC_NAND = (256, "SEMC-NAND", "SEMC NAND Memory") + SPI_NAND = (257, "SPI-NAND", "SPI NAND Memory") + SPI_NOR_EEPROM = (272, "SPI-MEM", "SPI NOR/EEPROM Memory") + I2C_NOR_EEPROM = (273, "I2C-MEM", "I2C NOR/EEPROM Memory") + SD_CARD = (288, "SD", "eSD/SD/SDHC/SDXC Memory Card") + MMC_CARD = (289, "MMC", "MMC/eMMC Memory Card") + + +class MemId(MemIdEnum): + """McuBoot Internal/External Memory Property Tags.""" + + INTERNAL_MEMORY = ( + 0, + "RAM/FLASH", + "Internal RAM/FLASH (Used for the PRINCE configuration)", + ) + QUAD_SPI0 = (1, "QSPI", "Quad SPI Memory 0") + IFR = (4, "IFR0", "Nonvolatile information register 0 (only used by SB loader)") + FUSE = (4, "FUSE", "Nonvolatile information register 0 (only used by SB loader)") + SEMC_NOR = (8, "SEMC-NOR", "SEMC NOR Memory") + FLEX_SPI_NOR = (9, "FLEX-SPI-NOR", "Flex SPI NOR Memory") + SPIFI_NOR = (10, "SPIFI-NOR", "SPIFI NOR Memory") + FLASH_EXEC_ONLY = (16, "FLASH-EXEC", "Execute-Only region on internal Flash") + SEMC_NAND = (256, "SEMC-NAND", "SEMC NAND Memory") + SPI_NAND = (257, "SPI-NAND", "SPI NAND Memory") + SPI_NOR_EEPROM = (272, "SPI-MEM", "SPI NOR/EEPROM Memory") + I2C_NOR_EEPROM = (273, "I2C-MEM", "I2C NOR/EEPROM Memory") + SD_CARD = (288, "SD", "eSD/SD/SDHC/SDXC Memory Card") + MMC_CARD = (289, "MMC", "MMC/eMMC Memory Card") + + +######################################################################################################################## +# McuBoot External Memory Property Tags +######################################################################################################################## + + +class ExtMemPropTags(SpsdkEnum): + """McuBoot External Memory Property Tags.""" + + INIT_STATUS = (0x00000000, "INIT_STATUS") + START_ADDRESS = (0x00000001, "START_ADDRESS") + SIZE_IN_KBYTES = (0x00000002, "SIZE_IN_KBYTES") + PAGE_SIZE = (0x00000004, "PAGE_SIZE") + SECTOR_SIZE = (0x00000008, "SECTOR_SIZE") + BLOCK_SIZE = (0x00000010, "BLOCK_SIZE") + + +class MemoryRegion: + """Base class for memory regions.""" + + def __init__(self, start: int, end: int) -> None: + """Initialize the memory region object. + + :param start: start address of region + :param end: end address of region + + """ + self.start = start + self.end = end + self.size = end - start + 1 + + def __repr__(self) -> str: + return f"Memory region, start: {hex(self.start)}" + + def __str__(self) -> str: + return ( + f"0x{self.start:08X} - 0x{self.end:08X}; Total Size: {size_fmt(self.size)}" + ) + + +class RamRegion(MemoryRegion): + """RAM memory regions.""" + + def __init__(self, index: int, start: int, size: int) -> None: + """Initialize the RAM memory region object. + + :param index: number of region + :param start: start address of region + :param size: size of region + + """ + super().__init__(start, start + size - 1) + self.index = index + + def __repr__(self) -> str: + return f"RAM Memory region, start: {hex(self.start)}" + + def __str__(self) -> str: + return f"Region {self.index}: {super().__str__()}" + + +class FlashRegion(MemoryRegion): + """Flash memory regions.""" + + def __init__(self, index: int, start: int, size: int, sector_size: int) -> None: + """Initialize the Flash memory region object. + + :param index: number of region + :param start: start address of region + :param size: size of region + :param sector_size: size of sector + + """ + super().__init__(start, start + size - 1) + self.index = index + self.sector_size = sector_size + + def __repr__(self) -> str: + return f"Flash Memory region, start: {hex(self.start)}" + + def __str__(self) -> str: + msg = f"Region {self.index}: {super().__str__()} Sector size: {size_fmt(self.sector_size)}" + return msg + + +class ExtMemRegion(MemoryRegion): + """External memory regions.""" + + def __init__(self, mem_id: int, raw_values: Optional[List[int]] = None) -> None: + """Initialize the external memory region object. + + :param mem_id: ID of the external memory + :param raw_values: List of integers representing the property + + """ + self.mem_id = mem_id + if not raw_values: + self.value = None + return + super().__init__(0, 0) + self.start_address = ( + raw_values[1] if raw_values[0] & ExtMemPropTags.START_ADDRESS.tag else None + ) + self.total_size = ( + raw_values[2] * 1024 + if raw_values[0] & ExtMemPropTags.SIZE_IN_KBYTES.tag + else None + ) + self.page_size = ( + raw_values[3] if raw_values[0] & ExtMemPropTags.PAGE_SIZE.tag else None + ) + self.sector_size = ( + raw_values[4] if raw_values[0] & ExtMemPropTags.SECTOR_SIZE.tag else None + ) + self.block_size = ( + raw_values[5] if raw_values[0] & ExtMemPropTags.BLOCK_SIZE.tag else None + ) + self.value = raw_values[0] + + @property + def name(self) -> str: + """Get the name of external memory for given memory ID.""" + return ExtMemId.get_label(self.mem_id) + + def __repr__(self) -> str: + return f"EXT Memory region, name: {self.name}, start: {hex(self.start)}" + + def __str__(self) -> str: + if not self.value: + return "Not Configured" + info = f"Start Address = 0x{self.start_address:08X} " + if self.total_size: + info += f"Total Size = {size_fmt(self.total_size)} " + info += f"Page Size = {self.page_size} " + info += f"Sector Size = {self.sector_size} " + if self.block_size: + info += f"Block Size = {self.block_size} " + return info diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/properties.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/properties.py new file mode 100644 index 0000000..26d65b0 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/properties.py @@ -0,0 +1,868 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2016-2018 Martin Olejar +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Helper module for more human-friendly interpretation of the target device properties.""" + + +import ctypes +from copy import deepcopy +from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union + +from ..exceptions import SPSDKKeyError +from ..mboot.exceptions import McuBootError +from ..utils.misc import Endianness +from ..utils.spsdk_enum import SpsdkEnum +from .commands import CommandTag +from .error_codes import StatusCode +from .memories import ExtMemPropTags, MemoryRegion + + +######################################################################################################################## +# McuBoot helper functions +######################################################################################################################## +def size_fmt(value: Union[int, float], kibibyte: bool = True) -> str: + """Convert size value into string format. + + :param value: The raw value + :param kibibyte: True if 1024 Bytes represent 1kB or False if 1000 Bytes represent 1kB + :return: Stringified value + """ + base, suffix = [(1000.0, "B"), (1024.0, "iB")][kibibyte] + x = "B" + for x in ["B"] + [prefix + suffix for prefix in list("kMGTP")]: + if -base < value < base: + break + value /= base + + return f"{value} {x}" if x == "B" else f"{value:3.1f} {x}" + + +######################################################################################################################## +# McuBoot helper classes +######################################################################################################################## + + +class Version: + """McuBoot current and target version type.""" + + def __init__(self, *args: Union[str, int], **kwargs: int): + """Initialize the Version object. + + :raises McuBootError: Argument passed the not str not int + """ + self.mark = kwargs.get("mark", "K") + self.major = kwargs.get("major", 0) + self.minor = kwargs.get("minor", 0) + self.fixation = kwargs.get("fixation", 0) + if args: + if isinstance(args[0], int): + self.from_int(args[0]) + elif isinstance(args[0], str): + self.from_str(args[0]) + else: + raise McuBootError("Value must be 'str' or 'int' type !") + + def __eq__(self, obj: object) -> bool: + return isinstance(obj, Version) and vars(obj) == vars(self) + + def __ne__(self, obj: object) -> bool: + return not self.__eq__(obj) + + def __lt__(self, obj: "Version") -> bool: + return self.to_int(True) < obj.to_int(True) + + def __le__(self, obj: "Version") -> bool: + return self.to_int(True) <= obj.to_int(True) + + def __gt__(self, obj: "Version") -> bool: + return self.to_int(True) > obj.to_int(True) + + def __ge__(self, obj: "Version") -> bool: + return self.to_int(True) >= obj.to_int(True) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.to_str() + + def from_int(self, value: int) -> None: + """Parse version data from raw int value. + + :param value: Raw integer input + """ + mark = (value >> 24) & 0xFF + self.mark = chr(mark) if 64 < mark < 91 else None # type: ignore + self.major = (value >> 16) & 0xFF + self.minor = (value >> 8) & 0xFF + self.fixation = value & 0xFF + + def from_str(self, value: str) -> None: + """Parse version data from string value. + + :param value: String representation input + """ + mark_major, minor, fixation = value.split(".") + if len(mark_major) > 1 and mark_major[0] not in "0123456789": + self.mark = mark_major[0] + self.major = int(mark_major[1:]) + else: + self.major = int(mark_major) + self.minor = int(minor) + self.fixation = int(fixation) + + def to_int(self, no_mark: bool = False) -> int: + """Get version value in raw integer format. + + :param no_mark: If True, return value without mark + :return: Integer representation + """ + value = self.major << 16 | self.minor << 8 | self.fixation + mark = 0 if no_mark or self.mark is None else ord(self.mark) << 24 # type: ignore + return value | mark + + def to_str(self, no_mark: bool = False) -> str: + """Get version value in readable string format. + + :param no_mark: If True, return value without mark + :return: String representation + """ + value = f"{self.major}.{self.minor}.{self.fixation}" + mark = "" if no_mark or self.mark is None else self.mark + return f"{mark}{value}" + + +######################################################################################################################## +# McuBoot Properties +######################################################################################################################## + +# fmt: off +class PropertyTag(SpsdkEnum): + """McuBoot Properties.""" + + LIST_PROPERTIES = (0x00, 'ListProperties', 'List Properties') + CURRENT_VERSION = (0x01, "CurrentVersion", "Current Version") + AVAILABLE_PERIPHERALS = (0x02, "AvailablePeripherals", "Available Peripherals") + FLASH_START_ADDRESS = (0x03, "FlashStartAddress", "Flash Start Address") + FLASH_SIZE = (0x04, "FlashSize", "Flash Size") + FLASH_SECTOR_SIZE = (0x05, "FlashSectorSize", "Flash Sector Size") + FLASH_BLOCK_COUNT = (0x06, "FlashBlockCount", "Flash Block Count") + AVAILABLE_COMMANDS = (0x07, "AvailableCommands", "Available Commands") + CRC_CHECK_STATUS = (0x08, "CrcCheckStatus", "CRC Check Status") + LAST_ERROR = (0x09, "LastError", "Last Error Value") + VERIFY_WRITES = (0x0A, "VerifyWrites", "Verify Writes") + MAX_PACKET_SIZE = (0x0B, "MaxPacketSize", "Max Packet Size") + RESERVED_REGIONS = (0x0C, "ReservedRegions", "Reserved Regions") + VALIDATE_REGIONS = (0x0D, "ValidateRegions", "Validate Regions") + RAM_START_ADDRESS = (0x0E, "RamStartAddress", "RAM Start Address") + RAM_SIZE = (0x0F, "RamSize", "RAM Size") + SYSTEM_DEVICE_IDENT = (0x10, "SystemDeviceIdent", "System Device Identification") + FLASH_SECURITY_STATE = (0x11, "FlashSecurityState", "Security State") + UNIQUE_DEVICE_IDENT = (0x12, "UniqueDeviceIdent", "Unique Device Identification") + FLASH_FAC_SUPPORT = (0x13, "FlashFacSupport", "Flash Fac. Support") + FLASH_ACCESS_SEGMENT_SIZE = (0x14, "FlashAccessSegmentSize", "Flash Access Segment Size",) + FLASH_ACCESS_SEGMENT_COUNT = (0x15, "FlashAccessSegmentCount", "Flash Access Segment Count",) + FLASH_READ_MARGIN = (0x16, "FlashReadMargin", "Flash Read Margin") + QSPI_INIT_STATUS = (0x17, "QspiInitStatus", "QuadSPI Initialization Status") + TARGET_VERSION = (0x18, "TargetVersion", "Target Version") + EXTERNAL_MEMORY_ATTRIBUTES = (0x19, "ExternalMemoryAttributes", "External Memory Attributes",) + RELIABLE_UPDATE_STATUS = (0x1A, "ReliableUpdateStatus", "Reliable Update Status") + FLASH_PAGE_SIZE = (0x1B, "FlashPageSize", "Flash Page Size") + IRQ_NOTIFIER_PIN = (0x1C, "IrqNotifierPin", "Irq Notifier Pin") + PFR_KEYSTORE_UPDATE_OPT = (0x1D, "PfrKeystoreUpdateOpt", "PFR Keystore Update Opt") + BYTE_WRITE_TIMEOUT_MS = (0x1E, "ByteWriteTimeoutMs", "Byte Write Timeout in ms") + FUSE_LOCKED_STATUS = (0x1F, "FuseLockedStatus", "Fuse Locked Status") + UNKNOWN = (0xFF, "Unknown", "Unknown property") + + +class PropertyTagKw45xx(SpsdkEnum): + """McuBoot Properties.""" + + VERIFY_ERASE = (0x0A, "VerifyErase", "Verify Erase") + BOOT_STATUS_REGISTER = (0x14, "BootStatusRegister", "Boot Status Register",) + FIRMWARE_VERSION = (0x15, "FirmwareVersion", "Firmware Version",) + FUSE_PROGRAM_VOLTAGE = (0x16, "FuseProgramVoltage", "Fuse Program Voltage") + + +class PeripheryTag(SpsdkEnum): + """Tags representing peripherals.""" + + UART = (0x01, "UART", "UART Interface") + I2C_SLAVE = (0x02, "I2C-Slave", "I2C Slave Interface") + SPI_SLAVE = (0x04, "SPI-Slave", "SPI Slave Interface") + CAN = (0x08, "CAN", "CAN Interface") + USB_HID = (0x10, "USB-HID", "USB HID-Class Interface") + USB_CDC = (0x20, "USB-CDC", "USB CDC-Class Interface") + USB_DFU = (0x40, "USB-DFU", "USB DFU-Class Interface") + LIN = (0x80, "LIN", "LIN Interface") + + +class FlashReadMargin(SpsdkEnum): + """Scopes for flash read.""" + + NORMAL = (0, "NORMAL") + USER = (1, "USER") + FACTORY = (2, "FACTORY") + + +class PfrKeystoreUpdateOpt(SpsdkEnum): + """Options for PFR updating.""" + + KEY_PROVISIONING = (0, "KEY_PROVISIONING", "KeyProvisioning") + WRITE_MEMORY = (1, "WRITE_MEMORY", "WriteMemory") +# fmt: on + +######################################################################################################################## +# McuBoot Properties Values +######################################################################################################################## + + +class PropertyValueBase: + """Base class for property value.""" + + __slots__ = ("tag", "name", "desc") + + def __init__( + self, tag: int, name: Optional[str] = None, desc: Optional[str] = None + ) -> None: + """Initialize the base of property. + + :param tag: Property tag, see: `PropertyTag` + :param name: Optional name for the property + :param desc: Optional description for the property + """ + self.tag = tag + self.name = name or PropertyTag.get_label(tag) or "" + self.desc = desc or PropertyTag.get_description(tag, "") + + def __str__(self) -> str: + return f"{self.desc} = {self.to_str()}" + + def to_str(self) -> str: + """Stringified representation of a property. + + Derived classes should implement this function. + + :return: String representation + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + +class IntValue(PropertyValueBase): + """Integer-based value property.""" + + __slots__ = ( + "value", + "_fmt", + ) + + def __init__( + self, tag: int, raw_values: List[int], str_format: str = "dec" + ) -> None: + """Initialize the integer-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + :param str_format: Format to display the value ('dec', 'hex', 'size') + """ + super().__init__(tag) + self._fmt = str_format + self.value = raw_values[0] + + def to_int(self) -> int: + """Get the raw integer property representation.""" + return self.value + + def to_str(self) -> str: + """Get stringified property representation.""" + if self._fmt == "size": + str_value = size_fmt(self.value) + elif self._fmt == "hex": + str_value = f"0x{self.value:08X}" + elif self._fmt == "dec": + str_value = str(self.value) + elif self._fmt == "int32": + str_value = str(ctypes.c_int32(self.value).value) + else: + str_value = self._fmt.format(self.value) + return str_value + + +class BoolValue(PropertyValueBase): + """Boolean-based value property.""" + + __slots__ = ( + "value", + "_true_values", + "_false_values", + "_true_string", + "_false_string", + ) + + def __init__( + self, + tag: int, + raw_values: List[int], + true_values: Tuple[int] = (1,), + true_string: str = "YES", + false_values: Tuple[int] = (0,), + false_string: str = "NO", + ) -> None: + """Initialize the Boolean-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + :param true_values: Values representing 'True', defaults to (1,) + :param true_string: String representing 'True, defaults to 'YES' + :param false_values: Values representing 'False', defaults to (0,) + :param false_string: String representing 'False, defaults to 'NO' + """ + super().__init__(tag) + self._true_values = true_values + self._true_string = true_string + self._false_values = false_values + self._false_string = false_string + self.value = raw_values[0] + + def __bool__(self) -> bool: + return self.value in self._true_values + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return self.value + + def to_str(self) -> str: + """Get stringified property representation.""" + return ( + self._true_string if self.value in self._true_values else self._false_string + ) + + +class EnumValue(PropertyValueBase): + """Enumeration value property.""" + + __slots__ = ("value", "enum", "_na_msg") + + def __init__( + self, + tag: int, + raw_values: List[int], + enum: Type[SpsdkEnum], + na_msg: str = "Unknown Item", + ) -> None: + """Initialize the enumeration-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + :param enum: Enumeration to pick from + :param na_msg: Message to display if an item is not found in the enum + """ + super().__init__(tag) + self._na_msg = na_msg + self.enum = enum + self.value = raw_values[0] + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return self.value + + def to_str(self) -> str: + """Get stringified property representation.""" + try: + return self.enum.get_label(self.value) + except SPSDKKeyError: + return f"{self._na_msg}: {self.value}" + + +class VersionValue(PropertyValueBase): + """Version property class.""" + + __slots__ = ("value",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the Version-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = Version(raw_values[0]) + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return self.value.to_int() + + def to_str(self) -> str: + """Get stringified property representation.""" + return self.value.to_str() + + +class DeviceUidValue(PropertyValueBase): + """Device UID value property.""" + + __slots__ = ("value",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the Version-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = b"".join( + [ + int.to_bytes(val, length=4, byteorder=Endianness.LITTLE.value) + for val in raw_values + ] + ) + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return int.from_bytes(self.value, byteorder=Endianness.BIG.value) + + def to_str(self) -> str: + """Get stringified property representation.""" + return " ".join(f"{item:02X}" for item in self.value) + + +class ReservedRegionsValue(PropertyValueBase): + """Reserver Regions property.""" + + __slots__ = ("regions",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the ReserverRegion-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.regions: List[MemoryRegion] = [] + for i in range(0, len(raw_values), 2): + if raw_values[i + 1] == 0: + continue + self.regions.append(MemoryRegion(raw_values[i], raw_values[i + 1])) + + def __str__(self) -> str: + return f"{self.desc} =\n{self.to_str()}" + + def to_str(self) -> str: + """Get stringified property representation.""" + return "\n".join( + [f" Region {i}: {region}" for i, region in enumerate(self.regions)] + ) + + +class AvailablePeripheralsValue(PropertyValueBase): + """Available Peripherals property.""" + + __slots__ = ("value",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the AvailablePeripherals-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = raw_values[0] + + def to_int(self) -> int: + """Get the raw integer portion of the property.""" + return self.value + + def to_str(self) -> str: + """Get stringified property representation.""" + return ", ".join( + [ + peripheral_tag.label + for peripheral_tag in PeripheryTag + if peripheral_tag.tag & self.value + ] + ) + + +class AvailableCommandsValue(PropertyValueBase): + """Available commands property.""" + + __slots__ = ("value",) + + @property + def tags(self) -> List[str]: + """List of tags representing Available commands.""" + return [ + cmd_tag.tag # type: ignore + for cmd_tag in CommandTag + if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value + ] + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the AvailableCommands-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = raw_values[0] + + def __contains__(self, item: int) -> bool: + return isinstance(item, int) and bool((1 << item - 1) & self.value) + + def to_str(self) -> str: + """Get stringified property representation.""" + return [ + cmd_tag.label # type: ignore + for cmd_tag in CommandTag + if cmd_tag.tag > 0 and (1 << cmd_tag.tag - 1) & self.value + ] + + +class IrqNotifierPinValue(PropertyValueBase): + """IRQ notifier pin property.""" + + __slots__ = ("value",) + + @property + def pin(self) -> int: + """Number of the pin used for reporting IRQ.""" + return self.value & 0xFF + + @property + def port(self) -> int: + """Number of the port used for reporting IRQ.""" + return (self.value >> 8) & 0xFF + + @property + def enabled(self) -> bool: + """Indicates whether IRQ reporting is enabled.""" + return bool(self.value & (1 << 32)) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the IrqNotifierPin-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.value = raw_values[0] + + def __bool__(self) -> bool: + return self.enabled + + def to_str(self) -> str: + """Get stringified property representation.""" + return f"IRQ Port[{self.port}], Pin[{self.pin}] is {'enabled' if self.enabled else 'disabled'}" + + +class ExternalMemoryAttributesValue(PropertyValueBase): + """Attributes for external memories.""" + + __slots__ = ( + "value", + "mem_id", + "start_address", + "total_size", + "page_size", + "sector_size", + "block_size", + ) + + def __init__(self, tag: int, raw_values: List[int], mem_id: int = 0) -> None: + """Initialize the ExternalMemoryAttributes-based property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + :param mem_id: ID of the external memory + """ + super().__init__(tag) + self.mem_id = mem_id + self.start_address = ( + raw_values[1] if raw_values[0] & ExtMemPropTags.START_ADDRESS.tag else None + ) + self.total_size = ( + raw_values[2] * 1024 + if raw_values[0] & ExtMemPropTags.SIZE_IN_KBYTES.tag + else None + ) + self.page_size = ( + raw_values[3] if raw_values[0] & ExtMemPropTags.PAGE_SIZE.tag else None + ) + self.sector_size = ( + raw_values[4] if raw_values[0] & ExtMemPropTags.SECTOR_SIZE.tag else None + ) + self.block_size = ( + raw_values[5] if raw_values[0] & ExtMemPropTags.BLOCK_SIZE.tag else None + ) + self.value = raw_values[0] + + def to_str(self) -> str: + """Get stringified property representation.""" + str_values = [] + if self.start_address is not None: + str_values.append(f"Start Address: 0x{self.start_address:08X}") + if self.total_size is not None: + str_values.append(f"Total Size: {size_fmt(self.total_size)}") + if self.page_size is not None: + str_values.append(f"Page Size: {size_fmt(self.page_size)}") + if self.sector_size is not None: + str_values.append(f"Sector Size: {size_fmt(self.sector_size)}") + if self.block_size is not None: + str_values.append(f"Block Size: {size_fmt(self.block_size)}") + return ", ".join(str_values) + + +class FuseLock: + """Fuse Lock.""" + + def __init__(self, index: int, locked: bool) -> None: + """Initialize object representing information about fuse lock. + + :param index: value of OTP index + :param locked: status of the lock, true if locked + """ + self.index = index + self.locked = locked + + def __str__(self) -> str: + status = "LOCKED" if self.locked else "UNLOCKED" + return f" FUSE{(self.index):03d}: {status}\r\n" + + +class FuseLockRegister: + """Fuse Lock Register.""" + + def __init__(self, value: int, index: int, start: int = 0) -> None: + """Initialize object representing the OTP Controller Program Locked Status. + + :param value: value of the register + :param index: index of the fuse + :param start: shift to the start of the register + + """ + self.value = value + self.index = index + self.msg = "" + self.bitfields: List[FuseLock] = [] + + shift = 0 + for _ in range(start, 32): + locked = (value >> shift) & 1 + self.bitfields.append(FuseLock(index + shift, bool(locked))) + shift += 1 + + def __str__(self) -> str: + """Get stringified property representation.""" + if self.bitfields: + for bitfield in self.bitfields: + self.msg += str(bitfield) + return f"\r\n{self.msg}" + + +class FuseLockedStatus(PropertyValueBase): + """Class representing FuseLocked registers.""" + + __slots__ = ("fuses",) + + def __init__(self, tag: int, raw_values: List[int]) -> None: + """Initialize the FuseLockedStatus property object. + + :param tag: Property tag, see: `PropertyTag` + :param raw_values: List of integers representing the property + """ + super().__init__(tag) + self.fuses: List[FuseLockRegister] = [] + idx = 0 + for count, val in enumerate(raw_values): + start = 0 + if count == 0: + start = 16 + self.fuses.append(FuseLockRegister(val, idx, start)) + idx += 32 + if count == 0: + idx -= 16 + + def to_str(self) -> str: + """Get stringified property representation.""" + msg = "\r\n" + for count, register in enumerate(self.fuses): + msg += f"OTP Controller Program Locked Status {count} Register: {register}" + return msg + + def get_fuses(self) -> List[FuseLock]: + """Get list of fuses bitfield objects. + + :return: list of FuseLockBitfield objects + """ + fuses = [] + for registers in self.fuses: + fuses.extend(registers.bitfields) + return fuses + + +######################################################################################################################## +# McuBoot property response parser +######################################################################################################################## + +PROPERTIES: Dict[PropertyTag, Dict[Any, Any]] = { + PropertyTag.CURRENT_VERSION: {"class": VersionValue, "kwargs": {}}, + PropertyTag.AVAILABLE_PERIPHERALS: { + "class": AvailablePeripheralsValue, + "kwargs": {}, + }, + PropertyTag.FLASH_START_ADDRESS: { + "class": IntValue, + "kwargs": {"str_format": "hex"}, + }, + PropertyTag.FLASH_SIZE: {"class": IntValue, "kwargs": {"str_format": "size"}}, + PropertyTag.FLASH_SECTOR_SIZE: { + "class": IntValue, + "kwargs": {"str_format": "size"}, + }, + PropertyTag.FLASH_BLOCK_COUNT: {"class": IntValue, "kwargs": {"str_format": "dec"}}, + PropertyTag.AVAILABLE_COMMANDS: {"class": AvailableCommandsValue, "kwargs": {}}, + PropertyTag.CRC_CHECK_STATUS: { + "class": EnumValue, + "kwargs": {"enum": StatusCode, "na_msg": "Unknown CRC Status code"}, + }, + PropertyTag.VERIFY_WRITES: { + "class": BoolValue, + "kwargs": {"true_string": "ON", "false_string": "OFF"}, + }, + PropertyTag.LAST_ERROR: { + "class": EnumValue, + "kwargs": {"enum": StatusCode, "na_msg": "Unknown Error"}, + }, + PropertyTag.MAX_PACKET_SIZE: {"class": IntValue, "kwargs": {"str_format": "size"}}, + PropertyTag.RESERVED_REGIONS: {"class": ReservedRegionsValue, "kwargs": {}}, + PropertyTag.VALIDATE_REGIONS: { + "class": BoolValue, + "kwargs": {"true_string": "ON", "false_string": "OFF"}, + }, + PropertyTag.RAM_START_ADDRESS: {"class": IntValue, "kwargs": {"str_format": "hex"}}, + PropertyTag.RAM_SIZE: {"class": IntValue, "kwargs": {"str_format": "size"}}, + PropertyTag.SYSTEM_DEVICE_IDENT: { + "class": IntValue, + "kwargs": {"str_format": "hex"}, + }, + PropertyTag.FLASH_SECURITY_STATE: { + "class": BoolValue, + "kwargs": { + "true_values": (0x00000000, 0x5AA55AA5), + "true_string": "UNSECURE", + "false_values": (0x00000001, 0xC33CC33C), + "false_string": "SECURE", + }, + }, + PropertyTag.UNIQUE_DEVICE_IDENT: {"class": DeviceUidValue, "kwargs": {}}, + PropertyTag.FLASH_FAC_SUPPORT: { + "class": BoolValue, + "kwargs": {"true_string": "ON", "false_string": "OFF"}, + }, + PropertyTag.FLASH_ACCESS_SEGMENT_SIZE: { + "class": IntValue, + "kwargs": {"str_format": "size"}, + }, + PropertyTag.FLASH_ACCESS_SEGMENT_COUNT: { + "class": IntValue, + "kwargs": {"str_format": "int32"}, + }, + PropertyTag.FLASH_READ_MARGIN: { + "class": EnumValue, + "kwargs": {"enum": FlashReadMargin, "na_msg": "Unknown Margin"}, + }, + PropertyTag.QSPI_INIT_STATUS: { + "class": EnumValue, + "kwargs": {"enum": StatusCode, "na_msg": "Unknown Error"}, + }, + PropertyTag.TARGET_VERSION: {"class": VersionValue, "kwargs": {}}, + PropertyTag.EXTERNAL_MEMORY_ATTRIBUTES: { + "class": ExternalMemoryAttributesValue, + "kwargs": {"mem_id": None}, + }, + PropertyTag.RELIABLE_UPDATE_STATUS: { + "class": EnumValue, + "kwargs": {"enum": StatusCode, "na_msg": "Unknown Error"}, + }, + PropertyTag.FLASH_PAGE_SIZE: {"class": IntValue, "kwargs": {"str_format": "size"}}, + PropertyTag.IRQ_NOTIFIER_PIN: {"class": IrqNotifierPinValue, "kwargs": {}}, + PropertyTag.PFR_KEYSTORE_UPDATE_OPT: { + "class": EnumValue, + "kwargs": {"enum": PfrKeystoreUpdateOpt, "na_msg": "Unknown"}, + }, + PropertyTag.BYTE_WRITE_TIMEOUT_MS: { + "class": IntValue, + "kwargs": {"str_format": "dec"}, + }, + PropertyTag.FUSE_LOCKED_STATUS: { + "class": FuseLockedStatus, + "kwargs": {}, + }, +} + +PROPERTIES_KW45XX = { + PropertyTagKw45xx.VERIFY_ERASE: { + "class": BoolValue, + "kwargs": {"true_string": "ENABLE", "false_string": "DISABLE"}, + }, + PropertyTagKw45xx.BOOT_STATUS_REGISTER: { + "class": IntValue, + "kwargs": {"str_format": "int32"}, + }, + PropertyTagKw45xx.FIRMWARE_VERSION: { + "class": IntValue, + "kwargs": {"str_format": "int32"}, + }, + PropertyTagKw45xx.FUSE_PROGRAM_VOLTAGE: { + "class": BoolValue, + "kwargs": { + "true_string": "Over Drive Voltage (2.5 V)", + "false_string": "Normal Voltage (1.8 V)", + }, + }, +} + +PROPERTIES_OVERRIDE = {"kw45xx": PROPERTIES_KW45XX, "k32w1xx": PROPERTIES_KW45XX} +PROPERTY_TAG_OVERRIDE = {"kw45xx": PropertyTagKw45xx, "k32w1xx": PropertyTagKw45xx} + + +def parse_property_value( + property_tag: int, + raw_values: List[int], + ext_mem_id: Optional[int] = None, + family: Optional[str] = None, +) -> Optional[PropertyValueBase]: + """Parse the property value received from the device. + + :param property_tag: Tag representing the property + :param raw_values: Data received from the device + :param ext_mem_id: ID of the external memory used to read the property, defaults to None + :param family: supported family + :return: Object representing the property + """ + assert isinstance(property_tag, int) + assert isinstance(raw_values, list) + properties_dict = deepcopy(PROPERTIES) + if family: + properties_dict.update(PROPERTIES_OVERRIDE[family]) # type: ignore + if property_tag not in list(properties_dict.keys()): + return None + property_value = next( + value for key, value in properties_dict.items() if key.tag == property_tag + ) + cls: Callable = property_value["class"] # type: ignore[type-arg] + kwargs: dict[str, Any] = property_value["kwargs"] + if "mem_id" in kwargs: + kwargs["mem_id"] = ext_mem_id + obj = cls(property_tag, raw_values, **kwargs) + if family: + property_tag_override = PROPERTY_TAG_OVERRIDE[family].from_tag(property_tag) + obj.name = property_tag_override.label + obj.desc = property_tag_override.description + assert isinstance(obj, PropertyValueBase) + return obj diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/__init__.py new file mode 100644 index 0000000..de8a321 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Mboot Protocols.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/base.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/base.py new file mode 100644 index 0000000..9733069 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/base.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""MBoot protocol base.""" +from ...utils.interfaces.protocol.protocol_base import ProtocolBase + + +class MbootProtocolBase(ProtocolBase): + """MBoot protocol base class.""" + + allow_abort: bool = False + need_data_split: bool = True diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py new file mode 100644 index 0000000..b3ef4f0 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/mboot/protocol/bulk_protocol.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Mboot bulk implementation.""" +import logging +from struct import pack, unpack_from +from typing import Optional, Union + +from nitrokey.trussed._bootloader.lpc55_upload.exceptions import SPSDKAttributeError + +from ...mboot.commands import CmdResponse, parse_cmd_response +from ...mboot.exceptions import McuBootConnectionError, McuBootDataAbortError +from ...mboot.protocol.base import MbootProtocolBase +from ...utils.exceptions import SPSDKTimeoutError +from ...utils.interfaces.commands import CmdPacketBase +from ...utils.spsdk_enum import SpsdkEnum + + +class ReportId(SpsdkEnum): + """Report ID enum.""" + + CMD_OUT = (0x01, "CMD_OUT") + CMD_IN = (0x03, "CMD_IN") + DATA_OUT = (0x02, "DATA_OUT") + DATA_IN = (0x04, "DATA_IN") + + +logger = logging.getLogger(__name__) + + +class MbootBulkProtocol(MbootProtocolBase): + """Mboot Bulk protocol.""" + + def open(self) -> None: + """Open the interface.""" + self.device.open() + + def close(self) -> None: + """Close the interface.""" + self.device.close() + + @property + def is_opened(self) -> bool: + """Indicates whether interface is open.""" + return self.device.is_opened + + def write_data(self, data: bytes) -> None: + """Encapsulate data into frames and send them to device. + + :param data: Data to be sent + """ + frame = self._create_frame(data, ReportId.DATA_OUT) + if self.allow_abort: + try: + abort_data = self.device.read(1024, timeout=10) + logger.debug(f"Read {len(abort_data)} bytes of abort data") + except Exception as e: + raise McuBootConnectionError(str(e)) from e + if abort_data: + logger.debug(f"{', '.join(f'{b:02X}' for b in abort_data)}") + raise McuBootDataAbortError() + self.device.write(frame) + + def write_command(self, packet: CmdPacketBase) -> None: + """Encapsulate command into frames and send them to device. + + :param packet: Command packet object to be sent + :raises SPSDKAttributeError: Command packed contains no data to be sent + """ + data = packet.to_bytes(padding=False) + if not data: + raise SPSDKAttributeError("Incorrect packet type") + frame = self._create_frame(data, ReportId.CMD_OUT) + self.device.write(frame) + + def read(self, length: Optional[int] = None) -> Union[CmdResponse, bytes]: + """Read data from device. + + :return: read data + :raises SPSDKTimeoutError: Timeout occurred + """ + data = self.device.read(1024) + if not data: + logger.error("Cannot read from HID device") + raise SPSDKTimeoutError() + return self._parse_frame(bytes(data)) + + def _create_frame(self, data: bytes, report_id: ReportId) -> bytes: + """Encode the USB packet. + + :param report_id: ID of the report (see: HID_REPORT) + :param data: Data to send + :return: Encoded bytes and length of the final report frame + """ + raw_data = pack("<2BH", report_id.tag, 0x00, len(data)) + raw_data += data + logger.debug(f"OUT[{len(raw_data)}]: {', '.join(f'{b:02X}' for b in raw_data)}") + return raw_data + + @staticmethod + def _parse_frame(raw_data: bytes) -> Union[CmdResponse, bytes]: + """Decodes the data read on USB interface. + + :param raw_data: Data received + :return: CmdResponse object or data read + :raises McuBootDataAbortError: Transaction aborted by target + """ + logger.debug(f"IN [{len(raw_data)}]: {', '.join(f'{b:02X}' for b in raw_data)}") + report_id, _, plen = unpack_from("<2BH", raw_data) + if plen == 0: + raise McuBootDataAbortError() + data = raw_data[4 : 4 + plen] + if report_id == ReportId.CMD_IN: + return parse_cmd_response(data) + return data diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/misc.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/misc.py new file mode 100644 index 0000000..09046b6 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/misc.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Miscellaneous functions in SBFile module.""" + +from datetime import datetime, timezone +from typing import Any, Sequence, Union + +from ..exceptions import SPSDKError +from ..utils import misc + + +class SecBootBlckSize: + """Helper methods allowing to convert size to number of blocks and back. + + Note: The class is not intended to be instantiated + """ + + # Size of cipher block in bytes + BLOCK_SIZE = 16 + + @staticmethod + def is_aligned(size: int) -> bool: + """Whether size is aligned to cipher block size. + + :param size: given size in bytes + :return: True if yes, False otherwise + """ + return size % SecBootBlckSize.BLOCK_SIZE == 0 + + @staticmethod + def align(size: int) -> int: + """Align given size to block size. + + :param size: in bytes + :return: size aligned up to block size + """ + return misc.align(size, SecBootBlckSize.BLOCK_SIZE) + + @staticmethod + def to_num_blocks(size: int) -> int: + """Converts size to number of cipher blocks. + + :param size: to be converted, the size must be aligned to block boundary + :return: corresponding number of cipher blocks + :raises SPSDKError: Raised when size is not aligned to block boundary + """ + if not SecBootBlckSize.is_aligned(size): + raise SPSDKError( + f"Invalid size {size}, expected number aligned to BLOCK size {SecBootBlckSize.BLOCK_SIZE}" + ) + return size // SecBootBlckSize.BLOCK_SIZE + + @staticmethod + def align_block_fill_random(data: bytes) -> bytes: + """Align block size to cipher block size. + + :param data: to be aligned + :return: data aligned to cipher block size, filled with random values + """ + return misc.align_block_fill_random(data, SecBootBlckSize.BLOCK_SIZE) + + +# the type represents input formats for BcdVersion3 value, see BcdVersion3.to_version +BcdVersion3Format = Union["BcdVersion3", str] + + +class BcdVersion3: + """Version in format #.#.#, where # is BCD number (1-4 digits).""" + + # default value + DEFAULT = "999.999.999" + + @staticmethod + def _check_number(num: int) -> bool: + """Check given number is a valid version number. + + :param num: to be checked + :return: True if number format is valid + :raises SPSDKError: If number format is not valid + """ + if num < 0 or num > 0x9999: + raise SPSDKError("Invalid number range") + for index in range(4): + if (num >> 4 * index) & 0xF > 0x9: + raise SPSDKError("Invalid number, contains digit > 9") + return True + + @staticmethod + def _num_from_str(text: str) -> int: + """Converts BCD number from text to int. + + :param text: given string to be converted to a version number + :return: version number + :raises SPSDKError: If format is not valid + """ + if len(text) < 0 or len(text) > 4: + raise SPSDKError("Invalid text length") + result = int(text, 16) + BcdVersion3._check_number(result) + return result + + @staticmethod + def from_str(text: str) -> "BcdVersion3": + """Convert string to BcdVersion instance. + + :param text: version in format #.#.#, where # is 1-4 decimal digits + :return: BcdVersion3 instance + :raises SPSDKError: If format is not valid + """ + parts = text.split(".") + if len(parts) != 3: + raise SPSDKError("Invalid length") + major = BcdVersion3._num_from_str(parts[0]) + minor = BcdVersion3._num_from_str(parts[1]) + service = BcdVersion3._num_from_str(parts[2]) + return BcdVersion3(major, minor, service) + + @staticmethod + def to_version(input_version: BcdVersion3Format) -> "BcdVersion3": + """Convert different input formats into BcdVersion3 instance. + + :param input_version: either directly BcdVersion3 or string + :raises SPSDKError: Raises when the format is unsupported + :return: BcdVersion3 instance + """ + if isinstance(input_version, BcdVersion3): + return input_version + if isinstance(input_version, str): + return BcdVersion3.from_str(input_version) + raise SPSDKError("unsupported format") + + def __init__(self, major: int = 1, minor: int = 0, service: int = 0): + """Initialize BcdVersion3. + + :param major: number in BCD format, 1-4 decimal digits + :param minor: number in BCD format, 1-4 decimal digits + :param service: number in BCD format, 1-4 decimal digits + :raises SPSDKError: Invalid version + """ + if not all( + [ + BcdVersion3._check_number(major), + BcdVersion3._check_number(minor), + BcdVersion3._check_number(service), + ] + ): + raise SPSDKError("Invalid version") + self.major = major + self.minor = minor + self.service = service + + def __str__(self) -> str: + return f"{self.major:X}.{self.minor:X}.{self.service:X}" + + def __repr__(self) -> str: + return self.__class__.__name__ + ": " + self.__str__() + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, BcdVersion3) + and (self.major == other.major) + and (self.minor == other.minor) + and (self.service == other.service) + ) + + @property + def nums(self) -> Sequence[int]: + """Return array of version numbers: [major, minor, service].""" + return [self.major, self.minor, self.service] + + +def pack_timestamp(value: datetime) -> int: + """Converts datetime to millisecond since 1.1.2000. + + :param value: datetime to be converted + :return: number of milliseconds since 1.1.2000 00:00:00; 64-bit integer + :raises SPSDKError: When there is incorrect result of conversion + """ + assert isinstance(value, datetime) + start = datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp() + result = int((value.timestamp() - start) * 1000000) + if result < 0 or result > 0xFFFFFFFFFFFFFFFF: + raise SPSDKError("Incorrect result of conversion") + return result + + +def unpack_timestamp(value: int) -> datetime: + """Converts timestamp in milliseconds into datetime. + + :param value: number of milliseconds since 1.1.2000 00:00:00; 64-bit integer + :return: corresponding datetime + :raises SPSDKError: When there is incorrect result of conversion + """ + assert isinstance(value, int) + if value < 0 or value > 0xFFFFFFFFFFFFFFFF: + raise SPSDKError("Incorrect result of conversion") + start = int( + datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=timezone.utc).timestamp() * 1000000 + ) + return datetime.fromtimestamp((start + value) / 1000000) diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/__init__.py new file mode 100644 index 0000000..02d575a --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module implementing SB2 and SB2.1 File.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/commands.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/commands.py new file mode 100644 index 0000000..787a7f0 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/commands.py @@ -0,0 +1,1074 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Commands used by SBFile module.""" +import math +from abc import abstractmethod +from struct import calcsize, pack, unpack_from +from typing import TYPE_CHECKING, Mapping, Optional, Type + +from crcmod.predefined import mkPredefinedCrcFun + +from ...exceptions import SPSDKError +from ...mboot.memories import ExtMemId +from ...sbfile.misc import SecBootBlckSize +from ...utils.abstract import BaseClass +from ...utils.misc import Endianness +from ...utils.spsdk_enum import SpsdkEnum + +if TYPE_CHECKING: + from typing_extensions import Self + +######################################################################################################################## +# Constants +######################################################################################################################## + +DEVICE_ID_MASK = 0xFF +DEVICE_ID_SHIFT = 0 +GROUP_ID_MASK = 0xF00 +GROUP_ID_SHIFT = 8 + + +######################################################################################################################## +# Enums +######################################################################################################################## +class EnumCmdTag(SpsdkEnum): + """Command tags.""" + + NOP = (0x0, "NOP") + TAG = (0x1, "TAG") + LOAD = (0x2, "LOAD") + FILL = (0x3, "FILL") + JUMP = (0x4, "JUMP") + CALL = (0x5, "CALL") + ERASE = (0x7, "ERASE") + RESET = (0x8, "RESET") + MEM_ENABLE = (0x9, "MEM_ENABLE") + PROG = (0xA, "PROG") + FW_VERSION_CHECK = (0xB, "FW_VERSION_CHECK", "Check FW version fuse value") + WR_KEYSTORE_TO_NV = ( + 0xC, + "WR_KEYSTORE_TO_NV", + "Restore key-store restore to non-volatile memory", + ) + WR_KEYSTORE_FROM_NV = ( + 0xD, + "WR_KEYSTORE_FROM_NV", + "Backup key-store from non-volatile memory", + ) + + +class EnumSectionFlag(SpsdkEnum): + """Section flags.""" + + BOOTABLE = (0x0001, "BOOTABLE") + CLEARTEXT = (0x0002, "CLEARTEXT") + LAST_SECT = (0x8000, "LAST_SECT") + + +######################################################################################################################## +# Header Class +######################################################################################################################## +class CmdHeader(BaseClass): + """SBFile command header.""" + + FORMAT = "<2BH3L" + SIZE = calcsize(FORMAT) + + @property + def crc(self) -> int: + """Calculate CRC for the header data.""" + raw_data = self._raw_data(crc=0) + checksum = 0x5A + for i in range(1, self.SIZE): + checksum = (checksum + raw_data[i]) & 0xFF + return checksum + + def __init__(self, tag: int, flags: int = 0) -> None: + """Initialize header.""" + if tag not in EnumCmdTag.tags(): + raise SPSDKError("Incorrect command tag") + self.tag = tag + self.flags = flags + self.address = 0 + self.count = 0 + self.data = 0 + + def __repr__(self) -> str: + return f"SB2 Command header, TAG:{self.tag}" + + def __str__(self) -> str: + tag = ( + EnumCmdTag.get_label(self.tag) + if self.tag in EnumCmdTag.tags() + else f"0x{self.tag:02X}" + ) + return ( + f"tag={tag}, flags=0x{self.flags:04X}, " + f"address=0x{self.address:08X}, count=0x{self.count:08X}, data=0x{self.data:08X}" + ) + + def _raw_data(self, crc: int) -> bytes: + """Return raw data of the header with specified CRC. + + :param crc: value to be used + :return: binary representation of the header + """ + return pack( + self.FORMAT, crc, self.tag, self.flags, self.address, self.count, self.data + ) + + def export(self) -> bytes: + """Export command header as bytes.""" + return self._raw_data(self.crc) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command header from bytes. + + :param data: Input data as bytes + :return: CMDHeader object + :raises SPSDKError: raised when size is incorrect + :raises SPSDKError: Raised when CRC is incorrect + """ + if calcsize(cls.FORMAT) > len(data): + raise SPSDKError("Incorrect size") + obj = cls(EnumCmdTag.NOP.tag) + (crc, obj.tag, obj.flags, obj.address, obj.count, obj.data) = unpack_from( + cls.FORMAT, data + ) + if crc != obj.crc: + raise SPSDKError("CRC does not match") + return obj + + +######################################################################################################################## +# Commands Classes +######################################################################################################################## +class CmdBaseClass(BaseClass): + """Base class for all commands.""" + + # bit mask for device ID inside flags + ROM_MEM_DEVICE_ID_MASK = 0xFF00 + # shift for device ID inside flags + ROM_MEM_DEVICE_ID_SHIFT = 8 + # bit mask for group ID inside flags + ROM_MEM_GROUP_ID_MASK = 0xF0 + # shift for group ID inside flags + ROM_MEM_GROUP_ID_SHIFT = 4 + + def __init__(self, tag: EnumCmdTag) -> None: + """Initialize CmdBase.""" + self._header = CmdHeader(tag.tag) + + @property + def header(self) -> CmdHeader: + """Return command header.""" + return self._header + + @property + def raw_size(self) -> int: + """Return size of the command in binary format (including header).""" + return CmdHeader.SIZE # this is default implementation + + def __repr__(self) -> str: + return "Command: " + str( + self._header + ) # default implementation: use command name + + def __str__(self) -> str: + """Return text info about the instance.""" + return repr(self) + "\n" # default implementation is same as __repr__ + + def export(self) -> bytes: + """Return object serialized into bytes.""" + return self._header.export() # default implementation + + +class CmdNop(CmdBaseClass): + """Command NOP class.""" + + def __init__(self) -> None: + """Initialize Command Nop.""" + super().__init__(EnumCmdTag.NOP) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: CMD Nop object + :raises SPSDKError: When there is incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.NOP: + raise SPSDKError("Incorrect header tag") + return cls() + + +class CmdTag(CmdBaseClass): + """Command TAG class. + + It is also used as header for boot section for SB file 1.x. + """ + + def __init__(self) -> None: + """Initialize Command Tag.""" + super().__init__(EnumCmdTag.TAG) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: parsed instance + :raises SPSDKError: When there is incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.TAG: + raise SPSDKError("Incorrect header tag") + result = cls() + result._header = header + return result + + +class CmdLoad(CmdBaseClass): + """Command Load. The load statement is used to store data into the memory.""" + + @property + def address(self) -> int: + """Return address in target processor to load data.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Setter. + + :param value: address in target processor to load data + :raises SPSDKError: When there is incorrect address + """ + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def flags(self) -> int: + """Return command's flag.""" + return self._header.flags + + @flags.setter + def flags(self, value: int) -> None: + """Set command's flag.""" + self._header.flags = value + + @property + def raw_size(self) -> int: + """Return aligned size of the command including header and data.""" + size = CmdHeader.SIZE + len(self.data) + if size % CmdHeader.SIZE: + size += CmdHeader.SIZE - (size % CmdHeader.SIZE) + return size + + def __init__(self, address: int, data: bytes, mem_id: int = 0) -> None: + """Initialize CMD Load.""" + super().__init__(EnumCmdTag.LOAD) + assert isinstance(data, (bytes, bytearray)) + self.address = address + self.data = bytes(data) + self.mem_id = mem_id + + device_id = get_device_id(mem_id) + group_id = get_group_id(mem_id) + + self.flags |= (self.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (device_id << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + ) + + self.flags |= (self.flags & ~self.ROM_MEM_GROUP_ID_MASK) | ( + (group_id << self.ROM_MEM_GROUP_ID_SHIFT) & self.ROM_MEM_GROUP_ID_MASK + ) + + def __str__(self) -> str: + return ( + f"LOAD: Address=0x{self.address:08X}, DataLen={len(self.data)}, " + f"Flags=0x{self.flags:08X}, MemId=0x{self.mem_id:08X}" + ) + + def export(self) -> bytes: + """Export command as binary.""" + self._update_data() + result = super().export() + return result + self.data + + def _update_data(self) -> None: + """Update command data.""" + # padding data + self.data = SecBootBlckSize.align_block_fill_random(self.data) + # update header + self._header.count = len(self.data) + crc32_function = mkPredefinedCrcFun("crc-32-mpeg") + self._header.data = crc32_function(self.data, 0xFFFFFFFF) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: CMD Load object + :raises SPSDKError: Raised when there is invalid CRC + :raises SPSDKError: When there is incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.LOAD: + raise SPSDKError("Incorrect header tag") + header_count = SecBootBlckSize.align(header.count) + cmd_data = data[CmdHeader.SIZE : CmdHeader.SIZE + header_count] + crc32_function = mkPredefinedCrcFun("crc-32-mpeg") + if header.data != crc32_function(cmd_data, 0xFFFFFFFF): + raise SPSDKError("Invalid CRC in the command header") + device_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT + group_id = ( + header.flags & cls.ROM_MEM_GROUP_ID_MASK + ) >> cls.ROM_MEM_GROUP_ID_SHIFT + mem_id = get_memory_id(device_id, group_id) + obj = cls(header.address, cmd_data, mem_id) + obj.header.data = header.data + obj.header.flags = header.flags + obj._update_data() + return obj + + +class CmdFill(CmdBaseClass): + """Command Fill class.""" + + PADDING_VALUE = 0x00 + + @property + def address(self) -> int: + """Return address of the command Fill.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set address for the command Fill.""" + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def raw_size(self) -> int: + """Calculate raw size of header.""" + size = CmdHeader.SIZE + size += len(self._pattern) - 4 + if size % CmdHeader.SIZE: + size += CmdHeader.SIZE - (size % CmdHeader.SIZE) + return size + + def __init__( + self, address: int, pattern: int, length: Optional[int] = None + ) -> None: + """Initialize Command Fill. + + :param address: to write data + :param pattern: data to be written + :param length: length of data to be filled, defaults to 4 + :raises SPSDKError: Raised when size is not aligned to 4 bytes + """ + super().__init__(EnumCmdTag.FILL) + length = length or 4 + if length % 4: + raise SPSDKError("Length of memory range to fill must be a multiple of 4") + # if the pattern is a zero, the length is considered also as zero and the + # conversion to bytes produces empty byte "array", which is wrong, as + # zero should be converted to zero byte. Thus in case the pattern_len + # evaluates to 0, we set it to 1. + pattern_len = pattern.bit_length() / 8 or 1 + # We can get a number of 3 bytes, so we consider this as a word and set + # the length to 4 bytes with the first byte being zero. + if 3 == math.ceil(pattern_len): + pattern_len = 4 + pattern_bytes = pattern.to_bytes(math.ceil(pattern_len), Endianness.BIG.value) + # The pattern length is computed above, but as we transform the number + # into bytes, compute the len again just in case - a bit paranoid + # approach chosen. + if len(pattern_bytes) not in [1, 2, 4]: + raise SPSDKError("Pattern must be 1, 2 or 4 bytes long") + replicate = 4 // len(pattern_bytes) + final_pattern = replicate * pattern_bytes + self.address = address + self._pattern = final_pattern + # update header + self._header.data = unpack_from(">L", self._pattern)[0] + self._header.count = length + + @property + def pattern(self) -> bytes: + """Return binary data to fill.""" + return self._pattern + + def __str__(self) -> str: + return f"FILL: Address=0x{self.address:08X}, Pattern=" + " ".join( + f"{byte:02X}" for byte in self._pattern + ) + + def export(self) -> bytes: + """Return command in binary form (serialization).""" + # export cmd + data = super().export() + # export additional data + data = SecBootBlckSize.align_block_fill_random(data) + return data + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Fill object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.FILL: + raise SPSDKError("Incorrect header tag") + return cls(header.address, header.data, header.count) + + +class CmdJump(CmdBaseClass): + """Command Jump class.""" + + @property + def address(self) -> int: + """Return address of the command Jump.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set address of the command Jump.""" + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def argument(self) -> int: + """Return command's argument.""" + return self._header.data + + @argument.setter + def argument(self, value: int) -> None: + """Set command's argument.""" + self._header.data = value + + @property + def spreg(self) -> Optional[int]: + """Return command's Stack Pointer.""" + if self._header.flags == 2: + return self._header.count + + return None + + @spreg.setter + def spreg(self, value: Optional[int] = None) -> None: + """Set command's Stack Pointer.""" + if value is None: + self._header.flags = 0 + self._header.count = 0 + else: + self._header.flags = 2 + self._header.count = value + + def __init__( + self, address: int = 0, argument: int = 0, spreg: Optional[int] = None + ) -> None: + """Initialize Command Jump.""" + super().__init__(EnumCmdTag.JUMP) + self.address = address + self.argument = argument + self.spreg = spreg + + def __str__(self) -> str: + nfo = f"JUMP: Address=0x{self.address:08X}, Argument=0x{self.argument:08X}" + if self.spreg is not None: + nfo += f", SP=0x{self.spreg:08X}" + return nfo + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Jump object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.JUMP: + raise SPSDKError("Incorrect header tag") + return cls(header.address, header.data, header.count if header.flags else None) + + +class CmdCall(CmdBaseClass): + """Command Call. + + The call statement is used for inserting a bootloader command that executes a function + from one of the files that are loaded into the memory. + """ + + @property + def address(self) -> int: + """Return command's address.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set command's address.""" + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def argument(self) -> int: + """Return command's argument.""" + return self._header.data + + @argument.setter + def argument(self, value: int) -> None: + """Set command's argument.""" + self._header.data = value + + def __init__(self, address: int = 0, argument: int = 0) -> None: + """Initialize Command Call.""" + super().__init__(EnumCmdTag.CALL) + self.address = address + self.argument = argument + + def __str__(self) -> str: + return f"CALL: Address=0x{self.address:08X}, Argument=0x{self.argument:08X}" + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Call object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.CALL: + raise SPSDKError("Incorrect header tag") + return cls(header.address, header.data) + + +class CmdErase(CmdBaseClass): + """Command Erase class.""" + + @property + def address(self) -> int: + """Return command's address.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set command's address.""" + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def length(self) -> int: + """Return command's count.""" + return self._header.count + + @length.setter + def length(self, value: int) -> None: + """Set command's count.""" + self._header.count = value + + @property + def flags(self) -> int: + """Return command's flag.""" + return self._header.flags + + @flags.setter + def flags(self, value: int) -> None: + """Set command's flag.""" + self._header.flags = value + + def __init__( + self, address: int = 0, length: int = 0, flags: int = 0, mem_id: int = 0 + ) -> None: + """Initialize Command Erase.""" + super().__init__(EnumCmdTag.ERASE) + self.address = address + self.length = length + self.flags = flags + self.mem_id = mem_id + + device_id = get_device_id(mem_id) + group_id = get_group_id(mem_id) + + self.flags |= (self.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (device_id << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + ) + + self.flags |= (self.flags & ~self.ROM_MEM_GROUP_ID_MASK) | ( + (group_id << self.ROM_MEM_GROUP_ID_SHIFT) & self.ROM_MEM_GROUP_ID_MASK + ) + + def __str__(self) -> str: + return ( + f"ERASE: Address=0x{self.address:08X}, Length={self.length}, Flags=0x{self.flags:08X}, " + f"MemId=0x{self.mem_id:08X}" + ) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Erase object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.ERASE: + raise SPSDKError("Invalid header tag") + device_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT + group_id = ( + header.flags & cls.ROM_MEM_GROUP_ID_MASK + ) >> cls.ROM_MEM_GROUP_ID_SHIFT + mem_id = get_memory_id(device_id, group_id) + return cls(header.address, header.count, header.flags, mem_id) + + +class CmdReset(CmdBaseClass): + """Command Reset class.""" + + def __init__(self) -> None: + """Initialize Command Reset.""" + super().__init__(EnumCmdTag.RESET) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: Cmd Reset object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.RESET: + raise SPSDKError("Invalid header tag") + return cls() + + +class CmdMemEnable(CmdBaseClass): + """Command to configure certain memory.""" + + @property + def address(self) -> int: + """Return command's address.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Set command's address.""" + self._header.address = value + + @property + def size(self) -> int: + """Return command's size.""" + return self._header.count + + @size.setter + def size(self, value: int) -> None: + """Set command's size.""" + self._header.count = value + + @property + def flags(self) -> int: + """Return command's flag.""" + return self._header.flags + + @flags.setter + def flags(self, value: int) -> None: + """Set command's flag.""" + self._header.flags = value + + def __init__(self, address: int, size: int, mem_id: int): + """Initialize CmdMemEnable. + + :param address: source address with configuration data for memory initialization + :param size: size of configuration data used for memory initialization + :param mem_id: identification of memory + """ + super().__init__(EnumCmdTag.MEM_ENABLE) + self.address = address + self.mem_id = mem_id + self.size = size + + device_id = get_device_id(mem_id) + group_id = get_group_id(mem_id) + + self.flags |= (self.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (device_id << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + ) + + self.flags |= (self.flags & ~self.ROM_MEM_GROUP_ID_MASK) | ( + (group_id << self.ROM_MEM_GROUP_ID_SHIFT) & self.ROM_MEM_GROUP_ID_MASK + ) + + def __str__(self) -> str: + return ( + f"MEM-ENABLE: Address=0x{self.address:08X}, Size={self.size}, " + f"Flags=0x{self.flags:08X}, MemId=0x{self.mem_id:08X}" + ) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: Command Memory Enable object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.MEM_ENABLE: + raise SPSDKError("Invalid header tag") + device_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT + group_id = ( + header.flags & cls.ROM_MEM_GROUP_ID_MASK + ) >> cls.ROM_MEM_GROUP_ID_SHIFT + mem_id = get_memory_id(device_id, group_id) + return cls(header.address, header.count, mem_id) + + +class CmdProg(CmdBaseClass): + """Command Program class.""" + + @property + def address(self) -> int: + """Return address in target processor to program data.""" + return self._header.address + + @address.setter + def address(self, value: int) -> None: + """Setter. + + :param value: address in target processor to load data + :raises SPSDKError: When there is incorrect address + """ + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect address") + self._header.address = value + + @property + def flags(self) -> int: + """Return command's flag.""" + return self._header.flags + + @flags.setter + def flags(self, value: int) -> None: + """Set command's flag.""" + self._header.flags = self.is_eight_byte + self._header.flags |= value + + @property + def data_word1(self) -> int: + """Return data word 1.""" + return self._header.count + + @data_word1.setter + def data_word1(self, value: int) -> None: + """Setter. + + :param value: first data word + :raises SPSDKError: When there is incorrect value + """ + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect data word 1") + self._header.count = value + + @property + def data_word2(self) -> int: + """Return data word 2.""" + return self._header.data + + @data_word2.setter + def data_word2(self, value: int) -> None: + """Setter. + + :param value: second data word + :raises SPSDKError: When there is incorrect value + """ + if value < 0x00000000 or value > 0xFFFFFFFF: + raise SPSDKError("Incorrect data word 2") + self._header.data = value + + def __init__( + self, + address: int, + mem_id: int, + data_word1: int, + data_word2: int = 0, + flags: int = 0, + ) -> None: + """Initialize CMD Prog.""" + super().__init__(EnumCmdTag.PROG) + + if data_word2: + self.is_eight_byte = 1 + else: + self.is_eight_byte = 0 + + if mem_id < 0 or mem_id > 0xFF: + raise SPSDKError("Invalid ID of memory") + + self.address = address + self.data_word1 = data_word1 + self.data_word2 = data_word2 + self.mem_id = mem_id + self.flags = flags + + self.flags = (self.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (self.mem_id << self.ROM_MEM_DEVICE_ID_SHIFT) & self.ROM_MEM_DEVICE_ID_MASK + ) + + def __str__(self) -> str: + return ( + f"PROG: Index=0x{self.address:08X}, DataWord1=0x{self.data_word1:08X}, " + f"DataWord2=0x{self.data_word2:08X}, Flags=0x{self.flags:08X}, MemId=0x{self.mem_id:08X}" + ) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: parsed command object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.PROG: + raise SPSDKError("Invalid header tag") + mem_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT + return cls(header.address, mem_id, header.count, header.data, header.flags) + + +class VersionCheckType(SpsdkEnum): + """Select type of the version check: either secure or non-secure firmware to be checked.""" + + SECURE_VERSION = (0, "SECURE_VERSION") + NON_SECURE_VERSION = (1, "NON_SECURE_VERSION") + + +class CmdVersionCheck(CmdBaseClass): + """FW Version Check command class. + + Validates version of secure or non-secure firmware. + The command fails if version is < expected. + """ + + def __init__(self, ver_type: VersionCheckType, version: int) -> None: + """Initialize CmdVersionCheck. + + :param ver_type: version check type, see `VersionCheckType` enum + :param version: to be checked + :raises SPSDKError: If invalid version check type + """ + super().__init__(EnumCmdTag.FW_VERSION_CHECK) + if ver_type not in VersionCheckType: + raise SPSDKError("Invalid version check type") + self.header.address = ver_type.tag + self.header.count = version + + @property + def type(self) -> VersionCheckType: + """Return type of the check version, see VersionCheckType enumeration.""" + return VersionCheckType.from_tag(self.header.address) + + @property + def version(self) -> int: + """Return minimal version expected.""" + return self.header.count + + def __str__(self) -> str: + return ( + f"CVER: Type={self.type.label}, Version={str(self.version)}, " + f"Flags=0x{self.header.flags:08X}" + ) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: parsed command object + :raises SPSDKError: If incorrect header tag + """ + header = CmdHeader.parse(data) + if header.tag != EnumCmdTag.FW_VERSION_CHECK: + raise SPSDKError("Invalid header tag") + ver_type = VersionCheckType.from_tag(header.address) + version = header.count + return cls(ver_type, version) + + +class CmdKeyStoreBackupRestore(CmdBaseClass): + """Shared, abstract implementation for key-store backup and restore command.""" + + # bit mask for controller ID inside flags + ROM_MEM_DEVICE_ID_MASK = 0xFF00 + # shift for controller ID inside flags + ROM_MEM_DEVICE_ID_SHIFT = 8 + + @classmethod + @abstractmethod + def cmd_id(cls) -> EnumCmdTag: + """Return command ID. + + :raises NotImplementedError: Derived class has to implement this method + """ + raise NotImplementedError("Derived class has to implement this method.") + + def __init__(self, address: int, controller_id: ExtMemId): + """Initialize CmdKeyStoreBackupRestore. + + :param address: where to backup key-store or source for restoring key-store + :param controller_id: ID of the memory to backup key-store or source memory to load key-store back + :raises SPSDKError: If invalid address + :raises SPSDKError: If invalid id of memory + """ + super().__init__(self.cmd_id()) + if address < 0 or address > 0xFFFFFFFF: + raise SPSDKError("Invalid address") + self.header.address = address + if controller_id.tag < 0 or controller_id.tag > 0xFF: + raise SPSDKError("Invalid ID of memory") + self.header.flags = (self.header.flags & ~self.ROM_MEM_DEVICE_ID_MASK) | ( + (controller_id.tag << self.ROM_MEM_DEVICE_ID_SHIFT) + & self.ROM_MEM_DEVICE_ID_MASK + ) + self.header.count = ( + 4 # this is useless, but it is kept for backward compatibility with elftosb + ) + + @property + def address(self) -> int: + """Return address where to backup key-store or source for restoring key-store.""" + return self.header.address + + @property + def controller_id(self) -> int: + """Return controller ID of the memory to backup key-store or source memory to load key-store back.""" + return ( + self.header.flags & self.ROM_MEM_DEVICE_ID_MASK + ) >> self.ROM_MEM_DEVICE_ID_SHIFT + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse command from bytes. + + :param data: Input data as bytes + :return: CmdKeyStoreBackupRestore object + :raises SPSDKError: When there is invalid header tag + """ + header = CmdHeader.parse(data) + if header.tag != cls.cmd_id(): + raise SPSDKError("Invalid header tag") + address = header.address + controller_id = ( + header.flags & cls.ROM_MEM_DEVICE_ID_MASK + ) >> cls.ROM_MEM_DEVICE_ID_SHIFT + return cls(address, ExtMemId.from_tag(controller_id)) + + +class CmdKeyStoreBackup(CmdKeyStoreBackupRestore): + """Command to backup keystore from non-volatile memory.""" + + @classmethod + def cmd_id(cls) -> EnumCmdTag: + """Return command ID for backup operation.""" + return EnumCmdTag.WR_KEYSTORE_FROM_NV + + +class CmdKeyStoreRestore(CmdKeyStoreBackupRestore): + """Command to restore keystore into non-volatile memory.""" + + @classmethod + def cmd_id(cls) -> EnumCmdTag: + """Return command ID for restore operation.""" + return EnumCmdTag.WR_KEYSTORE_TO_NV + + +######################################################################################################################## +# Command parser from binary format +######################################################################################################################## +_CMD_CLASS: Mapping[EnumCmdTag, Type[CmdBaseClass]] = { + EnumCmdTag.NOP: CmdNop, + EnumCmdTag.TAG: CmdTag, + EnumCmdTag.LOAD: CmdLoad, + EnumCmdTag.FILL: CmdFill, + EnumCmdTag.JUMP: CmdJump, + EnumCmdTag.CALL: CmdCall, + EnumCmdTag.ERASE: CmdErase, + EnumCmdTag.RESET: CmdReset, + EnumCmdTag.MEM_ENABLE: CmdMemEnable, + EnumCmdTag.PROG: CmdProg, + EnumCmdTag.FW_VERSION_CHECK: CmdVersionCheck, + EnumCmdTag.WR_KEYSTORE_TO_NV: CmdKeyStoreRestore, + EnumCmdTag.WR_KEYSTORE_FROM_NV: CmdKeyStoreBackup, +} + + +def parse_command(data: bytes) -> CmdBaseClass: + """Parse SB 2.x command from bytes. + + :param data: Input data as bytes + :return: parsed command object + :raises SPSDKError: Raised when there is unsupported command provided + """ + header_tag = data[1] + for cmd_tag, cmd in _CMD_CLASS.items(): + if cmd_tag.tag == header_tag: + return cmd.parse(data) + raise SPSDKError(f"Unsupported command: {str(header_tag)}") + + +def get_device_id(mem_id: int) -> int: + """Get device ID from memory ID. + + :param mem_id: memory ID + :return: device ID + """ + return ((mem_id) & DEVICE_ID_MASK) >> DEVICE_ID_SHIFT + + +def get_group_id(mem_id: int) -> int: + """Get group ID from memory ID. + + :param mem_id: memory ID + :return: group ID + """ + return ((mem_id) & GROUP_ID_MASK) >> GROUP_ID_SHIFT + + +def get_memory_id(device_id: int, group_id: int) -> int: + """Get memory ID from device ID and group ID. + + :param device_id: device ID + :param group_id: group ID + :return: memory ID + """ + return (((group_id) << GROUP_ID_SHIFT) & GROUP_ID_MASK) | ( + ((device_id) << DEVICE_ID_SHIFT) & DEVICE_ID_MASK + ) diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/headers.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/headers.py new file mode 100644 index 0000000..c890b9e --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/headers.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Image header.""" + +from datetime import datetime +from struct import calcsize, unpack_from +from typing import TYPE_CHECKING, Optional + +from ...exceptions import SPSDKError +from ...sbfile.misc import BcdVersion3, unpack_timestamp +from ...utils.abstract import BaseClass +from ...utils.misc import swap16 + +if TYPE_CHECKING: + from typing_extensions import Self + + +######################################################################################################################## +# Image Header Class (Version SB2) +######################################################################################################################## +# pylint: disable=too-many-instance-attributes +class ImageHeaderV2(BaseClass): + """Image Header V2 class.""" + + FORMAT = "<16s4s4s2BH4I4H4sQ12HI4s" + SIZE = calcsize(FORMAT) + SIGNATURE1 = b"STMP" + SIGNATURE2 = b"sgtl" + + def __init__( + self, + version: str = "2.0", + product_version: str = "1.0.0", + component_version: str = "1.0.0", + build_number: int = 0, + flags: int = 0x08, + nonce: Optional[bytes] = None, + timestamp: Optional[datetime] = None, + ) -> None: + """Initialize Image Header Version 2.x. + + :param version: The image version value (default: 2.0) + :param product_version: The product version (default: 1.0.0) + :param component_version: The component version (default: 1.0.0) + :param build_number: The build number value (default: 0) + :param flags: The flags value (default: 0x08) + :param nonce: The NONCE value; None if TODO ???? + :param timestamp: value requested in the test; None to use current value + """ + self.nonce = nonce + self.version = version + self.flags = flags + self.image_blocks = 0 # will be updated from boot image + self.first_boot_tag_block = 0 + self.first_boot_section_id = 0 + self.offset_to_certificate_block = 0 # will be updated from boot image + self.header_blocks = 0 # will be calculated in the BootImage later + self.key_blob_block = 8 + self.key_blob_block_count = 5 + self.max_section_mac_count = 0 # will be calculated in the BootImage later + self.timestamp = ( + timestamp + if timestamp is not None + else datetime.fromtimestamp(int(datetime.now().timestamp())) + ) + self.product_version: BcdVersion3 = BcdVersion3.to_version(product_version) + self.component_version: BcdVersion3 = BcdVersion3.to_version(component_version) + self.build_number = build_number + + def __repr__(self) -> str: + return f"Header: v{self.version}, {self.image_blocks}" + + def flags_desc(self) -> str: + """Return flag description.""" + return "Signed" if self.flags == 0x8 else "Unsigned" + + def __str__(self) -> str: + """Get info of Header as string.""" + nfo = str() + nfo += f" Version: {self.version}\n" + if self.nonce is not None: + nfo += f" Digest: {self.nonce.hex().upper()}\n" + nfo += f" Flag: 0x{self.flags:X} ({self.flags_desc()})\n" + nfo += f" Image Blocks: {self.image_blocks}\n" + nfo += f" First Boot Tag Block: {self.first_boot_tag_block}\n" + nfo += f" First Boot SectionID: {self.first_boot_section_id}\n" + nfo += f" Offset to Cert Block: {self.offset_to_certificate_block}\n" + nfo += f" Key Blob Block: {self.key_blob_block}\n" + nfo += f" Header Blocks: {self.header_blocks}\n" + nfo += f" Sections MAC Count: {self.max_section_mac_count}\n" + nfo += f" Key Blob Block Count: {self.key_blob_block_count}\n" + nfo += ( + f" Timestamp: {self.timestamp.strftime('%H:%M:%S (%d.%m.%Y)')}\n" + ) + nfo += f" Product Version: {self.product_version}\n" + nfo += f" Component Version: {self.component_version}\n" + nfo += f" Build Number: {self.build_number}\n" + return nfo + + # pylint: disable=too-many-locals + @classmethod + def parse(cls, data: bytes) -> "Self": + """Deserialization from binary form. + + :param data: binary representation + :return: parsed instance of the header + :raises SPSDKError: Unable to parse data + """ + if cls.SIZE > len(data): + raise SPSDKError("Insufficient amount of data") + ( + nonce, + # padding0 + _, + signature1, + # header version + major_version, + minor_version, + flags, + image_blocks, + first_boot_tag_block, + first_boot_section_id, + offset_to_certificate_block, + header_blocks, + key_blob_block, + key_blob_block_count, + max_section_mac_count, + signature2, + raw_timestamp, + # product version + pv0, + _, + pv1, + _, + pv2, + _, + # component version + cv0, + _, + cv1, + _, + cv2, + _, + build_number, + # padding1 + _, + ) = unpack_from(cls.FORMAT, data) + + # check header signature 1 + if signature1 != cls.SIGNATURE1: + raise SPSDKError("SIGNATURE #1 doesn't match") + + # check header signature 2 + if signature2 != cls.SIGNATURE2: + raise SPSDKError("SIGNATURE #2 doesn't match") + + obj = cls( + version=f"{major_version}.{minor_version}", + flags=flags, + product_version=f"{swap16(pv0):X}.{swap16(pv1):X}.{swap16(pv2):X}", + component_version=f"{swap16(cv0):X}.{swap16(cv1):X}.{swap16(cv2):X}", + build_number=build_number, + ) + + obj.nonce = nonce + obj.image_blocks = image_blocks + obj.first_boot_tag_block = first_boot_tag_block + obj.first_boot_section_id = first_boot_section_id + obj.offset_to_certificate_block = offset_to_certificate_block + obj.header_blocks = header_blocks + obj.key_blob_block = key_blob_block + obj.key_blob_block_count = key_blob_block_count + obj.max_section_mac_count = max_section_mac_count + obj.timestamp = unpack_timestamp(raw_timestamp) + + return obj diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/images.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/images.py new file mode 100644 index 0000000..719ad4f --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/images.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Boot Image V2.0, V2.1.""" + +import logging +from datetime import datetime +from typing import Iterator, List, Optional + +from ...crypto.hash import EnumHashAlgorithm, get_hash +from ...crypto.symmetric import Counter, aes_key_unwrap +from ...exceptions import SPSDKError +from ...sbfile.misc import SecBootBlckSize +from ...utils.abstract import BaseClass +from ...utils.crypto.cert_blocks import CertBlockV1 +from .headers import ImageHeaderV2 +from .sections import BootSectionV2 + +logger = logging.getLogger(__name__) + + +class SBV2xAdvancedParams: + """The class holds advanced parameters for the SB file encryption. + + These parameters are used for the tests; for production, use can use default values (random keys + current time) + """ + + def __init__( + self, + dek: bytes, + mac: bytes, + nonce: bytes, + timestamp: datetime, + ): + """Initialize SBV2xAdvancedParams. + + :param dek: DEK key + :param mac: MAC key + :param nonce: nonce + :param timestamp: fixed timestamp for the header; use None to use current date/time + :raises SPSDKError: Invalid dek or mac + :raises SPSDKError: Invalid length of nonce + """ + self._dek = dek + self._mac = mac + self._nonce = nonce + self._timestamp = datetime.fromtimestamp(int(timestamp.timestamp())) + if len(self._dek) != 32 and len(self._mac) != 32: + raise SPSDKError("Invalid dek or mac") + if len(self._nonce) != 16: + raise SPSDKError("Invalid length of nonce") + + @property + def dek(self) -> bytes: + """Return DEK key.""" + return self._dek + + @property + def mac(self) -> bytes: + """Return MAC key.""" + return self._mac + + @property + def nonce(self) -> bytes: + """Return NONCE.""" + return self._nonce + + @property + def timestamp(self) -> datetime: + """Return timestamp.""" + return self._timestamp + + +######################################################################################################################## +# Secure Boot Image Class (Version 2.1) +######################################################################################################################## +class BootImageV21(BaseClass): + """Boot Image V2.1 class.""" + + # Image specific data + HEADER_MAC_SIZE = 32 + KEY_BLOB_SIZE = 80 + SHA_256_SIZE = 32 + + # defines + FLAGS_SHA_PRESENT_BIT = 0x8000 # image contains SHA-256 + FLAGS_ENCRYPTED_SIGNED_BIT = 0x0008 # image is signed and encrypted + + def __init__( + self, + kek: bytes, + *sections: BootSectionV2, + product_version: str, + component_version: str, + build_number: int, + advanced_params: SBV2xAdvancedParams, + flags: int = FLAGS_SHA_PRESENT_BIT | FLAGS_ENCRYPTED_SIGNED_BIT, + ) -> None: + """Initialize Secure Boot Image V2.1. + + :param kek: key to wrap DEC and MAC keys + + :param product_version: The product version (default: 1.0.0) + :param component_version: The component version (default: 1.0.0) + :param build_number: The build number value (default: 0) + + :param advanced_params: optional advanced parameters for encryption; it is recommended to use default value + :param flags: see flags defined in class. + :param sections: Boot sections + """ + self._kek = kek + self._dek = advanced_params.dek + self._mac = advanced_params.mac + self._header = ImageHeaderV2( + version="2.1", + product_version=product_version, + component_version=component_version, + build_number=build_number, + flags=flags, + nonce=advanced_params.nonce, + timestamp=advanced_params.timestamp, + ) + self._cert_block: Optional[CertBlockV1] = None + self.boot_sections: List[BootSectionV2] = [] + # ... + for section in sections: + self.add_boot_section(section) + + @property + def header(self) -> ImageHeaderV2: + """Return image header.""" + return self._header + + @property + def dek(self) -> bytes: + """Data encryption key.""" + return self._dek + + @property + def mac(self) -> bytes: + """Message authentication code.""" + return self._mac + + @property + def kek(self) -> bytes: + """Return key to wrap DEC and MAC keys.""" + return self._kek + + @property + def cert_block(self) -> Optional[CertBlockV1]: + """Return certificate block; None if SB file not signed or block not assigned yet.""" + return self._cert_block + + @cert_block.setter + def cert_block(self, value: CertBlockV1) -> None: + """Setter. + + :param value: block to be assigned; None to remove previously assigned block + """ + assert isinstance(value, CertBlockV1) + self._cert_block = value + self._cert_block.alignment = 16 + + @property + def signed(self) -> bool: + """Return flag whether SB file is signed.""" + return True # SB2.1 is always signed + + @property + def cert_header_size(self) -> int: + """Return image raw size (not aligned) for certificate header.""" + size = ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + size += self.KEY_BLOB_SIZE + # Certificates Section + cert_blk = self.cert_block + if cert_blk: + size += cert_blk.raw_size + return size + + @property + def raw_size(self) -> int: + """Return image raw size (not aligned).""" + # Header, HMAC and KeyBlob + size = ImageHeaderV2.SIZE + self.HEADER_MAC_SIZE + size += self.KEY_BLOB_SIZE + # Certificates Section + cert_blk = self.cert_block + if cert_blk: + size += cert_blk.raw_size + if not self.signed: # pragma: no cover # SB2.1 is always signed + raise SPSDKError("Certificate block is not signed") + size += cert_blk.signature_size + # Boot Sections + for boot_section in self.boot_sections: + size += boot_section.raw_size + return size + + def __len__(self) -> int: + return len(self.boot_sections) + + def __getitem__(self, key: int) -> BootSectionV2: + return self.boot_sections[key] + + def __setitem__(self, key: int, value: BootSectionV2) -> None: + self.boot_sections[key] = value + + def __iter__(self) -> Iterator[BootSectionV2]: + return self.boot_sections.__iter__() + + def update(self) -> None: + """Update BootImageV21.""" + if self.boot_sections: + self._header.first_boot_section_id = self.boot_sections[0].uid + # calculate first boot tag block + data_size = self._header.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + cert_blk = self.cert_block + if cert_blk is not None: + data_size += cert_blk.raw_size + if not self.signed: # pragma: no cover # SB2.1 is always signed + raise SPSDKError("Certificate block is not signed") + data_size += cert_blk.signature_size + self._header.first_boot_tag_block = SecBootBlckSize.to_num_blocks(data_size) + # ... + self._header.image_blocks = SecBootBlckSize.to_num_blocks(self.raw_size) + self._header.header_blocks = SecBootBlckSize.to_num_blocks(self._header.SIZE) + self._header.offset_to_certificate_block = ( + self._header.SIZE + self.HEADER_MAC_SIZE + self.KEY_BLOB_SIZE + ) + # Get HMAC count + self._header.max_section_mac_count = 0 + for boot_sect in self.boot_sections: + boot_sect.is_last = True # unified with elftosb + self._header.max_section_mac_count += boot_sect.hmac_count + # Update certificates block header + cert_clk = self.cert_block + if cert_clk is not None: + cert_clk.header.build_number = self._header.build_number + cert_clk.header.image_length = self.cert_header_size + + def __repr__(self) -> str: + return f"SB2.1, {'Signed' if self.signed else 'Plain'} " + + def __str__(self) -> str: + """Return text description of the instance.""" + self.update() + nfo = "\n" + nfo += ":::::::::::::::::::::::::::::::::: IMAGE HEADER ::::::::::::::::::::::::::::::::::::::\n" + nfo += str(self._header) + if self.cert_block is not None: + nfo += "::::::::::::::::::::::::::::::: CERTIFICATES BLOCK ::::::::::::::::::::::::::::::::::::\n" + nfo += str(self.cert_block) + nfo += "::::::::::::::::::::::::::::::::::: BOOT SECTIONS ::::::::::::::::::::::::::::::::::::\n" + for index, section in enumerate(self.boot_sections): + nfo += f"[ SECTION: {index} | UID: 0x{section.uid:08X} ]\n" + nfo += str(section) + return nfo + + def add_boot_section(self, section: BootSectionV2) -> None: + """Add new Boot section into image. + + :param section: Boot section to be added + :raises SPSDKError: Raised when section is not instance of BootSectionV2 class + """ + if not isinstance(section, BootSectionV2): + raise SPSDKError("Section is not instance of BootSectionV2 class") + self.boot_sections.append(section) + + # pylint: disable=too-many-locals + @classmethod + def parse( + cls, + data: bytes, + offset: int = 0, + kek: bytes = bytes(), + plain_sections: bool = False, + ) -> "BootImageV21": + """Parse image from bytes. + + :param data: Raw data of parsed image + :param offset: The offset of input data + :param kek: The Key for unwrapping DEK and MAC keys (required) + :param plain_sections: Sections are not encrypted; this is used only for debugging, + not supported by ROM code + :return: BootImageV21 parsed object + :raises SPSDKError: raised when header is in incorrect format + :raises SPSDKError: raised when signature is incorrect + :raises SPSDKError: Raised when kek is empty + :raises SPSDKError: raised when header's nonce not present" + """ + if not kek: + raise SPSDKError("kek cannot be empty") + index = offset + header_raw_data = data[index : index + ImageHeaderV2.SIZE] + index += ImageHeaderV2.SIZE + # Not used right now: hmac_data = data[index: index + cls.HEADER_MAC_SIZE] + index += cls.HEADER_MAC_SIZE + key_blob = data[index : index + cls.KEY_BLOB_SIZE] + index += cls.KEY_BLOB_SIZE + key_blob_unwrap = aes_key_unwrap(kek, key_blob[:-8]) + dek = key_blob_unwrap[:32] + mac = key_blob_unwrap[32:] + # Parse Header + header = ImageHeaderV2.parse(header_raw_data) + if header.offset_to_certificate_block != (index - offset): + raise SPSDKError("Invalid offset") + # Parse Certificate Block + cert_block = CertBlockV1.parse(data[index:]) + index += cert_block.raw_size + + # Verify Signature + signature_index = index + # The image may contain SHA, in such a case the signature is placed + # after SHA. Thus we must shift the index by SHA size. + if header.flags & BootImageV21.FLAGS_SHA_PRESENT_BIT: + signature_index += BootImageV21.SHA_256_SIZE + result = cert_block.verify_data( + data[signature_index : signature_index + cert_block.signature_size], + data[offset:signature_index], + ) + + if not result: + raise SPSDKError("Verification failed") + # Check flags, if 0x8000 bit is set, the SB file contains SHA-256 between + # certificate and signature. + if header.flags & BootImageV21.FLAGS_SHA_PRESENT_BIT: + bootable_section_sha256 = data[index : index + BootImageV21.SHA_256_SIZE] + index += BootImageV21.SHA_256_SIZE + index += cert_block.signature_size + # Check first Boot Section HMAC + # Not implemented yet + # hmac_data_calc = hmac(mac, data[index + CmdHeader.SIZE: index + CmdHeader.SIZE + ((2) * 32)]) + # if hmac_data != hmac_data_calc: + # raise SPSDKError("HMAC failed") + if not header.nonce: + raise SPSDKError("Header's nonce not present") + counter = Counter(header.nonce) + counter.increment(SecBootBlckSize.to_num_blocks(index - offset)) + boot_section = BootSectionV2.parse( + data, index, dek=dek, mac=mac, counter=counter, plain_sect=plain_sections + ) + if header.flags & BootImageV21.FLAGS_SHA_PRESENT_BIT: + computed_bootable_section_sha256 = get_hash( + data[index:], algorithm=EnumHashAlgorithm.SHA256 + ) + + if bootable_section_sha256 != computed_bootable_section_sha256: + raise SPSDKError( + desc=( + "Error: invalid Bootable section SHA." + f"Expected {bootable_section_sha256.decode('utf-8')}," + f"got {computed_bootable_section_sha256.decode('utf-8')}" + ) + ) + adv_params = SBV2xAdvancedParams( + dek=dek, mac=mac, nonce=header.nonce, timestamp=header.timestamp + ) + obj = cls( + kek=kek, + product_version=str(header.product_version), + component_version=str(header.component_version), + build_number=header.build_number, + advanced_params=adv_params, + ) + obj.cert_block = cert_block + obj.add_boot_section(boot_section) + return obj diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/sections.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/sections.py new file mode 100644 index 0000000..73865a4 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/sbfile/sb2/sections.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Sections within SBfile.""" + +from struct import unpack_from +from typing import Iterator, List, Optional + +from ...crypto.hmac import hmac +from ...crypto.symmetric import Counter, aes_ctr_decrypt +from ...exceptions import SPSDKError +from ...sbfile.misc import SecBootBlckSize +from ...utils.abstract import BaseClass +from ...utils.crypto.cert_blocks import CertBlockV1 +from .commands import ( + CmdBaseClass, + CmdHeader, + EnumCmdTag, + EnumSectionFlag, + parse_command, +) + +######################################################################################################################## +# Boot Image Sections +######################################################################################################################## + + +class BootSectionV2(BaseClass): + """Boot Section V2.""" + + HMAC_SIZE = 32 + + @property + def uid(self) -> int: + """Boot Section UID.""" + return self._header.address + + @uid.setter + def uid(self, value: int) -> None: + self._header.address = value + + @property + def is_last(self) -> bool: + """Check whether the section is the last one.""" + return self._header.flags & EnumSectionFlag.LAST_SECT.tag != 0 + + @is_last.setter + def is_last(self, value: bool) -> None: + assert isinstance(value, bool) + self._header.flags = EnumSectionFlag.BOOTABLE.tag + if value: + self._header.flags |= EnumSectionFlag.LAST_SECT.tag + + @property + def hmac_count(self) -> int: + """Number of HMACs.""" + raw_size = 0 + hmac_count = 0 + for cmd in self._commands: + raw_size += cmd.raw_size + if raw_size > 0: + block_count = (raw_size + 15) // 16 + hmac_count = ( + self._hmac_count if block_count >= self._hmac_count else block_count + ) + return hmac_count + + @property + def raw_size(self) -> int: + """Raw size of section.""" + size = CmdHeader.SIZE + self.HMAC_SIZE + size += self.hmac_count * self.HMAC_SIZE + for cmd in self._commands: + size += cmd.raw_size + if size % 16: + size += 16 - (size % 16) + return size + + def __init__(self, uid: int, *commands: CmdBaseClass, hmac_count: int = 1) -> None: + """Initialize BootSectionV2. + + :param uid: section unique identification + :param commands: List of commands + :param hmac_count: The number of HMAC entries + """ + self._header = CmdHeader(EnumCmdTag.TAG.tag, EnumSectionFlag.BOOTABLE.tag) + self._commands: List[CmdBaseClass] = [] + self._hmac_count = hmac_count + for cmd in commands: + self.append(cmd) + # Initialize HMAC count + if not isinstance(self._hmac_count, int) or self._hmac_count == 0: + self._hmac_count = 1 + # section UID + self.uid = uid + + def __len__(self) -> int: + return len(self._commands) + + def __getitem__(self, key: int) -> CmdBaseClass: + return self._commands[key] + + def __setitem__(self, key: int, value: CmdBaseClass) -> None: + self._commands[key] = value + + def __iter__(self) -> Iterator[CmdBaseClass]: + return self._commands.__iter__() + + def append(self, cmd: CmdBaseClass) -> None: + """Add command to section.""" + assert isinstance(cmd, CmdBaseClass) + self._commands.append(cmd) + + def __repr__(self) -> str: + return f"BootSectionV2: {len(self)} commands." + + def __str__(self) -> str: + """Get object info.""" + nfo = "" + for index, cmd in enumerate(self._commands): + nfo += f" {index}) {str(cmd)}\n" + return nfo + + # pylint: disable=too-many-locals + @classmethod + def parse( + cls, + data: bytes, + offset: int = 0, + plain_sect: bool = False, + dek: bytes = b"", + mac: bytes = b"", + counter: Optional[Counter] = None, + ) -> "BootSectionV2": + """Parse Boot Section from bytes. + + :param data: Raw data of parsed image + :param offset: The offset of input data + :param plain_sect: If the sections are not encrypted; It is used for debugging only, not supported by ROM code + :param dek: The DEK value in bytes (required) + :param mac: The MAC value in bytes (required) + :param counter: The counter object (required) + :return: exported bytes + :raises SPSDKError: raised when dek, mac, counter have invalid format + """ + if not isinstance(dek, bytes): + raise SPSDKError("Invalid type of dek, should be bytes") + if not isinstance(mac, bytes): + raise SPSDKError("Invalid type of mac, should be bytes") + if not isinstance(counter, Counter): + raise SPSDKError("Invalid type of counter") + # Get Header specific data + header_encrypted = data[offset : offset + CmdHeader.SIZE] + header_hmac_data = data[ + offset + CmdHeader.SIZE : offset + CmdHeader.SIZE + cls.HMAC_SIZE + ] + offset += CmdHeader.SIZE + cls.HMAC_SIZE + # Check header HMAC + if header_hmac_data != hmac(mac, header_encrypted): + raise SPSDKError("Invalid header HMAC") + # Decrypt header + header_decrypted = aes_ctr_decrypt(dek, header_encrypted, counter.value) + counter.increment() + # Parse header + header = CmdHeader.parse(header_decrypted) + counter.increment((header.data + 1) * 2) + # Get HMAC data + hmac_data = data[offset : offset + (cls.HMAC_SIZE * header.data)] + offset += cls.HMAC_SIZE * header.data + encrypted_commands = data[offset : offset + (header.count * 16)] + # Check HMAC + hmac_index = 0 + hmac_count = header.data + block_size = (header.count // hmac_count) * 16 + section_size = header.count * 16 + while hmac_count > 0: + if hmac_count == 1: + block_size = section_size + hmac_block = hmac(mac, data[offset : offset + block_size]) + if hmac_block != hmac_data[hmac_index : hmac_index + cls.HMAC_SIZE]: + raise SPSDKError("HMAC failed") + hmac_count -= 1 + hmac_index += cls.HMAC_SIZE + section_size -= block_size + offset += block_size + # Decrypt commands + decrypted_commands = b"" + for hmac_index in range(0, len(encrypted_commands), 16): + encr_block = encrypted_commands[hmac_index : hmac_index + 16] + decrypted_block = ( + encr_block + if plain_sect + else aes_ctr_decrypt(dek, encr_block, counter.value) + ) + decrypted_commands += decrypted_block + counter.increment() + # ... + cmd_offset = 0 + obj = cls(header.address, hmac_count=header.data) + while cmd_offset < len(decrypted_commands): + cmd_obj = parse_command(decrypted_commands[cmd_offset:]) + cmd_offset += cmd_obj.raw_size + obj.append(cmd_obj) + return obj + + +class CertSectionV2(BaseClass): + """Certificate Section V2 class.""" + + HMAC_SIZE = 32 + SECT_MARK = unpack_from(" CertBlockV1: + """Return certification block.""" + return self._cert_block + + @property + def raw_size(self) -> int: + """Calculate raw size of section.""" + # Section header size + size = CmdHeader.SIZE + # Header HMAC 32 bytes + Certificate block HMAC 32 bytes + size += self.HMAC_SIZE * 2 + # Certificate block size in bytes + size += self.cert_block.raw_size + return size + + def __init__(self, cert_block: CertBlockV1): + """Initialize CertBlockV1.""" + assert isinstance(cert_block, CertBlockV1) + self._header = CmdHeader( + EnumCmdTag.TAG.tag, + EnumSectionFlag.CLEARTEXT.tag | EnumSectionFlag.LAST_SECT.tag, + ) + self._header.address = self.SECT_MARK + self._header.count = cert_block.raw_size // 16 + self._header.data = 1 + self._cert_block = cert_block + + def __repr__(self) -> str: + return f"CertSectionV2: Length={self._header.count * 16}" + + def __str__(self) -> str: + """Get object info.""" + return str(self.cert_block) + + @classmethod + def parse( + cls, + data: bytes, + offset: int = 0, + dek: bytes = b"", + mac: bytes = b"", + counter: Optional[Counter] = None, + ) -> "CertSectionV2": + """Parse Certificate Section from bytes array. + + :param data: Raw data of parsed image + :param offset: The offset of input data + :param dek: The DEK value in bytes (required) + :param mac: The MAC value in bytes (required) + :param counter: The counter object (required) + :return: parsed cert section v2 object + :raises SPSDKError: Raised when dek, mac, counter are not valid + :raises SPSDKError: Raised when there is invalid header HMAC, TAG, FLAGS, Mark + :raises SPSDKError: Raised when there is invalid certificate block HMAC + """ + if not isinstance(dek, bytes): + raise SPSDKError("DEK value has invalid format") + if not isinstance(mac, bytes): + raise SPSDKError("MAC value has invalid format") + if not isinstance(counter, Counter): + raise SPSDKError("Counter value has invalid format") + index = offset + header_encrypted = data[index : index + CmdHeader.SIZE] + index += CmdHeader.SIZE + header_hmac = data[index : index + cls.HMAC_SIZE] + index += cls.HMAC_SIZE + cert_block_hmac = data[index : index + cls.HMAC_SIZE] + index += cls.HMAC_SIZE + if header_hmac != hmac(mac, header_encrypted): + raise SPSDKError("Invalid Header HMAC") + header_encrypted = aes_ctr_decrypt(dek, header_encrypted, counter.value) + header = CmdHeader.parse(header_encrypted) + if header.tag != EnumCmdTag.TAG: + raise SPSDKError(f"Invalid Header TAG: 0x{header.tag:02X}") + if header.flags != ( + EnumSectionFlag.CLEARTEXT.tag | EnumSectionFlag.LAST_SECT.tag + ): + raise SPSDKError(f"Invalid Header FLAGS: 0x{header.flags:02X}") + if header.address != cls.SECT_MARK: + raise SPSDKError(f"Invalid Section Mark: 0x{header.address:08X}") + # Parse Certificate Block + cert_block = CertBlockV1.parse(data[index:]) + if cert_block_hmac != hmac(mac, data[index : index + cert_block.raw_size]): + raise SPSDKError("Invalid Certificate Block HMAC") + index += cert_block.raw_size + cert_section_obj = cls(cert_block) + counter.increment(SecBootBlckSize.to_num_blocks(index - offset)) + return cert_section_obj diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/__init__.py new file mode 100644 index 0000000..9fd6074 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module containing various functions/modules used throughout the SPSDK.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/abstract.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/abstract.py new file mode 100644 index 0000000..43c1408 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/abstract.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module for base abstract classes.""" + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from typing_extensions import Self + + +######################################################################################################################## +# Abstract Class for Data Classes +######################################################################################################################## +class BaseClass(ABC): + """Abstract Class for Data Classes.""" + + def __eq__(self, obj: Any) -> bool: + """Check object equality.""" + return isinstance(obj, self.__class__) and vars(obj) == vars(self) + + def __ne__(self, obj: Any) -> bool: + return not self.__eq__(obj) + + @abstractmethod + def __repr__(self) -> str: + """Object representation in string format.""" + + @abstractmethod + def __str__(self) -> str: + """Object description in string format.""" + + @classmethod + @abstractmethod + def parse(cls, data: bytes) -> "Self": + """Deserialize object from bytes array.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/__init__.py new file mode 100644 index 0000000..e9525d4 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module for cryptographic utilities.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/cert_blocks.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/cert_blocks.py new file mode 100644 index 0000000..f2cdf50 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/cert_blocks.py @@ -0,0 +1,850 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module for handling Certificate block.""" + +import logging +import re +from struct import calcsize, pack, unpack_from +from typing import TYPE_CHECKING, List, Optional, Sequence, Union + +from ...crypto.certificate import Certificate +from ...crypto.hash import EnumHashAlgorithm, get_hash +from ...crypto.keys import PrivateKeyRsa, PublicKeyEcc +from ...crypto.utils import extract_public_key_from_data +from ...exceptions import SPSDKError +from ...utils.abstract import BaseClass +from ...utils.crypto.rkht import RKHTv1, RKHTv21 +from ...utils.misc import Endianness, align + +if TYPE_CHECKING: + from typing_extensions import Self + +logger = logging.getLogger(__name__) + + +class CertBlock(BaseClass): + """Common general class for various CertBlocks.""" + + @property + def rkth(self) -> bytes: + """Root Key Table Hash 32-byte hash (SHA-256) of SHA-256 hashes of up to four root public keys.""" + return bytes() + + +######################################################################################################################## +# Certificate Block Header Class +######################################################################################################################## +class CertBlockHeader(BaseClass): + """Certificate block header.""" + + FORMAT = "<4s2H6I" + SIZE = calcsize(FORMAT) + SIGNATURE = b"cert" + + def __init__( + self, version: str = "1.0", flags: int = 0, build_number: int = 0 + ) -> None: + """Constructor. + + :param version: Version of the certificate in format n.n + :param flags: Flags for the Certificate Header + :param build_number: of the certificate + :raises SPSDKError: When there is invalid version + """ + if not re.match(r"[0-9]+\.[0-9]+", version): # check format of the version: N.N + raise SPSDKError("Invalid version") + self.version = version + self.flags = flags + self.build_number = build_number + self.image_length = 0 + self.cert_count = 0 + self.cert_table_length = 0 + + def __repr__(self) -> str: + nfo = f"CertBlockHeader: V={self.version}, F={self.flags}, BN={self.build_number}, IL={self.image_length}, " + nfo += f"CC={self.cert_count}, CTL={self.cert_table_length}" + return nfo + + def __str__(self) -> str: + """Info of the certificate header in text form.""" + nfo = str() + nfo += f" CB Version: {self.version}\n" + nfo += f" CB Flags: {self.flags}\n" + nfo += f" CB Build Number: {self.build_number}\n" + nfo += f" CB Image Length: {self.image_length}\n" + nfo += f" CB Cert. Count: {self.cert_count}\n" + nfo += f" CB Cert. Length: {self.cert_table_length}\n" + return nfo + + def export(self) -> bytes: + """Certificate block in binary form.""" + major_version, minor_version = [int(v) for v in self.version.split(".")] + return pack( + self.FORMAT, + self.SIGNATURE, + major_version, + minor_version, + self.SIZE, + self.flags, + self.build_number, + self.image_length, + self.cert_count, + self.cert_table_length, + ) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Deserialize object from bytes array. + + :param data: Input data as bytes + :return: Certificate Header instance + :raises SPSDKError: Unexpected size or signature of data + """ + if cls.SIZE > len(data): + raise SPSDKError("Incorrect size") + ( + signature, + major_version, + minor_version, + length, + flags, + build_number, + image_length, + cert_count, + cert_table_length, + ) = unpack_from(cls.FORMAT, data) + if signature != cls.SIGNATURE: + raise SPSDKError("Incorrect signature") + if length != cls.SIZE: + raise SPSDKError("Incorrect length") + obj = cls( + version=f"{major_version}.{minor_version}", + flags=flags, + build_number=build_number, + ) + obj.image_length = image_length + obj.cert_count = cert_count + obj.cert_table_length = cert_table_length + return obj + + +######################################################################################################################## +# Certificate Block Class +######################################################################################################################## +class CertBlockV1(CertBlock): + """Certificate block. + + Shared for SB file 2.1 and for MasterBootImage using RSA keys. + """ + + # default size alignment + DEFAULT_ALIGNMENT = 16 + + @property + def header(self) -> CertBlockHeader: + """Certificate block header.""" + return self._header + + @property + def rkh(self) -> List[bytes]: + """List of root keys hashes (SHA-256), each hash as 32 bytes.""" + return self._rkht.rkh_list + + @property + def rkth(self) -> bytes: + """Root Key Table Hash 32-byte hash (SHA-256) of SHA-256 hashes of up to four root public keys.""" + return self._rkht.rkth() + + @property + def rkth_fuses(self) -> List[int]: + """List of RKHT fuses, ordered from highest bit to lowest. + + Note: Returned values are in format that should be passed for blhost + """ + result = [] + rkht = self.rkth + while rkht: + fuse = int.from_bytes(rkht[:4], byteorder=Endianness.LITTLE.value) + result.append(fuse) + rkht = rkht[4:] + return result + + @property + def certificates(self) -> List[Certificate]: + """List of certificates in header. + + First certificate is root certificate and followed by optional chain certificates + """ + return self._cert + + @property + def signature_size(self) -> int: + """Size of the signature in bytes.""" + return len( + self.certificates[0].signature + ) # The certificate is self signed, return size of its signature + + @property + def rkh_index(self) -> Optional[int]: + """Index of the Root Key Hash that matches the certificate; None if does not match.""" + if self._cert: + rkh = self._cert[0].public_key_hash() + for index, value in enumerate(self.rkh): + if rkh == value: + return index + return None + + @property + def alignment(self) -> int: + """Alignment of the binary output, by default it is DEFAULT_ALIGNMENT but can be customized.""" + return self._alignment + + @alignment.setter + def alignment(self, value: int) -> None: + """Setter. + + :param value: new alignment + :raises SPSDKError: When there is invalid alignment + """ + if value <= 0: + raise SPSDKError("Invalid alignment") + self._alignment = value + + @property + def raw_size(self) -> int: + """Aligned size of the certificate block.""" + size = CertBlockHeader.SIZE + size += self._header.cert_table_length + size += self._rkht.RKH_SIZE * self._rkht.RKHT_SIZE + return align(size, self.alignment) + + @property + def expected_size(self) -> int: + """Expected size of binary block.""" + return self.raw_size + + @property + def image_length(self) -> int: + """Image length in bytes.""" + return self._header.image_length + + @image_length.setter + def image_length(self, value: int) -> None: + """Setter. + + :param value: new image length + :raises SPSDKError: When there is invalid image length + """ + if value <= 0: + raise SPSDKError("Invalid image length") + self._header.image_length = value + + def __init__( + self, version: str = "1.0", flags: int = 0, build_number: int = 0 + ) -> None: + """Constructor. + + :param version: of the certificate in format n.n + :param flags: Flags for the Certificate Block Header + :param build_number: of the certificate + """ + self._header = CertBlockHeader(version, flags, build_number) + self._rkht: RKHTv1 = RKHTv1([]) + self._cert: List[Certificate] = [] + self._alignment = self.DEFAULT_ALIGNMENT + + def __len__(self) -> int: + return len(self._cert) + + def set_root_key_hash( + self, index: int, key_hash: Union[bytes, bytearray, Certificate] + ) -> None: + """Add Root Key Hash into RKHT. + + Note: Multiple root public keys are supported to allow for key revocation. + + :param index: The index of Root Key Hash in the table + :param key_hash: The Root Key Hash value (32 bytes, SHA-256); + or Certificate where the hash can be created from public key + :raises SPSDKError: When there is invalid index of root key hash in the table + :raises SPSDKError: When there is invalid length of key hash + """ + if isinstance(key_hash, Certificate): + key_hash = get_hash(key_hash.get_public_key().export()) + assert isinstance(key_hash, (bytes, bytearray)) + if len(key_hash) != self._rkht.RKH_SIZE: + raise SPSDKError("Invalid length of key hash") + self._rkht.set_rkh(index, bytes(key_hash)) + + def add_certificate(self, cert: Union[bytes, Certificate]) -> None: + """Add certificate. + + First call adds root certificate. Additional calls add chain certificates. + + :param cert: The certificate itself in DER format + :raises SPSDKError: If certificate cannot be added + """ + if isinstance(cert, bytes): + cert_obj = Certificate.parse(cert) + elif isinstance(cert, Certificate): + cert_obj = cert + else: + raise SPSDKError("Invalid parameter type (cert)") + if cert_obj.version.name != "v3": + raise SPSDKError( + "Expected certificate v3 but received: " + cert_obj.version.name + ) + if self._cert: # chain certificate? + last_cert = self._cert[-1] # verify that it is signed by parent key + if not cert_obj.validate(last_cert): + raise SPSDKError( + "Chain certificate cannot be verified using parent public key" + ) + else: # root certificate + if not cert_obj.self_signed: + raise SPSDKError( + f"Root certificate must be self-signed.\n{str(cert_obj)}" + ) + self._cert.append(cert_obj) + self._header.cert_count += 1 + self._header.cert_table_length += cert_obj.raw_size + 4 + + def __repr__(self) -> str: + return str(self._header) + + def __str__(self) -> str: + """Text info about certificate block.""" + nfo = str(self.header) + nfo += " Public Root Keys Hash e.g. RKH (SHA256):\n" + rkh_index = self.rkh_index + for index, root_key in enumerate(self._rkht.rkh_list): + nfo += f" {index}) {root_key.hex().upper()} {'<- Used' if index == rkh_index else ''}\n" + rkth = self.rkth + nfo += f" RKTH (SHA256): {rkth.hex().upper()}\n" + for index, fuse in enumerate(self.rkth_fuses): + bit_ofs = (len(rkth) - 4 * index) * 8 + nfo += f" - RKTH fuse [{bit_ofs:03}:{bit_ofs - 31:03}]: {fuse:08X}\n" + for index, cert in enumerate(self._cert): + nfo += " Root Certificate:\n" if index == 0 else f" Certificate {index}:\n" + nfo += str(cert) + return nfo + + def verify_data(self, signature: bytes, data: bytes) -> bool: + """Signature verification. + + :param signature: to be verified + :param data: that has been signed + :return: True if the data signature can be confirmed using the certificate; False otherwise + """ + cert = self._cert[-1] + pub_key = cert.get_public_key() + return pub_key.verify_signature(signature=signature, data=data) + + def verify_private_key(self, private_key: PrivateKeyRsa) -> bool: + """Verify that given private key matches the public certificate. + + :param private_key: to be tested + :return: True if yes; False otherwise + """ + cert = self.certificates[-1] # last certificate + pub_key = cert.get_public_key() + return private_key.verify_public_key(pub_key) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Deserialize CertBlockV1 from binary file. + + :param data: Binary data + :return: Certificate Block instance + :raises SPSDKError: Length of the data doesn't match Certificate Block length + """ + header = CertBlockHeader.parse(data) + offset = CertBlockHeader.SIZE + if len(data) < ( + header.cert_table_length + (RKHTv1.RKHT_SIZE * RKHTv1.RKH_SIZE) + ): + raise SPSDKError( + "Length of the data doesn't match Certificate Block length" + ) + obj = cls( + version=header.version, flags=header.flags, build_number=header.build_number + ) + for _ in range(header.cert_count): + cert_len = unpack_from(" PublicKeyEcc: + """Convert key into EccKey instance.""" + if isinstance(key, PublicKeyEcc): + return key + try: + pub_key = extract_public_key_from_data(key) + if not isinstance(pub_key, PublicKeyEcc): + raise SPSDKError("Not ECC key") + return pub_key + except Exception: + pass + # Just recreate public key from the parsed data + return PublicKeyEcc.parse(key) + + +class CertificateBlockHeader(BaseClass): + """Create Certificate block header.""" + + FORMAT = "<4s2HL" + SIZE = calcsize(FORMAT) + MAGIC = b"chdr" + + def __init__(self, format_version: str = "2.1") -> None: + """Constructor for Certificate block header version 2.1. + + :param format_version: Major = 2, minor = 1 + """ + self.format_version = format_version + self.cert_block_size = 0 + + def export(self) -> bytes: + """Export Certificate block header as bytes array.""" + major_format_version, minor_format_version = [ + int(v) for v in self.format_version.split(".") + ] + + return pack( + self.FORMAT, + self.MAGIC, + minor_format_version, + major_format_version, + self.cert_block_size, + ) + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse Certificate block header from bytes array. + + :param data: Input data as bytes + :raises SPSDKError: Raised when SIZE is bigger than length of the data without offset + :raises SPSDKError: Raised when magic is not equal MAGIC + :return: CertificateBlockHeader + """ + if cls.SIZE > len(data): + raise SPSDKError("SIZE is bigger than length of the data without offset") + ( + magic, + minor_format_version, + major_format_version, + cert_block_size, + ) = unpack_from(cls.FORMAT, data) + + if magic != cls.MAGIC: + raise SPSDKError("Magic is not same!") + + obj = cls(format_version=f"{major_format_version}.{minor_format_version}") + obj.cert_block_size = cert_block_size + return obj + + def __len__(self) -> int: + """Length of the Certificate block header.""" + return calcsize(self.FORMAT) + + def __repr__(self) -> str: + return f"Cert block header {self.format_version}" + + def __str__(self) -> str: + """Get info of Certificate block header.""" + info = f"Format version: {self.format_version}\n" + info += f"Certificate block size: {self.cert_block_size}\n" + return info + + +class RootKeyRecord(BaseClass): + """Create Root key record.""" + + # P-256 + + def __init__( + self, + ca_flag: bool, + root_certs: Optional[Union[Sequence[PublicKeyEcc], Sequence[bytes]]] = None, + used_root_cert: int = 0, + ) -> None: + """Constructor for Root key record. + + :param ca_flag: CA flag + :param root_certs: Root cert used to ISK/image signature + :param used_root_cert: Used root cert number 0-3 + """ + self.ca_flag = ca_flag + self.root_certs_input = root_certs + self.root_certs: List[PublicKeyEcc] = [] + self.used_root_cert = used_root_cert + self.flags = 0 + self._rkht = RKHTv21([]) + self.root_public_key = b"" + + @property + def number_of_certificates(self) -> int: + """Get number of included certificates.""" + return (self.flags & 0xF0) >> 4 + + @property + def expected_size(self) -> int: + """Get expected binary block size.""" + # the '4' means 4 bytes for flags + return 4 + len(self._rkht.export()) + len(self.root_public_key) + + def __repr__(self) -> str: + cert_type = {0x1: "secp256r1", 0x2: "secp384r1"}[self.flags & 0xF] + return f"Cert Block: Root Key Record - ({cert_type})" + + def __str__(self) -> str: + """Get info of Root key record.""" + cert_type = {0x1: "secp256r1", 0x2: "secp384r1"}[self.flags & 0xF] + info = "" + info += f"Flags: {hex(self.flags)}\n" + info += f" - CA: {bool(self.ca_flag)}, ISK Certificate is {'not ' if self.ca_flag else ''}mandatory\n" + info += f" - Used Root c.:{self.used_root_cert}\n" + info += f" - Number of c.:{self.number_of_certificates}\n" + info += f" - Cert. type: {cert_type}\n" + if self.root_certs: + info += f"Root certs: {self.root_certs}\n" + if self._rkht.rkh_list: + info += f"CTRK Hash table: {self._rkht.export().hex()}\n" + if self.root_public_key: + info += ( + f"Root public key: {str(convert_to_ecc_key(self.root_public_key))}\n" + ) + + return info + + def _calculate_flags(self) -> int: + """Function to calculate parameter flags.""" + flags = 0 + if self.ca_flag is True: + flags |= 1 << 31 + if self.used_root_cert: + flags |= self.used_root_cert << 8 + flags |= len(self.root_certs) << 4 + if self.root_certs[0].curve in ["NIST P-256", "p256", "secp256r1"]: + flags |= 1 << 0 + if self.root_certs[0].curve in ["NIST P-384", "p384", "secp384r1"]: + flags |= 1 << 1 + return flags + + def _create_root_public_key(self) -> bytes: + """Function to create root public key.""" + root_key = self.root_certs[self.used_root_cert] + root_key_data = root_key.export() + return root_key_data + + def calculate(self) -> None: + """Calculate all internal members. + + :raises SPSDKError: The RKHT certificates inputs are missing. + """ + # pylint: disable=invalid-name + if not self.root_certs_input: + raise SPSDKError( + "Root Key Record: The root of trust certificates are not specified." + ) + self.root_certs = [convert_to_ecc_key(cert) for cert in self.root_certs_input] + self.flags = self._calculate_flags() + self._rkht = RKHTv21.from_keys(keys=self.root_certs) + if self._rkht.hash_algorithm != self.get_hash_algorithm(self.flags): + raise SPSDKError("Hash algorithm does not match the key size.") + self.root_public_key = self._create_root_public_key() + + def export(self) -> bytes: + """Export Root key record as bytes array.""" + data = bytes() + data += pack(" EnumHashAlgorithm: + """Get CTRK table hash algorithm. + + :param flags: Root Key Record flags + :return: Name of hash algorithm + """ + return {1: EnumHashAlgorithm.SHA256, 2: EnumHashAlgorithm.SHA384}[flags & 0xF] + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse Root key record from bytes array. + + :param data: Input data as bytes array + :return: Root key record object + """ + (flags,) = unpack_from("> 8 + number_of_hashes = (flags & 0xF0) >> 4 + rotkh_len = {0x0: 32, 0x1: 32, 0x2: 48}[flags & 0xF] + root_key_record = cls( + ca_flag=ca_flag, root_certs=[], used_root_cert=used_rot_ix + ) + root_key_record.flags = flags + offset = 4 # move offset just after FLAGS + if number_of_hashes > 1: + rkht_len = rotkh_len * number_of_hashes + rkht = data[offset : offset + rkht_len] + offset += rkht_len + root_key_record.root_public_key = data[offset : offset + rotkh_len * 2] + root_key_record._rkht = ( + RKHTv21.parse(rkht, cls.get_hash_algorithm(flags)) + if number_of_hashes > 1 + else RKHTv21( + [ + get_hash( + root_key_record.root_public_key, cls.get_hash_algorithm(flags) + ) + ] + ) + ) + return root_key_record + + +class IskCertificate(BaseClass): + """Create ISK certificate.""" + + def __init__( + self, + constraints: int = 0, + isk_cert: Optional[Union[PublicKeyEcc, bytes]] = None, + user_data: Optional[bytes] = None, + offset_present: bool = True, + ) -> None: + """Constructor for ISK certificate. + + :param constraints: Certificate version + :param isk_cert: ISK certificate + :param user_data: User data + """ + self.flags = 0 + self.offset_present = offset_present + self.constraints = constraints + self.isk_cert = convert_to_ecc_key(isk_cert) if isk_cert else None + self.user_data = user_data or bytes() + self.signature = bytes() + self.coordinate_length = 0 + self.isk_public_key_data = self.isk_cert.export() if self.isk_cert else bytes() + + self._calculate_flags() + + @property + def signature_offset(self) -> int: + """Signature offset inside the ISK Certificate.""" + offset = calcsize("<3L") if self.offset_present else calcsize("<2L") + signature_offset = offset + len(self.user_data) + if self.isk_cert: + signature_offset += 2 * self.isk_cert.coordinate_size + + return signature_offset + + @property + def expected_size(self) -> int: + """Binary block expected size.""" + sign_len = len(self.signature) or 0 + pub_key_len = ( + self.isk_cert.coordinate_size * 2 + if self.isk_cert + else len(self.isk_public_key_data) + ) + + offset = 4 if self.offset_present else 0 + return ( + offset # signature offset + + 4 # constraints + + 4 # flags + + pub_key_len # isk public key coordinates + + len(self.user_data) # user data + + sign_len # isk blob signature + ) + + def __repr__(self) -> str: + isk_type = {0: "secp256r1", 1: "secp256r1", 2: "secp384r1"}[self.flags & 0xF] + return f"ISK Certificate, {isk_type}" + + def __str__(self) -> str: + """Get info about ISK certificate.""" + isk_type = {0: "secp256r1", 1: "secp256r1", 2: "secp384r1"}[self.flags & 0xF] + info = "" + info += f"Constraints: {self.constraints}\n" + info += f"Flags: {self.flags}\n" + if self.user_data: + info += f"User data: {self.user_data.hex()}\n" + else: + info += "User data: Not included\n" + info += f"Type: {isk_type}\n" + info += f"Public Key: {str(self.isk_cert)}\n" + return info + + def _calculate_flags(self) -> None: + """Function to calculate parameter flags.""" + self.flags = 0 + if self.user_data: + self.flags |= 1 << 31 + assert self.isk_cert + if self.isk_cert.curve == "secp256r1": + self.flags |= 1 << 0 + if self.isk_cert.curve == "secp384r1": + self.flags |= 1 << 1 + + def create_isk_signature(self, key_record_data: bytes, force: bool = False) -> None: + """Function to create ISK signature. + + :raises SPSDKError: Signature provider is not specified. + """ + # pylint: disable=invalid-name + if self.signature and not force: + return + raise SPSDKError("ISK Certificate: The signature provider is not specified.") + + @classmethod + def parse(cls, data: bytes, signature_size: int) -> "Self": # type: ignore # pylint: disable=arguments-differ + """Parse ISK certificate from bytes array.This operation is not supported. + + :param data: Input data as bytes array + :param signature_size: The signature size of ISK block + :raises NotImplementedError: This operation is not supported + """ + (signature_offset, constraints, isk_flags) = unpack_from("<3L", data) + header_word_cnt = 3 + if ( + signature_offset & 0xFFFF == 0x4D43 + ): # This means that certificate has no offset + (constraints, isk_flags) = unpack_from("<2L", data) + signature_offset = 72 + header_word_cnt = 2 + user_data_flag = bool(isk_flags & 0x80000000) + isk_pub_key_length = {0x0: 32, 0x1: 32, 0x2: 48}[isk_flags & 0xF] + offset = header_word_cnt * 4 + isk_pub_key_bytes = data[offset : offset + isk_pub_key_length * 2] + offset += isk_pub_key_length * 2 + user_data = data[offset:signature_offset] if user_data_flag else None + signature = data[signature_offset : signature_offset + signature_size] + offset_present = header_word_cnt == 3 + certificate = cls( + constraints=constraints, + isk_cert=isk_pub_key_bytes, + user_data=user_data, + offset_present=offset_present, + ) + certificate.signature = signature + return certificate + + +class CertBlockV21(CertBlock): + """Create Certificate block version 2.1. + + Used for SB 3.1 and MBI using ECC keys. + """ + + MAGIC = b"chdr" + FORMAT_VERSION = "2.1" + + def __init__(self) -> None: + """The Constructor for Certificate block.""" + self.header = CertificateBlockHeader("2.1") + self.root_key_record = RootKeyRecord( + ca_flag=False, used_root_cert=0, root_certs=None + ) + + self.isk_certificate: Optional[IskCertificate] = None + + def _set_ca_flag(self, value: bool) -> None: + self.root_key_record.ca_flag = value + + def calculate(self) -> None: + """Calculate all internal members.""" + self.root_key_record.calculate() + + @property + def signature_size(self) -> int: + """Size of the signature in bytes.""" + # signature size is same as public key data + if self.isk_certificate: + return len(self.isk_certificate.isk_public_key_data) + + return len(self.root_key_record.root_public_key) + + @property + def expected_size(self) -> int: + """Expected size of binary block.""" + expected_size = self.header.SIZE + expected_size += self.root_key_record.expected_size + if self.isk_certificate: + expected_size += self.isk_certificate.expected_size + return expected_size + + @property + def rkth(self) -> bytes: + """Root Key Table Hash 32-byte hash (SHA-256) of SHA-256 hashes of up to four root public keys.""" + return self.root_key_record._rkht.rkth() + + def __repr__(self) -> str: + return f"Cert block 2.1, Size:{self.expected_size}B" + + def __str__(self) -> str: + """Get info of Certificate block.""" + msg = f"HEADER:\n{str(self.header)}\n" + msg += f"ROOT KEY RECORD:\n{str(self.root_key_record)}\n" + if self.isk_certificate: + msg += f"ISK Certificate:\n{str(self.isk_certificate)}\n" + return msg + + @classmethod + def parse(cls, data: bytes) -> "Self": + """Parse Certificate block from bytes array.This operation is not supported. + + :param data: Input data as bytes array + :raises SPSDKError: Magic do not match + """ + # CertificateBlockHeader + cert_header = CertificateBlockHeader.parse(data) + offset = len(cert_header) + # RootKeyRecord + root_key_record = RootKeyRecord.parse(data[offset:]) + offset += root_key_record.expected_size + # IskCertificate + isk_certificate = None + if root_key_record.ca_flag == 0: + isk_certificate = IskCertificate.parse( + data[offset:], len(root_key_record.root_public_key) + ) + # Certification Block V2.1 + cert_block = cls() + cert_block.header = cert_header + cert_block.root_key_record = root_key_record + cert_block.isk_certificate = isk_certificate + return cert_block + + def validate(self) -> None: + """Validate the settings of class members. + + :raises SPSDKError: Invalid configuration of certification block class members. + """ + self.header.parse(self.header.export()) + if self.isk_certificate and not self.isk_certificate.signature: + raise SPSDKError("Invalid ISK certificate.") diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/rkht.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/rkht.py new file mode 100644 index 0000000..316b964 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/crypto/rkht.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2022-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""The module provides support for Root Key Hash table.""" + +import logging +import math +from abc import abstractmethod +from typing import TYPE_CHECKING, List, Optional, Sequence, Union + +from ...crypto.certificate import Certificate +from ...crypto.hash import EnumHashAlgorithm, get_hash, get_hash_length +from ...crypto.keys import PrivateKey, PublicKey, PublicKeyEcc, PublicKeyRsa +from ...crypto.utils import extract_public_key, extract_public_key_from_data +from ...exceptions import SPSDKError +from ...utils.misc import Endianness + +if TYPE_CHECKING: + from typing_extensions import Self + +logger = logging.getLogger(__name__) + + +class RKHT: + """Root Key Hash Table class.""" + + def __init__(self, rkh_list: List[bytes]) -> None: + """Initialization of Root Key Hash Table class. + + :param rkh_list: List of Root Key Hashes + """ + if len(rkh_list) > 4: + raise SPSDKError("Number of Root Key Hashes can not be larger than 4.") + self.rkh_list = rkh_list + + @classmethod + def from_keys( + cls, + keys: Sequence[ + Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate] + ], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> "Self": + """Create RKHT from list of keys. + + :param keys: List of public keys/certificates/private keys/bytes + :param password: Optional password to open secured private keys, defaults to None + :param search_paths: List of paths where to search for the file, defaults to None + """ + public_keys = ( + [cls.convert_key(x, password, search_paths=search_paths) for x in keys] + if keys + else [] + ) + if not all(isinstance(x, type(public_keys[0])) for x in public_keys): + raise SPSDKError("RKHT must contains all keys of a same instances.") + if not all( + cls._get_hash_algorithm(x) == cls._get_hash_algorithm(public_keys[0]) + for x in public_keys + ): + raise SPSDKError("RKHT must have same hash algorithm for all keys.") + + rotk_hashes = [cls._calc_key_hash(key) for key in public_keys] + return cls(rotk_hashes) + + @abstractmethod + def rkth(self) -> bytes: + """Root Key Table Hash. + + :return: Hash of hashes of public keys. + """ + + @staticmethod + def _get_hash_algorithm(key: PublicKey) -> EnumHashAlgorithm: + """Get hash algorithm output size for the key. + + :param key: Key to get hash. + :raises SPSDKError: Invalid kye type. + :return: Size in bits of hash. + """ + if isinstance(key, PublicKeyEcc): + return EnumHashAlgorithm.from_label(f"sha{key.key_size}") + + if isinstance(key, PublicKeyRsa): + # In case of RSA keys, hash is always SHA-256, regardless of the key length + return EnumHashAlgorithm.SHA256 + + raise SPSDKError("Unsupported key type to load.") + + @property + def hash_algorithm(self) -> EnumHashAlgorithm: + """Used hash algorithm name.""" + if not len(self.rkh_list) > 0: + raise SPSDKError("Unknown hash algorighm name. No root key hashes.") + return EnumHashAlgorithm.from_label(f"sha{self.hash_algorithm_size}") + + @property + def hash_algorithm_size(self) -> int: + """Used hash algorithm size in bites.""" + if not len(self.rkh_list) > 0: + raise SPSDKError("Unknown hash algorithm size. No public keys provided.") + return len(self.rkh_list[0]) * 8 + + @staticmethod + def _calc_key_hash( + public_key: PublicKey, + algorithm: Optional[EnumHashAlgorithm] = None, + ) -> bytes: + """Calculate a hash out of public key's exponent and modulus in RSA case, X/Y in EC. + + :param public_key: List of public keys to compute hash from. + :param sha_width: Used hash algorithm. + :raises SPSDKError: Unsupported public key type + :return: Computed hash. + """ + n_1 = 0 + n_2 = 0 + if isinstance(public_key, PublicKeyRsa): + n_1 = public_key.e + n1_len = math.ceil(n_1.bit_length() / 8) + n_2 = public_key.n + n2_len = math.ceil(n_2.bit_length() / 8) + elif isinstance(public_key, PublicKeyEcc): + n_1 = public_key.y + n_2 = public_key.x + n1_len = n2_len = public_key.coordinate_size + else: + raise SPSDKError(f"Unsupported key type: {type(public_key)}") + + n1_bytes = n_1.to_bytes(n1_len, Endianness.BIG.value) + n2_bytes = n_2.to_bytes(n2_len, Endianness.BIG.value) + + algorithm = algorithm or RKHT._get_hash_algorithm(public_key) + return get_hash(n2_bytes + n1_bytes, algorithm=algorithm) + + @staticmethod + def convert_key( + key: Union[str, bytes, bytearray, PublicKey, PrivateKey, Certificate], + password: Optional[str] = None, + search_paths: Optional[List[str]] = None, + ) -> PublicKey: + """Convert practically whole input that could hold Public key into public key. + + :param key: Public key in Certificate/Private key, Public key as a path to file, + loaded bytes or supported class. + :param password: Optional password to open secured private keys, defaults to None. + :param search_paths: List of paths where to search for the file, defaults to None + :raises SPSDKError: Invalid kye type. + :return: Public Key object. + """ + if isinstance(key, PublicKey): + return key + + if isinstance(key, PrivateKey): + return key.get_public_key() + + if isinstance(key, Certificate): + return key.get_public_key() + + if isinstance(key, str): + return extract_public_key(key, password, search_paths=search_paths) + + if isinstance(key, (bytes, bytearray)): + return extract_public_key_from_data(key, password) + + raise SPSDKError("RKHT: Unsupported key to load.") + + +class RKHTv1(RKHT): + """Root Key Hash Table class for cert block v1.""" + + RKHT_SIZE = 4 + RKH_SIZE = 32 + + def __init__( + self, + rkh_list: List[bytes], + ) -> None: + """Initialization of Root Key Hash Table class. + + :param rkh_list: List of Root Key Hashes + """ + for key_hash in rkh_list: + if len(key_hash) != self.RKH_SIZE: + raise SPSDKError(f"Invalid key hash size: {len(key_hash)}") + super().__init__(rkh_list) + + @property + def hash_algorithm(self) -> EnumHashAlgorithm: + """Used Hash algorithm name.""" + return EnumHashAlgorithm.SHA256 + + def export(self) -> bytes: + """Export RKHT as bytes.""" + rotk_table = b"" + for i in range(self.RKHT_SIZE): + if i < len(self.rkh_list) and self.rkh_list[i]: + rotk_table += self.rkh_list[i] + else: + rotk_table += bytes(self.RKH_SIZE) + if len(rotk_table) != self.RKH_SIZE * self.RKHT_SIZE: + raise SPSDKError("Invalid length of data.") + return rotk_table + + @classmethod + def parse(cls, rkht: bytes) -> "Self": + """Parse Root Key Hash Table into RKHTv1 object. + + :param rkht: Valid RKHT table + """ + rotkh_len = len(rkht) // cls.RKHT_SIZE + offset = 0 + key_hashes = [] + for _ in range(cls.RKHT_SIZE): + key_hashes.append(rkht[offset : offset + rotkh_len]) + offset += rotkh_len + return cls(key_hashes) + + def rkth(self) -> bytes: + """Root Key Table Hash. + + :return: Hash of Hashes of public key. + """ + rotkh = get_hash(self.export(), self.hash_algorithm) + return rotkh + + def set_rkh(self, index: int, rkh: bytes) -> None: + """Set Root Key Hash with index. + + :param index: Index in the hash table + :param rkh: Root Key Hash to be set + """ + if index > 3: + raise SPSDKError("Key hash can not be larger than 3.") + if self.rkh_list and len(rkh) != len(self.rkh_list[0]): + raise SPSDKError("Root Key Hash must be the same size as other hashes.") + # fill the gap with zeros if the keys are not consecutive + for idx in range(index + 1): + if len(self.rkh_list) < idx + 1: + self.rkh_list.append(bytes(self.RKH_SIZE)) + assert len(self.rkh_list) <= 4 + self.rkh_list[index] = rkh + + +class RKHTv21(RKHT): + """Root Key Hash Table class for cert block v2.1.""" + + def export(self) -> bytes: + """Export RKHT as bytes.""" + hash_table = bytes() + if len(self.rkh_list) > 1: + hash_table = bytearray().join(self.rkh_list) + return hash_table + + @classmethod + def parse(cls, rkht: bytes, hash_algorithm: EnumHashAlgorithm) -> "Self": + """Parse Root Key Hash Table into RKHTv21 object. + + :param rkht: Valid RKHT table + :param hash_algorithm: Hash algorithm to be used + """ + rkh_len = get_hash_length(hash_algorithm) + if len(rkht) % rkh_len != 0: + raise SPSDKError( + f"The length of Root Key Hash Table does not match the hash algorithm {hash_algorithm}" + ) + offset = 0 + rkh_list = [] + rkht_size = len(rkht) // rkh_len + for _ in range(rkht_size): + rkh_list.append(rkht[offset : offset + rkh_len]) + offset += rkh_len + return cls(rkh_list) + + def rkth(self) -> bytes: + """Root Key Table Hash. + + :return: Hash of Hashes of public key. + """ + if not self.rkh_list: + logger.debug("RKHT has no records.") + return bytes() + if len(self.rkh_list) == 1: + rotkh = self.rkh_list[0] + else: + rotkh = get_hash(self.export(), self.hash_algorithm) + return rotkh diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/exceptions.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/exceptions.py new file mode 100644 index 0000000..6bf311b --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/exceptions.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2021-2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module provides exceptions for SPSDK utilities.""" +from ..exceptions import SPSDKError + + +class SPSDKTimeoutError(TimeoutError, SPSDKError): + """SPSDK Timeout.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/__init__.py new file mode 100644 index 0000000..7a3c4d8 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Device Interfaces.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/commands.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/commands.py new file mode 100644 index 0000000..232ec05 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/commands.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Generic commands implementation.""" +from abc import ABC, abstractmethod + + +class CmdResponseBase(ABC): + """Response base format class.""" + + @abstractmethod + def __str__(self) -> str: + """Get object info.""" + + @property + @abstractmethod + def value(self) -> int: + """Return a integer representation of the response.""" + + +class CmdPacketBase(ABC): + """COmmand protocol base.""" + + @abstractmethod + def to_bytes(self, padding: bool = True) -> bytes: + """Serialize CmdPacket into bytes. + + :param padding: If True, add padding to specific size + :return: Serialized object into bytes + """ diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/__init__.py new file mode 100644 index 0000000..28f99a1 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module implementing the low level device.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/base.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/base.py new file mode 100644 index 0000000..bc39113 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/base.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Low level device base class.""" +import logging +from abc import ABC, abstractmethod +from types import TracebackType +from typing import TYPE_CHECKING, Optional, Type + +if TYPE_CHECKING: + from typing_extensions import Self + +logger = logging.getLogger(__name__) + + +class DeviceBase(ABC): + """Device base class.""" + + def __enter__(self) -> "Self": + self.open() + return self + + def __exit__( + self, + exception_type: Optional[Type[Exception]] = None, + exception_value: Optional[Exception] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() + + @property + @abstractmethod + def is_opened(self) -> bool: + """Indicates whether interface is open.""" + + @abstractmethod + def open(self) -> None: + """Open the interface.""" + + @abstractmethod + def close(self) -> None: + """Close the interface.""" + + @abstractmethod + def read(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read data from the device. + + :param length: Length of data to be read + :param timeout: Read timeout to be applied + """ + + @abstractmethod + def write(self, data: bytes, timeout: Optional[int] = None) -> None: + """Write data to the device. + + :param data: Data to be written + :param timeout: Read timeout to be applied + """ + + @property + @abstractmethod + def timeout(self) -> int: + """Timeout property.""" + + @timeout.setter + @abstractmethod + def timeout(self, value: int) -> None: + """Timeout property setter.""" + + @abstractmethod + def __str__(self) -> str: + """Return string containing information about the interface.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/usb_device.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/usb_device.py new file mode 100644 index 0000000..86dba82 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/device/usb_device.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Low level Hid device.""" +import logging +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple + +import libusbsio + +from ....exceptions import SPSDKConnectionError, SPSDKError +from ....utils.exceptions import SPSDKTimeoutError +from ....utils.interfaces.device.base import DeviceBase +from ....utils.misc import get_hash +from ....utils.usbfilter import NXPUSBDeviceFilter, USBDeviceFilter + +if TYPE_CHECKING: + from typing_extensions import Self + +logger = logging.getLogger(__name__) + + +class UsbDevice(DeviceBase): + """USB device class.""" + + def __init__( + self, + vid: Optional[int] = None, + pid: Optional[int] = None, + path: Optional[bytes] = None, + serial_number: Optional[str] = None, + vendor_name: Optional[str] = None, + product_name: Optional[str] = None, + interface_number: Optional[int] = None, + timeout: Optional[int] = None, + ) -> None: + """Initialize the USB interface object.""" + self._opened = False + self.vid = vid or 0 + self.pid = pid or 0 + self.path = path or b"" + self.serial_number = serial_number or "" + self.vendor_name = vendor_name or "" + self.product_name = product_name or "" + self.interface_number = interface_number or 0 + self._timeout = timeout or 2000 + libusbsio_logger = logging.getLogger("libusbsio") + self._device: libusbsio.LIBUSBSIO.HID_DEVICE = libusbsio.usbsio( + loglevel=libusbsio_logger.getEffectiveLevel() + ).HIDAPI_DeviceCreate() + + @property + def timeout(self) -> int: + """Timeout property.""" + return self._timeout + + @timeout.setter + def timeout(self, value: int) -> None: + """Timeout property setter.""" + self._timeout = value + + @property + def is_opened(self) -> bool: + """Indicates whether device is open. + + :return: True if device is open, False othervise. + """ + return self._opened + + def open(self) -> None: + """Open the interface. + + :raises SPSDKError: if device is already opened + :raises SPSDKConnectionError: if the device can not be opened + """ + logger.debug(f"Opening the Interface: {str(self)}") + if self.is_opened: + # This would get HID_DEVICE into broken state + raise SPSDKError("Can't open already opened device") + try: + self._device.Open(self.path) + self._opened = True + except Exception as error: + raise SPSDKConnectionError( + f"Unable to open device '{str(self)}'" + ) from error + + def close(self) -> None: + """Close the interface. + + :raises SPSDKConnectionError: if no device is available + :raises SPSDKConnectionError: if the device can not be opened + """ + logger.debug(f"Closing the Interface: {str(self)}") + if self.is_opened: + try: + self._device.Close() + self._opened = False + except Exception as error: + raise SPSDKConnectionError( + f"Unable to close device '{str(self)}'" + ) from error + + def read(self, length: int, timeout: Optional[int] = None) -> bytes: + """Read data on the IN endpoint associated to the HID interface. + + :return: Return CmdResponse object. + :raises SPSDKConnectionError: Raises an error if device is not opened for reading + :raises SPSDKConnectionError: Raises if device is not available + :raises SPSDKConnectionError: Raises if reading fails + :raises SPSDKTimeoutError: Time-out + """ + timeout = timeout or self.timeout + if not self.is_opened: + raise SPSDKConnectionError("Device is not opened for reading") + try: + (data, result) = self._device.Read(length, timeout_ms=timeout) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if not data: + logger.error(f"Cannot read from HID device, error={result}") + raise SPSDKTimeoutError() + assert isinstance(data, bytes) + return data + + def write(self, data: bytes, timeout: Optional[int] = None) -> None: + """Send data to device. + + :param data: Data to send + :param timeout: Timeout to be used + :raises SPSDKConnectionError: Sending data to device failure + """ + timeout = timeout or self.timeout + if not self.is_opened: + raise SPSDKConnectionError("Device is not opened for writing") + try: + bytes_written = self._device.Write(data, timeout_ms=timeout) + except Exception as e: + raise SPSDKConnectionError(str(e)) from e + if bytes_written < 0 or bytes_written < len(data): + raise SPSDKConnectionError( + f"Invalid size of written bytes has been detected: {bytes_written} != {len(data)}" + ) + + def __str__(self) -> str: + """Return information about the USB interface.""" + return ( + f"{self.product_name:s} (0x{self.vid:04X}, 0x{self.pid:04X})" + f"path={self.path!r} sn='{self.serial_number}'" + ) + + @property + def path_str(self) -> str: + """BLHost-friendly string representation of USB path.""" + return NXPUSBDeviceFilter.convert_usb_path(self.path) + + @property + def path_hash(self) -> str: + """BLHost-friendly hash of the USB path.""" + return get_hash(self.path) + + def __hash__(self) -> int: + return hash(self.path) + + @classmethod + def scan( + cls, + device_id: Optional[str] = None, + usb_devices_filter: Optional[Dict[str, Tuple[int, int]]] = None, + timeout: Optional[int] = None, + ) -> List["Self"]: + """Scan connected USB devices. + + :param device_id: Device identifier , , device/instance path, device name are supported + :param usb_devices_filter: Dictionary holding NXP device vid/pid {"device_name": [vid(int), pid(int)]}. + If set, only devices included in the dictionary will be scanned + :param timeout: Read/write timeout + :return: list of matching RawHid devices + """ + usb_filter = NXPUSBDeviceFilter( + usb_id=device_id, nxp_device_names=usb_devices_filter + ) + devices = cls.enumerate(usb_filter, timeout=timeout) + return devices + + @classmethod + def enumerate( + cls, usb_device_filter: USBDeviceFilter, timeout: Optional[int] = None + ) -> List["Self"]: + """Get list of all connected devices which matches device_id. + + :param usb_device_filter: USBDeviceFilter object + :param timeout: Default timeout to be set + :return: List of interfaces found + """ + devices = [] + libusbsio_logger = logging.getLogger("libusbsio") + sio = libusbsio.usbsio(loglevel=libusbsio_logger.getEffectiveLevel()) + all_hid_devices = sio.HIDAPI_Enumerate() + + # iterate on all devices found + for dev in all_hid_devices: + if usb_device_filter.compare(vars(dev)) is True: + new_device = cls( + vid=dev["vendor_id"], + pid=dev["product_id"], + path=dev["path"], + vendor_name=dev["manufacturer_string"], + product_name=dev["product_string"], + interface_number=dev["interface_number"], + timeout=timeout, + ) + devices.append(new_device) + return devices diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/protocol/__init__.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/protocol/__init__.py new file mode 100644 index 0000000..e06c1a1 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/protocol/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Protocol base.""" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py new file mode 100644 index 0000000..6bd4a85 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/interfaces/protocol/protocol_base.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2023-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Protocol base.""" +from abc import ABC, abstractmethod +from types import TracebackType +from typing import TYPE_CHECKING, List, Optional, Type, Union + +from ....utils.interfaces.commands import CmdPacketBase, CmdResponseBase +from ....utils.interfaces.device.base import DeviceBase + +if TYPE_CHECKING: + from typing_extensions import Self + + +class ProtocolBase(ABC): + """Protocol base class.""" + + device: DeviceBase + identifier: str + + def __init__(self, device: DeviceBase) -> None: + """Initialize the MbootSerialProtocol object. + + :param device: The device instance + """ + self.device = device + + def __str__(self) -> str: + return f"identifier='{self.identifier}', device={self.device}" + + def __enter__(self) -> "Self": + self.open() + return self + + def __exit__( + self, + exception_type: Optional[Type[Exception]] = None, + exception_value: Optional[Exception] = None, + traceback: Optional[TracebackType] = None, + ) -> None: + self.close() + + @abstractmethod + def open(self) -> None: + """Open the interface.""" + + @abstractmethod + def close(self) -> None: + """Close the interface.""" + + @property + @abstractmethod + def is_opened(self) -> bool: + """Indicates whether interface is open.""" + + @classmethod + @abstractmethod + def scan_from_args( + cls, + params: str, + timeout: int, + extra_params: Optional[str] = None, + ) -> List["Self"]: + """Scan method.""" + + @abstractmethod + def write_command(self, packet: CmdPacketBase) -> None: + """Write command to the device. + + :param packet: Command packet to be sent + """ + + @abstractmethod + def write_data(self, data: bytes) -> None: + """Write data to the device. + + :param data: Data to be send + """ + + @abstractmethod + def read(self, length: Optional[int] = None) -> Union[CmdResponseBase, bytes]: + """Read data from device. + + :return: read data + """ diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/misc.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/misc.py new file mode 100644 index 0000000..8677f61 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/misc.py @@ -0,0 +1,502 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# Copyright 2020-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Miscellaneous functions used throughout the SPSDK.""" +import contextlib +import hashlib +import logging +import os +import re +from enum import Enum +from math import ceil +from typing import Callable, Iterable, Iterator, List, Optional, TypeVar, Union + +from ..crypto.rng import random_bytes +from ..exceptions import SPSDKError, SPSDKValueError + +# for generics +T = TypeVar("T") # pylint: disable=invalid-name + +logger = logging.getLogger(__name__) + + +class Endianness(str, Enum): + """Endianness enum.""" + + BIG = "big" + LITTLE = "little" + + @classmethod + def values(cls) -> List[str]: + """Get enumeration values.""" + return [mem.value for mem in Endianness.__members__.values()] + + +class BinaryPattern: + """Binary pattern class. + + Supported patterns: + - rand: Random Pattern + - zeros: Filled with zeros + - ones: Filled with all ones + - inc: Filled with repeated numbers incremented by one 0-0xff + - any kind of number, that will be repeated to fill up whole image. + The format could be decimal, hexadecimal, bytes. + """ + + SPECIAL_PATTERNS = ["rand", "zeros", "ones", "inc"] + + def __init__(self, pattern: str) -> None: + """Constructor of pattern class. + + :param pattern: Supported patterns: + - rand: Random Pattern + - zeros: Filled with zeros + - ones: Filled with all ones + - inc: Filled with repeated numbers incremented by one 0-0xff + - any kind of number, that will be repeated to fill up whole image. + The format could be decimal, hexadecimal, bytes. + :raises SPSDKValueError: Unsupported pattern detected. + """ + try: + value_to_int(pattern) + except SPSDKError: + if pattern not in BinaryPattern.SPECIAL_PATTERNS: + raise SPSDKValueError( # pylint: disable=raise-missing-from + f"Unsupported input pattern {pattern}" + ) + + self._pattern = pattern + + def get_block(self, size: int) -> bytes: + """Get block filled with pattern. + + :param size: Size of block to return. + :return: Filled up block with specified pattern. + """ + if self._pattern == "zeros": + return bytes(size) + + if self._pattern == "ones": + return bytes(b"\xff" * size) + + if self._pattern == "rand": + return random_bytes(size) + + if self._pattern == "inc": + return bytes((x & 0xFF for x in range(size))) + + pattern = value_to_bytes(self._pattern, align_to_2n=False) + block = bytes(pattern * (int((size / len(pattern))) + 1)) + return block[:size] + + @property + def pattern(self) -> str: + """Get the pattern. + + :return: Pattern in string representation. + """ + try: + return hex(value_to_int(self._pattern)) + except SPSDKError: + return self._pattern + + +def align(number: int, alignment: int = 4) -> int: + """Align number (size or address) size to specified alignment, typically 4, 8 or 16 bytes boundary. + + :param number: input to be aligned + :param alignment: the boundary to align; typical value is power of 2 + :return: aligned number; result is always >= size (e.g. aligned up) + :raises SPSDKError: When there is wrong alignment + """ + if alignment <= 0 or number < 0: + raise SPSDKError("Wrong alignment") + + return (number + (alignment - 1)) // alignment * alignment + + +def align_block( + data: Union[bytes, bytearray], + alignment: int = 4, + padding: Optional[Union[int, str, BinaryPattern]] = None, +) -> bytes: + """Align binary data block length to specified boundary by adding padding bytes to the end. + + :param data: to be aligned + :param alignment: boundary alignment (typically 2, 4, 16, 64 or 256 boundary) + :param padding: byte to be added or BinaryPattern + :return: aligned block + :raises SPSDKError: When there is wrong alignment + """ + assert isinstance(data, (bytes, bytearray)) + + if alignment < 0: + raise SPSDKError("Wrong alignment") + current_size = len(data) + num_padding = align(current_size, alignment) - current_size + if not num_padding: + return bytes(data) + if not padding: + padding = BinaryPattern("zeros") + elif not isinstance(padding, BinaryPattern): + padding = BinaryPattern(str(padding)) + return bytes(data + padding.get_block(num_padding)) + + +def align_block_fill_random(data: bytes, alignment: int = 4) -> bytes: + """Same as `align_block`, just parameter `padding` is fixed to `-1` to fill with random data.""" + return align_block(data, alignment, BinaryPattern("rand")) + + +def extend_block(data: bytes, length: int, padding: int = 0) -> bytes: + """Add padding to the binary data block to extend the length to specified value. + + :param data: block to be extended + :param length: requested block length; the value must be >= current block length + :param padding: 8-bit value value to be used as a padding + :return: block extended with padding + :raises SPSDKError: When the length is incorrect + """ + current_len = len(data) + if length < current_len: + raise SPSDKError("Incorrect length") + num_padding = length - current_len + if not num_padding: + return data + return data + bytes([padding]) * num_padding + + +def find_first(iterable: Iterable[T], predicate: Callable[[T], bool]) -> Optional[T]: + """Find first element from the list, that matches the condition. + + :param iterable: list of elements + :param predicate: function for selection of the element + :return: found element; None if not found + """ + return next((a for a in iterable if predicate(a)), None) + + +def load_binary(path: str, search_paths: Optional[List[str]] = None) -> bytes: + """Loads binary file into bytes. + + :param path: Path to the file. + :param search_paths: List of paths where to search for the file, defaults to None + :return: content of the binary file as bytes + """ + path = find_file(path, search_paths=search_paths) + logger.debug(f"Loading binary file from {path}") + with open(path, "rb") as f: + return f.read() + + +def load_text(path: str, search_paths: Optional[List[str]] = None) -> str: + """Loads text file into string. + + :param path: Path to the file. + :param search_paths: List of paths where to search for the file, defaults to None + :return: content of the text file as string + """ + path = find_file(path, search_paths=search_paths) + logger.debug(f"Loading text file from {path}") + with open(path, "r") as f: + return f.read() + + +def write_file( + data: Union[str, bytes], path: str, mode: str = "w", encoding: Optional[str] = None +) -> int: + """Writes data into a file. + + :param data: data to write + :param path: Path to the file. + :param mode: writing mode, 'w' for text, 'wb' for binary data, defaults to 'w' + :param encoding: Encoding of written file ('ascii', 'utf-8'). + :return: number of written elements + """ + path = path.replace("\\", "/") + folder = os.path.dirname(path) + if folder and not os.path.exists(folder): + os.makedirs(folder, exist_ok=True) + + logger.debug(f"Storing {'binary' if 'b' in mode else 'text'} file at {path}") + with open(path, mode, encoding=encoding) as f: + return f.write(data) + + +def get_abs_path(file_path: str, base_dir: Optional[str] = None) -> str: + """Return a full path to the file. + + param base_dir: Base directory to create absolute path, if not specified the system CWD is used. + return: Absolute file path. + """ + if os.path.isabs(file_path): + return file_path.replace("\\", "/") + + return os.path.abspath(os.path.join(base_dir or os.getcwd(), file_path)).replace( + "\\", "/" + ) + + +def _find_path( + path: str, + check_func: Callable[[str], bool], + use_cwd: bool = True, + search_paths: Optional[List[str]] = None, + raise_exc: bool = True, +) -> str: + """Return a full path to the file. + + `search_paths` takes precedence over `CWD` if used (default) + + :param path: File name, part of file path or full path + :param use_cwd: Try current working directory to find the file, defaults to True + :param search_paths: List of paths where to search for the file, defaults to None + :param raise_exc: Raise exception if file is not found, defaults to True + :return: Full path to the file + :raises SPSDKError: File not found + """ + path = path.replace("\\", "/") + + if os.path.isabs(path): + if not check_func(path): + raise SPSDKError(f"Path '{path}' not found") + return path + if search_paths: + for dir_candidate in search_paths: + if not dir_candidate: + continue + dir_candidate = dir_candidate.replace("\\", "/") + path_candidate = get_abs_path(path, base_dir=dir_candidate) + if check_func(path_candidate): + return path_candidate + if use_cwd and check_func(path): + return get_abs_path(path) + # list all directories in error message + searched_in: List[str] = [] + if use_cwd: + searched_in.append(os.path.abspath(os.curdir)) + if search_paths: + searched_in.extend(filter(None, search_paths)) + searched_in = [s.replace("\\", "/") for s in searched_in] + err_str = f"Path '{path}' not found, Searched in: {', '.join(searched_in)}" + if not raise_exc: + logger.debug(err_str) + return "" + raise SPSDKError(err_str) + + +def find_dir( + dir_path: str, + use_cwd: bool = True, + search_paths: Optional[List[str]] = None, + raise_exc: bool = True, +) -> str: + """Return a full path to the directory. + + `search_paths` takes precedence over `CWD` if used (default) + + :param dir_path: Directory name, part of directory path or full path + :param use_cwd: Try current working directory to find the directory, defaults to True + :param search_paths: List of paths where to search for the directory, defaults to None + :param raise_exc: Raise exception if directory is not found, defaults to True + :return: Full path to the directory + :raises SPSDKError: File not found + """ + return _find_path( + path=dir_path, + check_func=os.path.isdir, + use_cwd=use_cwd, + search_paths=search_paths, + raise_exc=raise_exc, + ) + + +def find_file( + file_path: str, + use_cwd: bool = True, + search_paths: Optional[List[str]] = None, + raise_exc: bool = True, +) -> str: + """Return a full path to the file. + + `search_paths` takes precedence over `CWD` if used (default) + + :param file_path: File name, part of file path or full path + :param use_cwd: Try current working directory to find the file, defaults to True + :param search_paths: List of paths where to search for the file, defaults to None + :param raise_exc: Raise exception if file is not found, defaults to True + :return: Full path to the file + :raises SPSDKError: File not found + """ + return _find_path( + path=file_path, + check_func=os.path.isfile, + use_cwd=use_cwd, + search_paths=search_paths, + raise_exc=raise_exc, + ) + + +@contextlib.contextmanager +def use_working_directory(path: str) -> Iterator[None]: + # pylint: disable=missing-yield-doc + """Execute the block in given directory. + + Cd into specific directory. + Execute the block. + Change the directory back into the original one. + + :param path: the path, where the current directory will be changed to + """ + current_dir = os.getcwd() + try: + os.chdir(path) + yield + finally: + os.chdir(current_dir) + assert os.getcwd() == current_dir + + +def format_value( + value: int, size: int, delimiter: str = "_", use_prefix: bool = True +) -> str: + """Convert the 'value' into either BIN or HEX string, depending on 'size'. + + if 'size' is divisible by 8, function returns HEX, BIN otherwise + digits in result string are grouped by 4 using 'delimiter' (underscore) + """ + padding = size if size % 8 else (size // 8) * 2 + infix = "b" if size % 8 else "x" + sign = "-" if value < 0 else "" + parts = re.findall(".{1,4}", f"{abs(value):0{padding}{infix}}"[::-1]) + rev = delimiter.join(parts)[::-1] + prefix = f"0{infix}" if use_prefix else "" + return f"{sign}{prefix}{rev}" + + +def get_bytes_cnt_of_int( + value: int, align_to_2n: bool = True, byte_cnt: Optional[int] = None +) -> int: + """Returns count of bytes needed to store handled integer. + + :param value: Input integer value. + :param align_to_2n: The result will be aligned to standard sizes 1,2,4,8,12,16,20. + :param byte_cnt: The result count of bytes. + :raises SPSDKValueError: The integer input value doesn't fit into byte_cnt. + :return: Number of bytes needed to store integer. + """ + cnt = 0 + if value == 0: + return byte_cnt or 1 + + while value != 0: + value >>= 8 + cnt += 1 + + if align_to_2n and cnt > 2: + cnt = int(ceil(cnt / 4)) * 4 + + if byte_cnt and cnt > byte_cnt: + raise SPSDKValueError( + f"Value takes more bytes than required byte count {byte_cnt} after align." + ) + + cnt = byte_cnt or cnt + + return cnt + + +def value_to_int( + value: Union[bytes, bytearray, int, str], default: Optional[int] = None +) -> int: + """Function loads value from lot of formats to integer. + + :param value: Input value. + :param default: Default Value in case of invalid input. + :return: Value in Integer. + :raises SPSDKError: Unsupported input type. + """ + if isinstance(value, int): + return value + + if isinstance(value, (bytes, bytearray)): + return int.from_bytes(value, Endianness.BIG.value) + + if isinstance(value, str) and value != "": + match = re.match( + r"(?P0[box])?(?P[0-9a-f_]+)(?P[ul]{0,3})$", + value.strip().lower(), + ) + if match: + base = {"0b": 2, "0o": 8, "0": 10, "0x": 16, None: 10}[ + match.group("prefix") + ] + try: + return int(match.group("number"), base=base) + except ValueError: + pass + + if default is not None: + return default + raise SPSDKError(f"Invalid input number type({type(value)}) with value ({value})") + + +def value_to_bytes( + value: Union[bytes, bytearray, int, str], + align_to_2n: bool = True, + byte_cnt: Optional[int] = None, + endianness: Endianness = Endianness.BIG, +) -> bytes: + """Function loads value from lot of formats. + + :param value: Input value. + :param align_to_2n: When is set, the function aligns length of return array to 1,2,4,8,12 etc. + :param byte_cnt: The result count of bytes. + :param endianness: The result bytes endianness ['big', 'little']. + :return: Value in bytes. + """ + if isinstance(value, bytes): + return value + + if isinstance(value, bytearray): + return bytes(value) + + value = value_to_int(value) + return value.to_bytes( + get_bytes_cnt_of_int(value, align_to_2n, byte_cnt=byte_cnt), endianness.value + ) + + +def size_fmt(num: Union[float, int], use_kibibyte: bool = True) -> str: + """Size format.""" + base, suffix = [(1000.0, "B"), (1024.0, "iB")][use_kibibyte] + i = "B" + for i in ["B"] + [i + suffix for i in list("kMGTP")]: + if num < base: + break + num /= base + + return f"{int(num)} {i}" if i == "B" else f"{num:3.1f} {i}" + + +def swap16(x: int) -> int: + """Swap bytes in half word (16bit). + + :param x: Original number + :return: Number with swapped bytes + :raises SPSDKError: When incorrect number to be swapped is provided + """ + if x < 0 or x > 0xFFFF: + raise SPSDKError("Incorrect number to be swapped") + return ((x << 8) & 0xFF00) | ((x >> 8) & 0x00FF) + + +def get_hash(text: Union[str, bytes]) -> str: + """Returns hash of given text.""" + if isinstance(text, str): + text = text.encode("utf-8") + return hashlib.sha1(text).digest().hex()[:8] diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/spsdk_enum.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/spsdk_enum.py new file mode 100644 index 0000000..2983e8d --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/spsdk_enum.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2023 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Custom enum extension.""" +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, List, Optional, Union + +if TYPE_CHECKING: + from typing_extensions import Self + +from ..exceptions import SPSDKKeyError, SPSDKTypeError + + +@dataclass(frozen=True) +class SpsdkEnumMember: + """SPSDK Enum member.""" + + tag: int + label: str + description: Optional[str] = None + + +class SpsdkEnum(SpsdkEnumMember, Enum): + """SPSDK Enum type.""" + + def __eq__(self, __value: object) -> bool: + return self.tag == __value or self.label == __value + + def __hash__(self) -> int: + return hash((self.tag, self.label, self.description)) + + @classmethod + def labels(cls) -> List[str]: + """Get list of labels of all enum members. + + :return: List of all labels + """ + return [value.label for value in cls.__members__.values()] + + @classmethod + def tags(cls) -> List[int]: + """Get list of tags of all enum members. + + :return: List of all tags + """ + return [value.tag for value in cls.__members__.values()] + + @classmethod + def contains(cls, obj: Union[int, str]) -> bool: + """Check if given member with given tag/label exists in enum. + + :param obj: Label or tag of enum + :return: True if exists False otherwise + """ + if not isinstance(obj, (int, str)): + raise SPSDKTypeError("Object must be either string or integer") + try: + cls.from_attr(obj) + return True + except SPSDKKeyError: + return False + + @classmethod + def get_tag(cls, label: str) -> int: + """Get tag of enum member with given label. + + :param label: Label to be used for searching + :return: Tag of found enum member + """ + value = cls.from_label(label) + return value.tag + + @classmethod + def get_label(cls, tag: int) -> str: + """Get label of enum member with given tag. + + :param tag: Tag to be used for searching + :return: Label of found enum member + """ + value = cls.from_tag(tag) + return value.label + + @classmethod + def get_description(cls, tag: int, default: Optional[str] = None) -> Optional[str]: + """Get description of enum member with given tag. + + :param tag: Tag to be used for searching + :param default: Default value if member contains no description + :return: Description of found enum member + """ + value = cls.from_tag(tag) + return value.description or default + + @classmethod + def from_attr(cls, attribute: Union[int, str]) -> "Self": + """Get enum member with given tag/label attribute. + + :param attribute: Attribute value of enum member + :return: Found enum member + """ + # Let's make MyPy happy, see https://github.com/python/mypy/issues/10740 + if isinstance(attribute, int): + return cls.from_tag(attribute) + else: + return cls.from_label(attribute) + + @classmethod + def from_tag(cls, tag: int) -> "Self": + """Get enum member with given tag. + + :param tag: Tag to be used for searching + :raises SPSDKKeyError: If enum with given label is not found + :return: Found enum member + """ + for item in cls.__members__.values(): + if item.tag == tag: + return item + raise SPSDKKeyError( + f"There is no {cls.__name__} item in with tag {tag} defined" + ) + + @classmethod + def from_label(cls, label: str) -> "Self": + """Get enum member with given label. + + :param label: Label to be used for searching + :raises SPSDKKeyError: If enum with given label is not found + :return: Found enum member + """ + for item in cls.__members__.values(): + if item.label.upper() == label.upper(): + return item + raise SPSDKKeyError( + f"There is no {cls.__name__} item with label {label} defined" + ) + + +class SpsdkSoftEnum(SpsdkEnum): + """SPSDK Soft Enum type. + + It has API with default values for labels and + descriptions with defaults for non existing members. + """ + + @classmethod + def get_label(cls, tag: int) -> str: + """Get label of enum member with given tag. + + If member not found and default is specified, the default is returned. + + :param tag: Tag to be used for searching + :return: Label of found enum member + """ + try: + return super().get_label(tag) + except SPSDKKeyError: + return f"Unknown ({tag})" + + @classmethod + def get_description(cls, tag: int, default: Optional[str] = None) -> Optional[str]: + """Get description of enum member with given tag. + + :param tag: Tag to be used for searching + :param default: Default value if member contains no description + :return: Description of found enum member + """ + try: + return super().get_description(tag, default) + except SPSDKKeyError: + return f"Unknown ({tag})" diff --git a/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/usbfilter.py b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/usbfilter.py new file mode 100644 index 0000000..de89c97 --- /dev/null +++ b/src/nitrokey/trussed/_bootloader/lpc55_upload/utils/usbfilter.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# +# Copyright 2019-2024 NXP +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Module defining a USB filtering class.""" +import platform +import re +from typing import Any, Dict, Optional, Tuple + +from .misc import get_hash + + +class USBDeviceFilter: + """Generic USB Device Filtering class. + + Create a filtering instance. This instance holds the USB ID you are interested + in during USB HID device search and allows you to compare, whether + provided USB HID object is the one you are interested in. + The allowed format of `usb_id` string is following: + + vid or pid - vendor ID or product ID. String holding hex or dec number. + Hex number must be preceded by 0x or 0X. Number of characters after 0x is + 1 - 4. Mixed upper & lower case letters is allowed. e.g. "0xaB12", "0XAB12", + "0x1", "0x0001". + The decimal number is restricted only to have 1 - 5 digits, e.g. "65535" + It's allowed to set the USB filter ID to decimal number "99999", however, as + the USB VID number is four-byte hex number (max value is 65535), this will + lead to zero results. Leading zeros are not allowed e.g. 0001. This will + result as invalid match. + + The user may provide a single number as usb_id. In such a case the number + may represent either VID or PID. By default, the filter expects this number + to be a VID. In rare cases the user may want to filter based on PID. + Initialize the `search_by_pid` parameter to True in such cases. + + vid/pid - string of vendor ID & product ID separated by ':' or ',' + Same rules apply to the number format as in VID case, except, that the + string consists of two numbers separated by ':' or ','. It's not allowed + to mix hex and dec numbers, e.g. "0xab12:12345" is not allowed. + Valid vid/pid strings: + "0x12aB:0xabc", "1,99999" + + Windows specific: + instance ID - String in following format "HID\\VID_&PID_\\", + see instance ID in device manager under Windows OS. + + Linux specific: + USB device path - HID API returns path in following form: + '0003:0002:00' + + The first number represents the Bus, the second Device and the third interface. The Bus:Device + number is unique so interface is not necessary and Bus:Device should be sufficient. + + The Bus:Device can be observed using 'lsusb' command. The interface can be observed using + 'lsusb -t'. lsusb returns the Bus and Device as a 3-digit number. + It has been agreed, that the expected input is: + #, e.g. 3#11 + + Mac specific: + USB device path - HID API returns path in roughly following form: + 'IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC1@14/XHC1@14000000/HS01@14100000/SE + Blank RT Family @14100000/IOUSBHostInterface@0/AppleUserUSBHostHIDDevice' + + This path can be found using the 'ioreg' utility or using 'IO Hardware Registry Explorer' tool. + However, using the system report from 'About This MAC -> System Report -> USB' a partial path + can also be gathered. Using the name of USB device from the 'USB Device Tree' and appending + the 'Location ID' should work. The name can be 'SE Blank RT Family' and the 'Location ID' is + in form / , e.g. '0x14200000 / 18'. + So the 'usb_id' name should be 'SE Blank RT Family @14200000' and the filter should be able to + filter out such device. + """ + + def __init__( + self, + usb_id: Optional[str] = None, + search_by_pid: bool = False, + ): + """Initialize the USB Device Filtering. + + :param usb_id: usb_id string + :param search_by_pid: if true, expects usb_id to be a PID number, VID otherwise. + """ + self.usb_id = usb_id + self.search_by_pid = search_by_pid + + def compare(self, usb_device_object: Dict[str, Any]) -> bool: + """Compares the internal `usb_id` with provided `usb_device_object`. + + The provided USB ID during initialization may be VID or PID, VID/PID pair, + or a path. See private methods for details. + + :param usb_device_object: Libusbsio/HID_API device object (dictionary) + + :return: True on match, False otherwise + """ + # Determine, whether given device matches one of the expected criterion + if self.usb_id is None: + return True + + vendor_id = usb_device_object.get("vendor_id") + product_id = usb_device_object.get("product_id") + serial_number = usb_device_object.get("serial_number") + device_name = usb_device_object.get("device_name") + # the Libusbsio/HID_API holds the path as bytes, so we convert it to string + usb_path_raw = usb_device_object.get("path") + + if usb_path_raw: + if self.usb_id == get_hash(usb_path_raw): + return True + usb_path = self.convert_usb_path(usb_path_raw) + if self._is_path(usb_path=usb_path): + return True + + if self._is_vid_or_pid(vid=vendor_id, pid=product_id): + return True + + if vendor_id and product_id and self._is_vid_pid(vid=vendor_id, pid=product_id): + return True + + if serial_number and self.usb_id.casefold() == serial_number.casefold(): + return True + + if device_name and self.usb_id.casefold() == device_name.casefold(): + return True + + return False + + def _is_path(self, usb_path: str) -> bool: + """Compares the internal usb_id with provided path. + + If the path is a substring of the usb_id, this is considered as a match + and True is returned. + + :param usb_path: path to be compared with usd_id. + :return: true on a match, false otherwise. + """ + # we check the len of usb_id, because usb_id = "" is considered + # to be always in the string returning True, which is not expected + # behavior + # the provided usb string id fully matches the instance ID + usb_id = self.usb_id or "" + if usb_id.casefold() in usb_path.casefold() and len(usb_id) > 0: + return True + + return False + + def _is_vid_or_pid(self, vid: Optional[int], pid: Optional[int]) -> bool: + # match anything starting with 0x or 0X followed by 0-9 or a-f or + # match either 0 or decimal number not starting with zero + # this regex is the same for vid and pid => xid + xid_regex = "0[xX][0-9a-fA-F]{1,4}|0|[1-9][0-9]{0,4}" + usb_id = self.usb_id or "" + if re.fullmatch(xid_regex, usb_id) is not None: + # the string corresponds to the vid/pid specification, check a match + if self.search_by_pid and pid: + if int(usb_id, 0) == pid: + return True + elif vid: + if int(usb_id, 0) == vid: + return True + + return False + + def _is_vid_pid(self, vid: int, pid: int) -> bool: + """If usb_id corresponds to VID/PID pair, compares it with provided vid/pid. + + :param vid: vendor ID to compare. + :param pid: product ID to compare. + :return: true on a match, false otherwise. + """ + # match anything starting with 0x or 0X followed by 0-9 or a-f or + # match either 0 or decimal number not starting with zero + # Above pattern is combined to match a pair corresponding to vid/pid. + vid_pid_regex = "0[xX][0-9a-fA-F]{1,4}(,|:)0[xX][0-9a-fA-F]{1,4}|(0|[1-9][0-9]{0,4})(,|:)(0|[1-9][0-9]{0,4})" + usb_id = self.usb_id or "" + if re.fullmatch(vid_pid_regex, usb_id): + # the string corresponds to the vid/pid specification, check a match + vid_pid = re.split(":|,", usb_id) + if vid == int(vid_pid[0], 0) and pid == int(vid_pid[1], 0): + return True + + return False + + @staticmethod + def convert_usb_path(hid_api_usb_path: bytes) -> str: + """Converts the Libusbsio/HID_API path into string, which can be observed from OS. + + DESIGN REMARK: this function is not part of the USBLogicalDevice, as the + class intention is to be just a simple container. But to help the class + to get the required inputs, this helper method has been provided. Additionally, + this method relies on the fact that the provided path comes from the Libusbsio/HID_API. + This method will most probably fail or provide improper results in case + path from different USB API is provided. + + :param hid_api_usb_path: USB device path from Libusbsio/HID_API + :return: Libusbsio/HID_API path converted for given platform + """ + if platform.system() == "Windows": + device_manager_path = hid_api_usb_path.decode("utf-8").upper() + device_manager_path = device_manager_path.replace("#", "\\") + result = re.search(r"\\\\\?\\(.+?)\\{", device_manager_path) + if result: + device_manager_path = result.group(1) + + return device_manager_path + + if platform.system() == "Linux": + # we expect the path in form of #, Libusbsio/HID_API returns + # :: + linux_path = hid_api_usb_path.decode("utf-8") + linux_path_parts = linux_path.split(":") + + if len(linux_path_parts) > 1: + linux_path = str.format( + "{}#{}", int(linux_path_parts[0], 16), int(linux_path_parts[1], 16) + ) + + return linux_path + + if platform.system() == "Darwin": + return hid_api_usb_path.decode("utf-8") + + return "" + + +class NXPUSBDeviceFilter(USBDeviceFilter): + """NXP Device Filtering class. + + Extension of the generic USB device filter class to support filtering + based on NXP devices. Modifies the way, how single number is handled. + By default, if single value is provided, it's content is expected to be VID. + However, legacy tooling were expecting PID, so from this perspective if + a single number is provided, we expect that VID is out of range NXP_VIDS. + """ + + NXP_VIDS = [0x1FC9, 0x15A2, 0x0471, 0x0D28] + + def __init__( + self, + usb_id: Optional[str] = None, + nxp_device_names: Optional[Dict[str, Tuple[int, int]]] = None, + ): + """Initialize the USB Device Filtering. + + :param usb_id: usb_id string + :param nxp_device_names: Dictionary holding NXP device vid/pid {"device_name": [vid(int), pid(int)]} + """ + super().__init__(usb_id=usb_id, search_by_pid=True) + self.nxp_device_names = nxp_device_names or {} + + def compare(self, usb_device_object: Any) -> bool: + """Compares the internal `usb_id` with provided `usb_device_object`. + + Extends the comparison by USB names - dictionary of device name and + corresponding VID/PID. + + :param usb_device_object: lpcusbsio USB HID device object + + :return: True on match, False otherwise + """ + vendor_id = usb_device_object["vendor_id"] + product_id = usb_device_object["product_id"] + + if self.usb_id: + if super().compare(usb_device_object=usb_device_object): + return True + + return self._is_nxp_device_name(vendor_id, product_id) + + return self._is_nxp_device(vendor_id) + + def _is_vid_or_pid(self, vid: Optional[int], pid: Optional[int]) -> bool: + if vid and vid in NXPUSBDeviceFilter.NXP_VIDS: + return super()._is_vid_or_pid(vid, pid) + + return False + + def _is_nxp_device_name(self, vid: int, pid: int) -> bool: + nxp_device_name_to_compare = { + k.lower(): v for k, v in self.nxp_device_names.items() + } + assert isinstance(self.usb_id, str) + if self.usb_id.lower() in nxp_device_name_to_compare: + vendor_id, product_id = nxp_device_name_to_compare[self.usb_id.lower()] + if vendor_id == vid and product_id == pid: + return True + return False + + @staticmethod + def _is_nxp_device(vid: int) -> bool: + return vid in NXPUSBDeviceFilter.NXP_VIDS diff --git a/src/nitrokey/trussed/_utils.py b/src/nitrokey/trussed/_utils.py index c32bf9d..b0acf45 100644 --- a/src/nitrokey/trussed/_utils.py +++ b/src/nitrokey/trussed/_utils.py @@ -10,8 +10,6 @@ from functools import total_ordering from typing import Optional, Sequence -from spsdk.sbfile.misc import BcdVersion3 - @dataclass(order=True, frozen=True) class Uuid: @@ -226,10 +224,6 @@ def from_v_str(cls, s: str) -> "Version": raise ValueError(f"Missing v prefix for firmware version: {s}") return Version.from_str(s[1:]) - @classmethod - def from_bcd_version(cls, version: BcdVersion3) -> "Version": - return cls(major=version.major, minor=version.minor, patch=version.service) - @dataclass class Fido2Certs: diff --git a/stubs/crcmod/__init__.pyi b/stubs/crcmod/__init__.pyi new file mode 100644 index 0000000..e69de29 diff --git a/stubs/crcmod/predefined.pyi b/stubs/crcmod/predefined.pyi new file mode 100644 index 0000000..7efc9dc --- /dev/null +++ b/stubs/crcmod/predefined.pyi @@ -0,0 +1,3 @@ +from typing import Callable + +def mkPredefinedCrcFun(crc_name: str) -> Callable[[bytes, int], int]: ...