Packages · supply-chain

k2gl/tuf

A pure-PHP client for The Update Framework (TUF).

Resolve and verify TUF metadata so you can trust what a repository claims to distribute.

k2gl/tuf version on Packagist k2gl/tuf total downloads k2gl/tuf license

Install

composer require k2gl/tuf
PHP
>=8.1

Reach for it when

  • You need to resolve and verify TUF metadata (root, targets, snapshot, timestamp).
  • You’re building a client that trusts a TUF-secured repository.

Look elsewhere when

  • You only want Sigstore’s trust root — sigstore-verify pulls it via TUF for you.

k2gl/tuf

A fail-closed TUF (The Update Framework) client for PHP. Starting from an embedded root.json trust anchor, it refreshes signed metadata in the order the spec mandates and downloads targets, every byte verified against threshold-signed metadata or it throws.

The motivating use case is fetching Sigstore's trusted_root.json securely, but the client is a complete, general TUF implementation with no Sigstore specifics baked in.

What it guarantees

Refreshing and downloading enforce, in order and fail-closed, the TUF client workflow:

  1. Root — each new root is signed by the threshold of keys of both the currently trusted root and the new one, and its version increases by exactly one (key-compromise and rollback protection).
  2. Timestamp — signed by the root's timestamp role; neither its version nor the snapshot version it points at may roll back.
  3. Snapshot — matches the length and hashes the timestamp recorded, is signed by the root's snapshot role, matches the version the timestamp points at, and never rolls back or drops any targets metadata.
  4. Targets — match the length and hashes the snapshot recorded, signed by the delegating role, version matching the snapshot.
  5. Target files — verified against the length and hashes in the trusted targets metadata before the bytes are handed back.

Anything missing, expired, mis-versioned, or insufficiently signed throws. There is no "best effort" path.

Install

composer require k2gl/tuf

Requires PHP 8.1+ and ext-json. For signature verification, enable the extension matching the repository's key schemes:

  • ext-sodium for ed25519 keys;
  • ext-openssl for ecdsa-sha2-nistp256 and rsassa-pss-sha256 keys.

Any other scheme, an unloadable key, or a malformed signature simply does not count toward a threshold (fail-closed).

Usage

You supply the initial root.json (the trust anchor — ship it with your application) and the repository URLs. The updater does the rest.

use K2gl\Tuf\Updater;
use K2gl\Tuf\HttpFetcher;
use K2gl\Tuf\Exception\TufException;

$updater = new Updater(
    trustedRoot:     file_get_contents(__DIR__ . '/root.json'),
    metadataBaseUrl: 'https://tuf-repo-cdn.sigstore.dev',
    targetBaseUrl:   'https://tuf-repo-cdn.sigstore.dev/targets',
    fetcher:         new HttpFetcher(),
);

try {
    $updater->refresh();

    $info = $updater->getTargetInfo('trusted_root.json');

    if ($info === null) {
        throw new RuntimeException('Target is not listed in trusted metadata.');
    }
    $trustedRootJson = $updater->downloadTarget($info); // verified bytes
} catch (TufException $e) {
    // Trust could not be established — fail closed.
    throw $e;
}

getTargetInfo() walks delegations depth-first (honouring terminating delegations), fetching delegated targets metadata as needed, and returns null when no trusted role vouches for the path. downloadTarget() re-verifies the content's length and hashes before returning it.

Keeping it offline (bring your own fetcher)

The network is reached only through the Fetcher interface, so the trust logic itself is pure. Point the client at a local mirror, an HTTP client you already use, or a fully in-memory source by implementing one method:

use K2gl\Tuf\Fetcher;
use K2gl\Tuf\Exception\DownloadException;

final class MirrorFetcher implements Fetcher
{
    public function fetch(string $url, int $maxLength): string
    {
        // Return at most $maxLength bytes, or throw DownloadException
        // (including for "not found", which ends the root version chain).
    }
}

The trust anchor

The security of the whole chain rests on the initial root.json you embed: it is the one piece that is trusted a priori. Ship it with your application and update it deliberately. Its own expiry is intentionally not enforced on load — the refresh immediately walks the root version chain to the latest — but every other piece of metadata must be current.

Persisting a rotated root

If the repository has rotated its root since the copy you embedded, refresh() walks the version chain and trusts the newer one for that process — but the next process still starts from the old embedded root.json and has to walk the same chain again. Persist the latest trusted root after a successful refresh and load it next time instead of the embedded one:

$updater->refresh();
file_put_contents($localRootPath, $updater->getTrustedRootBytes());

Lower-level API

For advanced or fully offline use, TrustedMetadataSet is the verification core without any I/O: construct it with a trusted root and feed it metadata bytes (updateRoot(), updateTimestamp(), updateSnapshot(), updateTargets(), updateDelegatedTargets()) in workflow order. It applies exactly the checks listed above and exposes the verified Root, Timestamp, Snapshot and Targets value objects from K2gl\Tuf\Metadata.

Exceptions

Everything thrown implements K2gl\Tuf\Exception\TufException:

  • UnsignedMetadataException — a role's signature threshold was not met.
  • BadVersionException — a version rollback or a version that disagrees with its referring metadata.
  • ExpiredMetadataException — metadata is past its expiry.
  • LengthOrHashMismatchException — a downloaded file does not match the trusted length or hashes.
  • DownloadException — a file could not be fetched.
  • RepositoryException — metadata is malformed or otherwise inconsistent.

License

MIT — see LICENSE. An independent, clean-room implementation of the TUF specification.

API

Public classes and methods, generated from the source.

K2gl\Tuf\Exception\BadVersionException class

K2gl\Tuf\Exception\DownloadException class

K2gl\Tuf\Exception\ExpiredMetadataException class

K2gl\Tuf\Exception\LengthOrHashMismatchException class

K2gl\Tuf\Exception\RepositoryException class

K2gl\Tuf\Exception\TufException interface

K2gl\Tuf\Exception\UnsignedMetadataException class

K2gl\Tuf\Fetcher interface

  • fetch(string $url, int $maxLength): string

K2gl\Tuf\HttpFetcher class

  • __construct( private readonly int $timeoutSeconds = 30, )
  • fetch(string $url, int $maxLength): string

K2gl\Tuf\Metadata\DelegatedRole class

  • __construct( public readonly string $name, array $keyids, int $threshold, public readonly array $paths = [], public readonly bool $terminating = false, public readonly array $pathHashPrefixes = [], )
  • fromArray(array $data): self

K2gl\Tuf\Metadata\Delegations class

  • __construct( public readonly array $keys, public readonly array $roles, )
  • fromArray(array $data): self

K2gl\Tuf\Metadata\Key class

  • __construct( public readonly string $keytype, public readonly string $scheme, public readonly string $public, )
  • fromArray(array $data): self
  • verifySignature(string $message, string $signatureHex): bool

K2gl\Tuf\Metadata\MetaFile class

  • __construct( public readonly int $version, public readonly ?int $length = null, public readonly array $hashes = [], )
  • fromArray(array $data): self
  • verify(string $bytes): void

K2gl\Tuf\Metadata\Metadata class

  • fromJson(string $json): self
  • verifySignedBy(string $roleName, array $keys, Role $role): void

K2gl\Tuf\Metadata\Role class

  • __construct( public readonly array $keyids, public readonly int $threshold, )
  • fromArray(array $data): self

K2gl\Tuf\Metadata\Root class

  • __construct( string $specVersion, int $version, DateTimeImmutable $expires, public readonly bool $consistentSnapshot, public readonly array $keys, public readonly array $roles, )
  • fromArray(array $signed): self
  • role(string $name): Role
  • verifyDelegate(string $roleName, Metadata $metadata): void

K2gl\Tuf\Metadata\Signed class

  • __construct( public readonly string $type, public readonly string $specVersion, public readonly int $version, public readonly DateTimeImmutable $expires, )
  • fromArray(array $signed): self
  • isExpired(DateTimeImmutable $reference): bool

K2gl\Tuf\Metadata\Snapshot class

  • __construct( string $specVersion, int $version, DateTimeImmutable $expires, public readonly array $meta, )
  • fromArray(array $signed): self
  • metaFor(string $filename): ?MetaFile

K2gl\Tuf\Metadata\TargetFile class

  • __construct( public readonly string $path, public readonly int $length, public readonly array $hashes, public readonly array $custom = [], )
  • fromArray(string $path, array $data): self
  • verifyLengthAndHashes(string $bytes): void

K2gl\Tuf\Metadata\Targets class

  • __construct( string $specVersion, int $version, DateTimeImmutable $expires, public readonly array $targets, public readonly ?Delegations $delegations = null, )
  • fromArray(array $signed): self
  • target(string $path): ?TargetFile
  • verifyDelegate(string $roleName, Metadata $metadata): void

K2gl\Tuf\Metadata\Timestamp class

  • __construct( string $specVersion, int $version, DateTimeImmutable $expires, public readonly MetaFile $snapshotMeta, )
  • fromArray(array $signed): self

K2gl\Tuf\TrustedMetadataSet class

  • __construct(string $rootBytes, ?DateTimeImmutable $referenceTime = null)
  • root(): Root
  • rootBytes(): string
  • timestamp(): ?Timestamp
  • snapshot(): ?Snapshot
  • targets(string $role = 'targets'): ?Targets
  • updateRoot(string $rootBytes): Root
  • updateTimestamp(string $timestampBytes): Timestamp
  • updateSnapshot(string $snapshotBytes, bool $trusted = false): Snapshot
  • updateTargets(string $targetsBytes): Targets
  • updateDelegatedTargets(string $targetsBytes, string $roleName, string $delegatorName): Targets

K2gl\Tuf\Updater class

  • __construct( private readonly string $trustedRoot, string $metadataBaseUrl, string $targetBaseUrl, private readonly Fetcher $fetcher, private readonly ?DateTimeImmutable $referenceTime = null, )
  • refresh(): void
  • getTrustedRootBytes(): string
  • getTargetInfo(string $targetPath): ?TargetFile
  • downloadTarget(TargetFile $info): string