SiLabs_SYMCRYPTO_1: fix SHA-256 multi-step hashing - #2
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9530010781
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "Codex (@codex) review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".
| // descriptor on the final op ({0x80, zeros, bit-length}): feeding it to | ||
| // BouncyCastle and then calling DoFinal pads TWICE. | ||
| var data = fetcherDescriptorList[inputIndex].Data; | ||
| var isPadding = hashFinal && data.Length > 0 && data[0] == 0x80; |
There was a problem hiding this comment.
Do not treat every 0x80-leading final descriptor as padding
When a final hash operation contains a legitimate Message descriptor whose payload starts with 0x80, this condition drops the entire descriptor. For example, a one-shot SHA-256 operation over the single byte 0x80 has no separate software-padding descriptor, yet the model now computes the digest of an empty message. The padding descriptor must be identified using its full structure and operation context rather than only its first byte.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Fixes the SiLabs SYMCRYPTO Hash engine model’s SHA-256 multi-step hashing behavior to match the SDK driver’s expected descriptor semantics and avoid corrupting multi-step digests in emulation.
Changes:
- Filter SHA input so only
CryptoDataType.Messagedescriptors are fed into the digest (skippingInitializationData). - Skip the SDK-provided explicit SHA padding descriptor on final operations to avoid double-padding when
DoFinalis called.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| var data = fetcherDescriptorList[inputIndex].Data; | ||
| var isPadding = hashFinal && data.Length > 0 && data[0] == 0x80; | ||
| if(fetcherDescriptorList[inputIndex].IsData | ||
| && fetcherDescriptorList[inputIndex].DataType == CryptoDataType.Message | ||
| && !isPadding) |
RunHashEngine mishandled the descriptors it feeds to the digest in three
independent ways. Each is verified byte-exact against python hashlib using a
firmware probe built from Simplicity SDK 2025.12.2 for SIMG301, running the real
SDK hash chain (mbedtls -> sli_hostcrypto_transparent_* -> sx_hash -> SYMCRYPTO).
1. InitializationData descriptors were hashed.
The update loop fed every IsData descriptor into the digest. On a resumed
(multi-step) operation the driver passes an InitializationData descriptor
holding the model's own non-final writeback -- 32 bytes of 0xF4 -- so every
multi-step digest was polluted by model scratch data. The comment above the
loop already said these "can be ignored", and the HMAC branch in the same
function already filters on DataType == Message; only this loop did not.
2. The explicit software-padding block was hashed, then padded again.
In software-padding mode the driver appends the SHA padding block
({0x80, zeros, bit-length}) as a trailing Message descriptor. The model fed it
to BouncyCastle and then called DoFinal, which pads a second time.
The padding descriptor is now identified by reconstructing the expected block
for the real bytes seen so far and comparing it byte-for-byte against the last
Message descriptor. An earlier revision of this patch tested only whether a
descriptor's first byte was 0x80; that also matches legitimate message data,
and a one-shot hash of the single byte 0x80 was consequently digested as the
empty string. Thanks to the PR reviewers for catching that.
Hardware-padding ops (Padding=1) carry no explicit padding descriptor and are
excluded outright. Reconstruction covers every mode the engine supports; for a
mode whose rule is not modelled the helper returns null and nothing is skipped,
which is the pre-existing behaviour -- so a reconstruction that is wrong for
some mode cannot corrupt a digest that is correct today, it can only fail to
match.
3. InvalidBytesOrBits was ignored.
The fetcher transfers whole words; InvalidBytesOrBits counts the trailing bytes
of the transfer that are alignment padding rather than message. A one-byte
message arrives as a 4-byte descriptor with InvalidBytesOrBits=3 and was hashed
as 4 bytes, yielding sha256(80 00 00 00) instead of sha256(80). The AES key
path and the CMAC payload path already account for this, and the HMAC branch
subtracts it unconditionally for message data; the hash path did not.
Verification -- all six probe cases now equal the python hashlib reference,
including two regression cases for the false-positive described in defect 2:
multi100 (updates 64+36) 5a2cda23...
single60 (one update, control) d13c823c...
three100 (updates 40+40+20) 5a2cda23...
c200 (updates 100+100) 5662cd43...
one80 (one-shot, message = {0x80}) 76be8b52...
midlead80 (updates 20+20, second chunk starts with 0x80)
ebe671bd...
Impact: any firmware performing multi-step PSA hashing on the SYMCRYPTO engine
computed wrong digests in emulation. Observed on Matter PASE key derivation on an
EFR32xG31, where commissioning failed at Pake2 MAC verification on the
commissioner while every device-side getter returned success; with these fixes the
same image reaches "PASE establishment successful".
9530010 to
0ccb8e3
Compare
|
Hi, this is now fixed in our latest 26q3 branch. Please take a look, thank you |
Summary
Two defects in
RunHashEngine's SHA-256 path corrupted every multi-step digest computed by the model:IsDatadescriptor into the digest; the HMAC branch in the same function already filters onDataType == CryptoDataType.Message, and the loop's own comment says InitializationData "can be ignored". The hashed bytes are the model's own non-final writeback (0xF4).{0x80, zeros, bit-length BE}) as a Message descriptor on the final hash op. The model feeds it to BouncyCastle and then callsDoFinal, which pads again.Step by step: how a 100-byte multi-step hash goes wrong
Reproduced on a symbolized firmware built from Simplicity SDK 2025.12.2 running the real SDK hash chain (mbedtls -> sli_hostcrypto -> sx_hash -> SYMCRYPTO) with a deterministic 100-byte input.
What the firmware wants to compute:
Reference:
sha256(msg100) = 5a2cda2351d1cdd9dd7957e57c0b3c8522451f25b6494569b7e94388c46f0980How the SDK driver maps that to SYMCRYPTO operations. The first update becomes one engine op (64 message bytes, no final): the model hashes them into its internal BouncyCastle engine, keeps the engine state, and writes its own intermediate state -- 32 bytes of
0xF4-- back into a buffer the driver owns. The second update plus finish becomes another op carrying three descriptors:InitializationData-> the 32-byte buffer, now full of0xF4Message-> the real 36 bytesMessage-> the 28-byte SHA padding block80 00 00 ... 00 00 03 20(0x80marker, zeros, bit-length 800 =0x320)What the model actually feeds into the digest engine. The loop checks only
IsData, so all three descriptors are hashed, in order:Then, because
hashFinalis set, it callsDoFinal-- and BouncyCastle appends its own RFC padding (computed for the fed length of 160 bytes) and finalizes. So the model computes:instead of
sha256(real100). Byte-exact verification:Why single-step hashing was never wrong. A one-shot update+finish has nothing to resume, so the driver sends no
InitializationDatadescriptor -- and in that flow it sends no explicit padding block either. The model hashes the message,DoFinalpads once, and the result is correct on the unmodified model. That is why this defect only surfaces for multi-step hashing.What each fix removes. With only the descriptor-type filter, the
0xF4*32is no longer hashed, but the 28-byte padding block still is --sha256(msg100 + pad28)-- verified byte-exact against the model's observed digest (20b7d04c...), still wrong. With both fixes (also skipping0x80-leading Message descriptors athashFinal), the engine sees only the 100 real bytes,DoFinalpads once, and the digest equals the hashlib reference (5a2cda23...) exactly. All three multi-step probe cases converge; the single-step control stays correct on every variant.Impact
Any firmware doing multi-step PSA hashing on the SYMCRYPTO engine computed wrong digests in emulation. Observed in the wild: Matter PASE key derivation on an EFR32xG31 device fails Pake2 MAC verification on the commissioner while every device-side getter returns success.
Notes
0x80first byte athashFinal(the SHA padding marker). A last-descriptor-based rule was considered and rejected: single-step ops carry no padding descriptor, and skipping their last message would drop real data.InvalidBytesOrBitsfrom message lengths (the HMAC branch does). The SDK probe observedInvalid=0on all SHA message descriptors, so no divergence was produced, but it is a latent asymmetry worth checking against the SYMCRYPTO IP specification.