Add securekey module for hardware-held cryptographic keys - #11319
Conversation
dhalbert
left a comment
There was a problem hiding this comment.
Thanks for working on this.
Given our effort to be more port-agnostic, do you think you could come up with a more general API that would cover cryptography peripherals on other chips as well, and also eventually cover the other functionality on espressif? Then we would have more portable API's.
What is your use case for this functionality? Is it for Matter?
As we are moving toward Zephyr in the long run, looking at its API's may be helpful.
There is cryptography, the CPython library, but it is quite complicated, and the hardware-specific parts of it may be buried.
Reworks PR adafruit#11319 per maintainer review (dhalbert): make the eFuse-bound HMAC API port-agnostic instead of an espressif-only module. - Remove esphmac / esphmac.HMACKey entirely (nothing merged depends on it). - Add securekey.HardwareKey in shared-bindings + shared-module: HardwareKey(key_slot) key_slot is port-defined .hmac_sha256(data) -> bytes PSA psa_mac_compute .verify_hmac_sha256(data, mac) PSA psa_mac_verify, constant time (new) .key_slot, .exportable (exportable = old read_protected, inverted) - Split follows os/hashlib: shared-module owns the PSA operations on a stored psa_key_id_t; the only per-port file is common-hal construct(), which maps the key slot to a psa_key_id_t. A second port (Zephyr's PSA build included) only needs that shim. - espressif construct() imports an esp_hmac_opaque_key_t reference through ESP-IDF's vendored PSA opaque-key driver (built for every SOC_HMAC_SUPPORTED chip), caching one import per eFuse block so repeated construction does not leak PSA key slots. Still fail-closed on the HMAC_UP eFuse purpose; still no raw-key read and no burn/write. - CIRCUITPY_SECUREKEY: default off, on for espressif HMAC-capable chips (off for esp32 / esp32c2 / esp32c61). Drop the CIRCUITPY_ESPHMAC wiring and the esphmac SRC block in ports/espressif/Makefile. - Update locale/circuitpython.pot.
My pleasure! CircuitPython has given me a lot of joy over the years, so I'm always happy when a chance arises to contribute back.
No, I'm helping build a medical device that needs to compute an HMAC using a key that we obviously don't want to be readable. So, being able to use the ESP32-S3's own eFuse-bound HMAC key instead of a second chip is a real BOM win.
@dhalbert I really like this idea, thank you for pushing back. Since ESP-IDF ships a PSA Crypto opaque-key driver for eFuse HMAC keys, the eFuse block shows up as a normal I've reworked the approach and put an example below. The working name is import securekey
# Handle to a key held in hardware. The argument that selects WHICH key is
# port-defined (same pattern as board pins): on espressif it is the eFuse
# key block index; other ports would use their own key-slot identifier.
key = securekey.HardwareKey(0)
key.hmac_sha256(b"message") -> bytes # -> psa_mac_compute
key.verify_hmac_sha256(b"msg", mac) -> bool # -> psa_mac_verify (constant time)
key.exportable -> bool # informational (RD_DIS on espressif)Operations live in |
|
@mmabey Mike, this is really cool. I had never used this function before or really looked at the efuse stuff at all. I flashed/burned a random 32byte key (saved locally as hmac_key.bin) to BLOCK5 on a QtPy ESP32-S3 NOPSRAM and signed "Hello World" on my Mac and then ran a few commands shown below using the built CircuitPython artifact with your changes and everything works great. Bob CircuitPython Test Output Edit: Added output from off by one bit incorrect digest showing False for verify Validation on Mac OSX hmac_key.bin contains the key that was burned into the efuse BLOCK5 |
securekey exposes keys that live in a hardware key store -- eFuse, a key
manager, a secure element -- and can be used but never read back. Python
code can compute or verify an HMAC-SHA256 with the key; there is no API to
read the raw key bytes and no API to write or burn keys. Provisioning is a
manufacturing-time step done with vendor tools (e.g. espefuse.py).
The module is portable, split across the usual three layers:
- shared-bindings/securekey/ -- portable arg parsing + docstrings
- shared-module/securekey/ -- HardwareKey object and the
psa_mac_compute / psa_mac_verify operations, port-independent
- ports/espressif/common-hal/securekey/ -- only construct(): validates
the eFuse key block and imports a PSA opaque-key reference
securekey.HardwareKey(key_slot) takes a port-defined identifier. On
espressif it is the eFuse key block index 0-5; the block must be burned
with purpose HMAC_UP or construction fails (fail-closed), so a HardwareKey
can never be pointed at a block reserved for flash encryption, secure
boot, or the Digital Signature peripheral. Methods: hmac_sha256(data),
verify_hmac_sha256(data, mac) (constant-time). Properties: key_slot,
exportable (informational; False once RD_DIS is burned).
Build wiring: CIRCUITPY_SECUREKEY, default 0, enabled on espressif for
all chips with the HMAC peripheral (off for esp32 / esp32c2 / esp32c61,
which lack it).
ca84d5f to
6a2e0ab
Compare
|
@grgrant That is very reassuring to know you were also able to confirm this works on actual hardware. Thank you! And thanks for being willing to permanently give up one of your key slots! |
|
Was happy to dedicate a slot. Let me know if/when any changes should be retested. I would really like to have more crypto available -- especially as I use things like RFM95.
|
|
@dhalbert Dan, have you had a chance to look at the new structure of my approach? Do you have any additional changes you want to see me make before this is ready to merge? Over the weekend, I took the liberty of implementing the remainder of the security features requested in #3341 that I planned to put in a fast-follow PR once this was merged in. I don't know what your preference is between smaller, more incremental PRs and larger PRs with more related code packed in. So, if it would be easier to review all of that work at once, just let me know. |
|
Ok, I've taken a look at this and think we should keep the idea of the Once created, the HardwareKey object implementation should use psa_crypto only so it is port-agnostic. This can be done by placing names and Let's rename the module to You could allow export by implementing buffer conversion to get a bytes object out but I doubt it is worth it. HMAC should be done through the CPython standard For any other crypto extensions we should create CPython cryptography library subsets that are backed by PSA Crypto. (HKDF and AESCCM would be useful for CircuitMatter) |
Summary
Adds
esphmac, a new Espressif-port-only module exposing the ESP32-S3's on-chip HMACperipheral as a compute-only primitive:
esphmac.HMACKey(key_block)binds to one of thesix eFuse key blocks (
BLOCK_KEY0-BLOCK_KEY5), and its only operations arehmac(data) -> bytesand a read-onlyread_protectedstatus check.This is related to #3341 (Support the ESP32-S2's Digital Signature
Peripheral), but deliberately scoped to HMAC only, not the Digital Signature / RSA-signing
peripheral - that's a separate, larger surface this PR doesn't attempt.
Why
Once an eFuse key block's
RD_DISbit is set (whichespefuse burn-keydoes by default),the eFuse controller enforces in hardware that the raw key can never be read back by any
software, regardless of API design. The HMAC peripheral has an internal hardware path to
the key that bypasses that block-out, so it can still compute a correct HMAC-SHA256 using
a key that is otherwise permanently unreadable. This gives the chip a "use the secret,
never expose it" capability similar to what a discrete secure element (e.g. an ATSHA204A)
provides, without needing extra hardware.
This module is intentionally compute-only:
eFuse hardware itself once
RD_DISis set, not by this module's design.espefuse.pytoolat manufacturing time, which already supports burning a key with the
HMAC_UPpurpose.Adding eFuse write access from CircuitPython is a materially different, higher-risk
change than this PR, and is not included here.
Design notes
HMACKey.__init__fails closed: it checks the target block's actual eFuse purpose viaesp_efuse_get_key_purpose()and raisesValueErrorunless it's alreadyHMAC_UP.This matters because
esp_hmac_calculate()itself does not validate the purpose ofthe key block it's given - without this check,
HMACKeycould be pointed at a blockreserved for flash encryption, secure boot, JTAG re-enable, or the DS peripheral by
mistake.
read_protectedreports whetherRD_DISis actually set on the bound block. It'sinformational only and does not gate whether
hmac()can be called, sinceespefusealready sets
RD_DISby default when burning anHMAC_UPkey - this is meant formanufacturing-time self-test code to confirm a key block was burned as expected.
SOC_HMAC_SUPPORTED(all except theoriginal ESP32, which predates this peripheral).
esp_security(which providesesp_hmac_calculate()) is already an unconditional component in this port'sCMakeLists.txt, and is already a hard dependency ofmbedtls, which the port needsfor TLS regardless.
Testing
Built and flash-tested on a real ESP32-S3-DevKitC-1-N8R8
(
espressif_esp32s3_devkitc_1_n8r8board target):to the board.
BLOCK_KEY5withpurpose
HMAC_UP, using the stockespefuse burn-key BLOCK_KEY5 <keyfile> HMAC_UP(default read-protect left enabled, i.e.
RD_DISset).hmac.new(key_bytes, b"test message", hashlib.sha256).hexdigest()using Python's standard libraryhmacmodule and the samekey bytes used in the burn.
This confirms the full path end-to-end: the eFuse-bound key is not independently readable
(
read_protectedisTrue), yethmac()still produces a correct, standardFIPS-198/RFC-2104 HMAC-SHA256 result using it.
No automated test suite entries were added. Sibling Espressif-only modules with a similar
hardware dependency (
espnow,espulp,espcamera) have none either, sincetests/runsagainst the
unixport and this functionality is inherently dependent on a real, manuallypre-burned eFuse key block.
Also verified as part of this PR:
tools/codeformat.py -c(Uncrustify) passes with no reformatting needed.make check-translatepasses (locale/circuitpython.potupdated with the three newMP_ERROR_TEXT()strings this adds).pre-commit runpasses.tools/extract_pyi.pyextracts a clean.pyistub from the module's inlinedocumentation with no errors, confirming the docs build cleanly.