Add hmac module: CPython-compatible HMAC backed by PSA Crypto - #11341
Add hmac module: CPython-compatible HMAC backed by PSA Crypto#11341mmabey wants to merge 2 commits into
Conversation
New shared-bindings/shared-module module `hmac`, mirroring the CPython `hmac` API and the three-layer split already used by `hashlib` (bindings + shared-module on the PSA Crypto interface, no common-hal). - hmac.new(key, msg=b"", digestmod) -> HMAC - hmac.digest(key, msg, digest) -> bytes (one-shot) - hmac.compare_digest(a, b) -> bool (constant time) - HMAC: update, digest, hexdigest, copy, digest_size, block_size, name `digestmod` accepts "sha256" or "sha1". The key is a bytes-like object; it is kept as an owned copy and imported into PSA as a volatile PSA_KEY_TYPE_HMAC key for each digest() call, then destroyed -- so there is no finaliser and no PSA key-slot leak. The message fed via update() is buffered so digest() is a single psa_mac_compute(), which keeps copy(), repeated digest(), and update()-after-digest() all CPython-compatible (PSA has no psa_mac_clone()). Build flag CIRCUITPY_HMAC, enabled where a full PSA crypto build with HMAC is already present (espressif's ESP-IDF mbedtls, SSL builds). The CIRCUITPY_HASHLIB_MBEDTLS_ONLY subset does not yet ship the PSA MAC driver, so those ports (and zephyr-cp) are left for a follow-up. Verified on an ESP32-S3-DevKitC-1-N8R8 against RFC 4231 / RFC 2202 vectors and host openssl.
tannewt
left a comment
There was a problem hiding this comment.
Thanks! I'd prefer to not support copy() in favor of streaming digest. That will remove allocations that happen as update() is called. Just have copy() raise not implemented error.
You'll need to switch to the PSA crypto's multipart API. The hash object should be able to store the multipart state.
|
Thanks for the review! Digging into the multipart API, I ran into a design snag I want to flag before picking a direction. PSA's multipart MAC API ( That conflicts with PSA does have an equivalent for plain hashes, Three ways I can see to resolve this. I wanted your take before implementing one:
Let me know which direction you'd prefer, or if I'm missing something about the multipart API that resolves this more cleanly. |
Addresses tannewt's review comment on PR adafruit#11341: the hand-rolled constant-time loop was fine in source but not guaranteed constant-time after compiler optimization. Switch to mbedtls_ct_memcmp(), which uses volatile accesses (and assembly on some platforms) specifically to defeat that. It takes a single length, so the existing a_len/b_len mismatch handling (compare a against itself, force a mismatch) stays in place around it. Verified this compiles and links on espressif_esp32s3_devkitc_1_n8r8.
Adds an
hmacmodule: keyed-hash message authentication (HMAC), following the CPythonhmacAPI and backed by PSA Crypto.This is a small, self-contained piece split out of the discussion on #11319. In that review @tannewt suggested that HMAC should go through a CPython-standard
hmacmodule rather than living on the key object. This PR is that module. It is useful on its own withbyteskeys; #11319 will be reworked so that its hardware-held key object is accepted byhmac.new()in place ofbytes(a small, additive change on top of this).What's added
digestmodaccepts"sha256"or"sha1". (CPython also accepts a hashlib constructor or module; those are not supported here — a name string only.)keyis anybytes-like object.hmac.HMACis exposed forisinstancechecks.Implementation notes
hashlib:shared-bindings/hmac/+shared-module/hmac/, with nocommon-hal— every operation ispsa_mac_compute()/ a PSA key import, so any port with a PSA Crypto backend gets it unchanged.PSA_KEY_TYPE_HMACkey for the duration of eachdigest()call, then destroyed. There is no finaliser and no way to leak a PSA key slot.update()are buffered, sodigest()is a single one-shotpsa_mac_compute(). That keepscopy(), callingdigest()more than once, andupdate()afterdigest()all behaving like CPython (PSA Crypto has nopsa_mac_clone()for a streaming MAC operation).compare_digest()mirrors CPython's_tscmp: the work done depends only onlen(a), and a length mismatch still runs the loop before returningFalse.Build scope
New
CIRCUITPY_HMACflag, on by default wherever a full PSA Crypto build that already includes HMAC is present — i.e.CIRCUITPY_HASHLIB_MBEDTLSand notCIRCUITPY_HASHLIB_MBEDTLS_ONLY. That covers espressif (ESP-IDF's mbedtls) and SSL builds.The
CIRCUITPY_HASHLIB_MBEDTLS_ONLYsubset (e.g. nordic) does not currently ship the PSA MAC driver, andports/zephyr-cpwould needCONFIG_PSA_WANT_ALG_HMAC. Enabling those is left for a follow-up so this PR stays small.Testing
tests/circuitpython/hmac.py— RFC 4231 (SHA-256) and RFC 2202 (SHA-1) test vectors, plus empty message, block-size key reduction, incremental vs. one-shot,copy()independence,update()afterdigest(),digest()/hexdigest()agreement,hmac.digest(), metadata, an unsupported-algorithmValueError, andcompare_digestcases (equal / unequal / different length /bytesvsbytearrayvsmemoryview). The module isn't in theunixcoverage variant, so this testSKIPs underrun-tests; it runs on any build that has the module.hmac/openssl dgst -sha256 -mac HMAC. As a cross-check,hmac.new()with the raw bytes of a key that was burned into an eFuseHMAC_UPblock produces exactly the MAC that the ESP32-S3 HMAC peripheral produces for the same key in Add securekey module for hardware-held cryptographic keys #11319 — so the two approaches interoperate.pre-commitpasses.Docs
API docs are generated from the
//|docstrings; the module appears under the shared-bindings reference automatically.