Skip to content

Instantly share code, notes, and snippets.

@PhilETaylor
Created March 11, 2026 22:04
Show Gist options
  • Select an option

  • Save PhilETaylor/0fee6f69316cc10e8bf56cd3a22ee887 to your computer and use it in GitHub Desktop.

Select an option

Save PhilETaylor/0fee6f69316cc10e8bf56cd3a22ee887 to your computer and use it in GitHub Desktop.

Joomla Fork (joomla-backports/php-tuf) vs Upstream (php-tuf/php-tuf)

The Joomla fork diverged from upstream around Dec 2023 (commit 7c2c58c5) and has 18 Joomla-specific commits on top. Meanwhile, upstream has continued with 25+ commits including major refactoring. The two have diverged significantly.

Joomla-Specific Changes (not in upstream)

1. Signature Verifier Recreation During Root Rotation (Critical Security Fix)

File: src/Client/Updater.php

Joomla added lines to recreate the SignatureVerifier and UniversalVerifier after each root metadata update in the updateRoot() loop (between steps 5.3.7 and 5.3.8 of the TUF spec).

Upstream does not do this — it creates the verifier once before calling updateRoot() and never recreates it during the root rotation loop. This means upstream validates intermediate root versions using the original root's keys, while Joomla validates each new root using the previous root's keys (as the TUF spec arguably requires for signature verification with key rotation).

This is the most significant divergence and could be a real bug in upstream or a difference in spec interpretation.

// In Joomla's updateRoot() loop, after § 5.3.7:
+ // Recreate Verifier with new root metadata
+ $this->signatureVerifier = SignatureVerifier::createFromRootMetadata($rootData);
+ $this->universalVerifier = new UniversalVerifier($this->storage, $this->signatureVerifier, $this->metadataExpiration);

2. Canonical JSON Sorting Order Fix

File: src/CanonicalJsonTrait.php

Joomla moved recursive sortKeys() calls before the array_is_list() check and changed visibility from private to protected. The upstream version recurses after sorting the current level; Joomla recurses first, then sorts.

This changes behavior for nested arrays inside indexed (list) arrays — Joomla ensures sub-arrays inside lists still get their keys sorted, even though the list itself doesn't need sorting. Upstream does the same thing but in a different order (recurse after sort vs before).

Note: upstream later refactored this in March 2024 to a one-liner (if (!array_is_list($data) && !ksort(...))), with recursion always happening after.

- private static function sortKeys(array &$data): void
+ protected static function sortKeys(array &$data): void
  {
+     // Apply recursively on potential subarrays
+     foreach ($data as $key => $value) {
+         if (is_array($value)) {
+             static::sortKeys($data[$key]);
+         }
+     }
+
      // If $data is numerically indexed, the keys are already sorted
      if (array_is_list($data)) {
          return;
      }
      if (!ksort($data, SORT_STRING)) {
          throw new \RuntimeException("Failure sorting keys.");
      }
-     foreach ($data as $key => $value) {
-         if (is_array($value)) {
-             static::sortKeys($data[$key]);
-         }
-     }
  }

3. Relaxed spec_version Validation

File: src/Metadata/MetadataBase.php

  • Joomla: Regex /^1(\.[0-9]+){1,2}$/ — accepts 1.0, 1.0.31, and even 1.31
  • Upstream: Uses AtLeastOneOf with IdenticalTo('1.0') OR Regex('/^1\.[0-9]+\.[0-9]+$/') — accepts 1.0 and 1.x.y but not 1.31

Both address the same issue (the original regex rejected 1.0), but the upstream solution is stricter.

4. Removed toNormalizedArray() Indirection

Files: src/Metadata/MetadataBase.php, src/Metadata/TargetsMetadata.php

Joomla removed the toNormalizedArray() method entirely and inlined the logic directly into toCanonicalJson(). Upstream kept toNormalizedArray() as a separate method that toCanonicalJson() calls — it's used as an override point in TargetsMetadata.

Joomla's TargetsMetadata::toCanonicalJson() also explicitly calls self::sortKeys() before encoding, while upstream relies on encodeJson() to handle sorting.

5. PHP 8.4 Compatibility Fixes

Files: Multiple exception classes, Repository.php, DelegatedRole.php, SnapshotVerifier.php, TargetsVerifier.php

Joomla added ?Type nullable syntax to replace the deprecated implicit nullable (Type $param = null) across ~8 files. Upstream addressed PHP 8.4 compat separately in their own test matrix update (May 2025).

6. guzzlehttp/promises ^2.0 Support

File: composer.json

Joomla allows ^1.5 || ^2.0. Upstream's current composer.json likely handles this differently (they've had broader dependency updates).

What Upstream Has That the Fork Doesn't

Since the fork point, upstream has added:

  • Static caching of createFromJson() for performance
  • Performance optimizations for delegated role lookups
  • Symfony 7.3 deprecation fixes
  • PHP 8.4 and 8.5 test coverage
  • Complete removal of Prophecy (testing framework migration)
  • Read-only properties replacing getter methods
  • TUF spec update to 1.0.33
  • Removal of keyid_hash_algorithms support
  • Various code quality improvements

Summary

Change Joomla Fork Upstream
Verifier recreation on root rotation Yes (security fix) No
Canonical JSON sort order Recurse-first Sort-first (refactored)
spec_version accepts 1.0 Via relaxed regex Via AtLeastOneOf
toNormalizedArray() Removed Kept
PHP 8.4 nullable syntax Backported Handled separately
guzzle/promises v2 ^1.5 || ^2.0 Updated independently
Performance optimizations None Static cache, delegated role optimization
TUF spec version 1.0.32 1.0.33

The verifier recreation fix is the most interesting — it's arguably a spec compliance fix that upstream hasn't adopted. The canonical JSON changes also address real bugs with nested array sorting that upstream fixed differently.

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