# 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.

## Install

```bash
composer require k2gl/tuf
```

## Requirements

- PHP >=8.1

## Documentation

# k2gl/tuf

[![CI](https://img.shields.io/github/actions/workflow/status/k2gl/tuf/ci.yml?branch=main&label=CI&logo=github)](https://github.com/k2gl/tuf/actions/workflows/ci.yml)
[![Latest Stable Version](https://img.shields.io/packagist/v/k2gl/tuf?logo=packagist&logoColor=white)](https://packagist.org/packages/k2gl/tuf)
[![Total Downloads](https://img.shields.io/packagist/dt/k2gl/tuf?logo=packagist&logoColor=white)](https://packagist.org/packages/k2gl/tuf)
[![PHPStan Level](https://img.shields.io/badge/PHPStan-level%209-2a5ea7?logo=php&logoColor=white)](https://phpstan.org)
[![License](https://img.shields.io/packagist/l/k2gl/tuf?color=yellowgreen)](https://packagist.org/packages/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

```bash
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.

```php
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:

```php
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:

```php
$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](LICENSE). An independent, clean-room implementation of the
TUF specification.

## API

### K2gl\Tuf\Exception\BadVersionException (class)

_no public methods_

### K2gl\Tuf\Exception\DownloadException (class)

_no public methods_

### K2gl\Tuf\Exception\ExpiredMetadataException (class)

_no public methods_

### K2gl\Tuf\Exception\LengthOrHashMismatchException (class)

_no public methods_

### K2gl\Tuf\Exception\RepositoryException (class)

_no public methods_

### K2gl\Tuf\Exception\TufException (interface)

_no public methods_

### K2gl\Tuf\Exception\UnsignedMetadataException (class)

_no public methods_

### 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`

## Links

- GitHub: https://github.com/k2gl/tuf
- Packagist: https://packagist.org/packages/k2gl/tuf
