Skip to content

Verify GBLv4 content hashes, memory sections and the manifest signature - #5

Open
yarrrly wants to merge 1 commit into
zigpy:devfrom
yarrrly:gbl4-verification
Open

Verify GBLv4 content hashes, memory sections and the manifest signature#5
yarrrly wants to merge 1 commit into
zigpy:devfrom
yarrrly:gbl4-verification

Conversation

@yarrrly

@yarrrly yarrrly commented Sep 9, 2026

Copy link
Copy Markdown

Implements the API sketched in #4.

What the manifest binds, and where

Checked against public files, since the format page does not give the offsets:

  • CONTENT_HASH is SHA-256 over everything after the MANIFEST tag.
  • UpdateMemorySection.hash is SHA-256 over the whole MEMORY_SECTION_INFO tag, its 8 byte header included.
  • UpdateMemorySection.memory_section_position is a file offset of the MEMORY_SECTION tag counted from the start of the image, so it includes the root tag's own header. UpdateSe.tlv_position is not the same kind of pointer: it lands on the SE blob's payload, 8 bytes further in. Both hold on the three Hue files that have an SE blob.
  • MemorySectionInfo.final_image_hash[:32] is SHA-256 of the expanded image and plain_image_size is its length. The other 32 bytes are zero in every file I have.
  • The manifest signature (hash_type 2) is ECDSA P-256 over SHA-256 of the manifest children that follow the SIGNATURE tag: manifest info, bundle version, content hash and update process, headers included. The certificate sits before the signature and is outside the signed range. I found that range by trying; five other plausible ranges fail on the same files. If the series 3 parser in the SDK contradicts this, I would rather match it than keep my reading.

API

image.verify_content_hash()     # the payload after the manifest
image.verify_memory_sections()  # each section against the update that names it
image.verify_signature()        # or verify_signature(public_key)
image.plain_image(section)      # decompress one section
image.memory_section_at(position)
image.root_child_offsets()
image.content(), image.signing_content(), image.signing_digest()

Plus GBL4HashType, GBL4SignatureType, GBL4CompressionScheme, GBL4EncryptionScheme, HashValue.digest/matches, GBL4Certificate.public_key, GBL4MemorySection.info/blob, and load_public_key in crypto.

Evidence

Every GBLv4 file I could find in public catalogues, with pygbl at 70c4e59 plus this branch, cryptography 50.0.0, Python 3.14:

corpus images sections content hash section binding signature
IKEA, 18 Matter OTA files in the CSA DCL under vendor 4476 18 18 18 ok 18 ok 18 ok
Philips Hue, the three SiMG301 images in Koenkk/zigbee-OTA and their vendor subelements 104 107 104 ok 104 ok no certificate

All 122 round-trip byte for byte and produce no unknown tags, as before. The Hue images are signed but carry no CERTIFICATE tag, so verify_signature() raises KeyError unless a key is passed; their sections are AES-CCM, so plain_image refuses them and verify_memory_sections checks them as far as the info tag digest.

Two questions I would rather you decide

  1. verify_signature() with no argument falls back to the key in the image's own certificate. That shows the manifest is internally consistent, not that a vendor you trust signed it, and the docstring says so. GBL3Image.verify_signature takes the key instead. Happy to drop the fallback.
  2. Nothing calls these during parsing. parse_firmware_image(validate=True) still does nothing for v4, since a failed hash is a different kind of problem from a failed CRC.

Decryption is not implemented: the key lives in the device, and I have encrypted images but no key to check a guess against, so I left it out rather than ship an untested counter construction.

One existing test changed

test_manifest_fields asserted ENCRYPTION_AESCCM in info.features, which holds for the Hue corpus and fails on all 18 IKEA files. It now asserts compression, then cross-checks the feature bit against the per section encryption_scheme.

Running it

pip install -e .[dev]
pytest tests/test_gbl4_verification.py

Those 22 tests build their own images and need no fixtures. tests/test_gbl4.py picks up the new checks for any .gbl4 file in tests/files; for the IKEA files the GBL starts at offset 84, after the Matter header, and the Hue ones are subelements of a ZCL OTA file. I did not add a downloading fixture, on the assumption you would rather not have CI reach the network, but I have the URLs and digests if you want them.

The manifest already cross-references the rest of a v4 file: CONTENT_HASH covers
the payload after it, each UPDATE_MEMORY_SECTION carries the digest of a
MEMORY_SECTION_INFO tag and the offset of the section it belongs to, and that
info tag carries the digest of the image the section expands to. None of it was
checkable, unlike GBL3Image.verify_signature.

Adds verify_content_hash, verify_memory_sections, verify_signature, plain_image
and the offsets and enums they need, mirroring the v3 API.
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.

1 participant