Skip to content

SiLabs_SYMCRYPTO_1: fix SHA-256 multi-step hashing - #2

Draft
Junming Chen (Chapoly1305) wants to merge 1 commit into
SiliconLabsSoftware:silabs/release/26q1from
Chapoly1305:fix/symcrypto-sha256-descriptor-and-padding
Draft

SiLabs_SYMCRYPTO_1: fix SHA-256 multi-step hashing#2
Junming Chen (Chapoly1305) wants to merge 1 commit into
SiliconLabsSoftware:silabs/release/26q1from
Chapoly1305:fix/symcrypto-sha256-descriptor-and-padding

Conversation

@Chapoly1305

@Chapoly1305 Junming Chen (Chapoly1305) commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Two defects in RunHashEngine's SHA-256 path corrupted every multi-step digest computed by the model:

  1. InitializationData descriptors are hashed as message data. The update loop feeds every IsData descriptor into the digest; the HMAC branch in the same function already filters on DataType == 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).
  2. Explicit SHA padding is double-hashed. The SDK driver sends the padding block ({0x80, zeros, bit-length BE}) as a Message descriptor on the final hash op. The model feeds it to BouncyCastle and then calls DoFinal, 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:

mbedtls_sha256_starts()
mbedtls_sha256_update(first 64 bytes)
mbedtls_sha256_update(last 36 bytes)
mbedtls_sha256_finish()

Reference: sha256(msg100) = 5a2cda2351d1cdd9dd7957e57c0b3c8522451f25b6494569b7e94388c46f0980

How 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 of 0xF4
  • Message -> the real 36 bytes
  • Message -> the 28-byte SHA padding block 80 00 00 ... 00 00 03 20 (0x80 marker, 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:

real 64 bytes
+ 0xF4 * 32        <- the model's own writeback, hashed as message data
+ real 36 bytes
+ the 28-byte explicit padding block

Then, because hashFinal is set, it calls DoFinal -- and BouncyCastle appends its own RFC padding (computed for the fed length of 160 bytes) and finalizes. So the model computes:

sha256( real64 + 0xF4*32 + real36 + explicit-pad28 + BouncyCastle's implicit padding )

instead of sha256(real100). Byte-exact verification:

hashlib.sha256(msg64 + b'\xF4'*32 + msg36 + pad28).hexdigest()
# = 07b8fe7fdbc8d15c0f193292c0725449346d3fb5d0ee730c35efce9391250d83
# = the stock model's observed digest, exactly

Why single-step hashing was never wrong. A one-shot update+finish has nothing to resume, so the driver sends no InitializationData descriptor -- and in that flow it sends no explicit padding block either. The model hashes the message, DoFinal pads 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*32 is 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 skipping 0x80-leading Message descriptors at hashFinal), the engine sees only the 100 real bytes, DoFinal pads 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

  • The fix only touches the SHA-256 path; AES/GCM/HMAC branches are untouched.
  • The padding detection uses the 0x80 first byte at hashFinal (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.
  • Not addressed here: the SHA path does not subtract InvalidBytesOrBits from message lengths (the HMAC branch does). The SDK probe observed Invalid=0 on all SHA message descriptors, so no divergence was produced, but it is a latent asymmetry worth checking against the SYMCRYPTO IP specification.

@Chapoly1305 Junming Chen (Chapoly1305) changed the title SiLabs_SYMCRYPTO_1: fix SHA-256 multi-step hashing (descriptor filter + double padding) SiLabs_SYMCRYPTO_1: fix SHA-256 multi-step hashing Aug 19, 2026
@Chapoly1305
Junming Chen (Chapoly1305) marked this pull request as ready for review August 19, 2026 20:08
Copilot AI lite review requested due to automatic review settings August 19, 2026 20:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.Message descriptors are fed into the digest (skipping InitializationData).
  • Skip the SDK-provided explicit SHA padding descriptor on final operations to avoid double-padding when DoFinal is called.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +1066 to +1070
var data = fetcherDescriptorList[inputIndex].Data;
var isPadding = hashFinal && data.Length > 0 && data[0] == 0x80;
if(fetcherDescriptorList[inputIndex].IsData
&& fetcherDescriptorList[inputIndex].DataType == CryptoDataType.Message
&& !isPadding)
@Chapoly1305
Junming Chen (Chapoly1305) marked this pull request as draft August 19, 2026 20:20
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".
@Chapoly1305
Junming Chen (Chapoly1305) force-pushed the fix/symcrypto-sha256-descriptor-and-padding branch from 9530010 to 0ccb8e3 Compare August 19, 2026 20:56
@mananni-silabs

Copy link
Copy Markdown
Collaborator

Hi, this is now fixed in our latest 26q3 branch. Please take a look, thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants