* feat(extensions): verify catalog archive sha256 before install
Extension and preset archives were downloaded over HTTPS and unpacked
(with Zip-Slip protection) but their bytes were never checked against a
known digest. Trust rested entirely on TLS and the integrity of the
release host, so a tampered or swapped archive from a compromised
third-party release would be installed silently. Maintainers do not audit
extension code, so consumer-side integrity is the only available defence.
Catalog entries may now pin an optional `sha256` digest. When present, the
downloaded archive is verified before it is written to disk and installed;
a mismatch aborts with a clear error. Entries without `sha256` keep
working unchanged (a DEBUG line records that the download was unverified),
so the change is backwards compatible. The check runs on both download
paths (extensions and presets) via a single shared helper so the two stay
in parity.
- Add `verify_archive_sha256` helper in shared_infra (digest match,
`sha256:` prefix, case-insensitive; DEBUG log when no digest declared)
- Enforce it in ExtensionCatalog.download_extension and
PresetCatalog.download_pack, before the archive is written to disk
- Document the optional `sha256` field in the publishing guides
- Tests: helper unit tests + matching/mismatch/no-digest on both paths
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
Assisted-by: AI
* fix(extensions): harden sha256 parsing and tidy download test mocks
Follow-up to the review on #3080:
- shared_infra.verify_archive_sha256: strip only a literal `sha256:`
algorithm prefix (case-insensitive) instead of `split(':', 1)[-1]`,
which silently dropped any prefix — so `md5:<64-hex>` was accepted as
if it were a valid SHA-256. Validate that the declared value is exactly
64 hex characters and raise a clear error otherwise, and compare with
`hmac.compare_digest` for a constant-time check. Add tests covering a
malformed digest and a non-`sha256:` prefix (both previously accepted).
- Download test helpers: configure the context-manager mock via
`__enter__.return_value`/`__exit__.return_value` rather than assigning a
`lambda s: s`, which is clearer and independent of the invocation arity.
Assisted-by: AI
Signed-off-by: Zied Jlassi (Architect AI) <6190550+zied-jlassi@users.noreply.github.com>
* fix(extensions): reject a declared-but-empty sha256 instead of skipping verification
verify_archive_sha256 skipped on any falsy expected value, so a present-but-empty digest (e.g. sha256: "" reached via ...get("sha256")) silently disabled the integrity check instead of surfacing the authoring error. Guard on expected is None so only an absent digest skips; blank/whitespace/bare-prefix values fall through to the 64-hex validation and are rejected. Adds a regression test.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
* docs(shared_infra): clarify _SHA256_HEX_RE accepts and normalizes uppercase
The comment described the regex as matching '64 lowercase' hex characters,
but verify_archive_sha256 lowercases the declared value (raw.lower()) before
matching, so an uppercase digest is accepted and normalized rather than
rejected. Clarify the comment to avoid misleading future readers.
Addresses Copilot review feedback on shared_infra.py.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
* test(presets): cover the no-sha256 backwards-compatible path
Address Copilot review: download_pack's optional sha256 verification was
tested for match/mismatch but not the backwards-compatible path where a
catalog entry has no sha256 (pack_info.get("sha256") is None). Add a
no-sha256 test mirroring the extensions coverage so the helper never
silently becomes mandatory for presets.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
---------
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
Signed-off-by: Zied Jlassi (Architect AI) <6190550+zied-jlassi@users.noreply.github.com>