Skip to content

Introduce support for TUF in Update Server#191

Open
doanac wants to merge 13 commits into
mainfrom
tuf-v2
Open

Introduce support for TUF in Update Server#191
doanac wants to merge 13 commits into
mainfrom
tuf-v2

Conversation

@doanac

@doanac doanac commented Jun 26, 2026

Copy link
Copy Markdown
Member

This is a fairly substantial change that introduces the ability to transparently manage TUF metadata for Updates. The branch produces a fully functioning server with TUF metadata that aklite will consume. There are some features that will be added once this has been merged:

The current logic for handling timestamp and targets metadata may not jive well with users wanting to move devices between tags. Given the size and scope of this current change, I'd like to delay work on that until I can get this work merged.

I also haven't tested rollback support. The current approach creates a single Target in the targets.json and doesn't not include any other older Targets. I believe aklite will work correctly but this will need to be tested.

This has been reviewed by Claude Sonnet and Claude Opus.

@doanac

doanac commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

Screenshot of improved Update page:
image

The Update creation section defaults to:
image

and expands to:
image

@vkhoroz vkhoroz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good feature.

LGTM

Comment thread storage/tuf/tuf_data.go Outdated
Comment thread storage/tuf/tuf_data.go
// MetaItem references a version of another metadata file. The ota-tuf format
// for snapshot and timestamp metadata only records the version.
type MetaItem struct {
Version int `json:"version"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem secure enough to only reference the version in the snapshot/timestamp meta.
Our Foundries.io implementation references a version, a length, and a sha256 hash.
I'm not even sure if TUF allows skipping the hash info.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might reference it, but we don't do it. Here's my timestamp.json for an offline update:

"signed": {
    "_type": "Timestamp",
    "expires": "2026-06-30T18:45:39Z",
    "version": 107,
    "meta": {
      "snapshot.json": {
        "version": 107
      }
    }
  }

I think bypassing the hash is how we allow for condensed targets to work across tags.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spec says:

: HASHES :: A dictionary that specifies one or more hashes of the metadata file at METAPATH, with the cryptographic hash function as key and the value as HASH, the hexdigest of the cryptographic function computed on the metadata file at METAPATH. For example: { "sha256": HASH, ... }. HASHES is OPTIONAL and can be omitted to reduce the snapshot metadata file size. In that case the repository MUST guarantee that VERSION alone unambiguously identifies the metadata at METAPATH.

In our case, a version alone does not unambiguously identify the metadata, as we may have different metadata for different tags with the same version.
So, I think it is a good hygiene to include its hash into the metadata.

Comment thread server/ui/api/handlers_tuf.go Outdated
Signed: tuf.TargetsMeta{
SignedCommon: tuf.SignedCommon{
Type: tuf.RoleTargets.TufType(),
Version: tufVer + 10,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right to me, unless I'm missing something.

IIUC, the tufVer is derived from the individual targets versions somehow, and then targets.json version is set to tufVer + 10, in a hope that we have some room for re-signing between the current version and the next tufVer. But, the flaw I see is this: if tufVer gets incremented by e.g. 1, or 2, or 5 - it mangles with the re-signing logic.

I guess, a safer approach would be for version = tufVer * 10, so that there is always an allowance of 10 targets.json versions for every single TUF version, regardless of how the tufVer is managed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may be missing something subtle, but let me try and explain how I think the flow would be:

  • Initial target added. tufVer would be 0, so the first targets.json version would be 10.
  • We add another target. getLatestVersions will return 10. So target 2 will have targets.json version (10 + 10 = 20)

Later on we add support to resign a targets.json. This initial update could go from 10 -> 11.

@vkhoroz vkhoroz Jul 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how exactly you were going to implement the whole thing.
As it is, I think we need to factor in that we have multiple tags, and each tag has its own version of targets.

It is not easy to explain, resulting into TL;DR. Sorry for that.

This is how it was handled for the production timestamp in ota-lite:

  • Let's assume tags A and B have targets.json at version 10 and tag C has targets.json at version 12.
  • We set timestamp version to 10000 for tags A and B, and to version 12000 for tag C.
  • When we need to refresh the expires field, we increment version for tags A and B to 10001 and, for tag C to 12001.
  • That allows to freely switch between tags A and B, which are on the same version; but don't allow switching from a higher version (tag C) to a lower version (tags A, B).

Now, let's try to extend the same logic to the current case, when you want to allow refreshing snapshot/targets expires field.
I think, we still need to allow switching freely between tags A and B in that case.
So, what we should do is this:

  • Set targets and snapshot versions to 100 for tags A/B and 120 for tag C.
  • When any targets.json for version 100 expires - we refresh all targets with the same version to 101; and leave version 120 alone. That preserves an ability to switch between tags of the same version. I think we need to be mindful here, so that we don't extend beyond version 109.
  • When a new app is released with e.g. version 11 for tag A - its version jumps to 110, i.e. upgrades into a higher league.

If we were simply adding 10 (not multiplying) - all our tags quickly run out of sync when refreshing expiration; and we lose an ability to switch between tags on the same version.

IIUC you were kind of copying a simplified logic from ota-lite for CI targets. But, over there we've had a "primary" targets.json, which was then condensed into tags, so that every tag was getting the same exact version. In update-server we handle each tag's targets separately, meaning that their versions are not synchronized.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's assume tags A and B have targets.json at version 10 and tag C has targets.json at version 12.

The idea is that no two tags will ever have the same version. I believe the real issue is that s.getLatestVersions is looking at a single tag. It needs to look across all updates so that things always go forward. In other words, we can't really create an Update in this system that would be like a Target in our current product where you have a Target with tags, ["foo", "bar"].

I also think bumping the targets version should be a manual thing operators do when they want to keep an update around over 90 days. ie, its not a part of the periodic signing daemon.

I'm posting now for you to think on this. I think something still isn't quite right with how this new notion of Updates and tags is working.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something still isn't quite right with how this new notion of Updates and tags is working.

Yeah... I think we need to work out a better tag/version management over time.

As for the time being, let's keep things simple:

  • Auto-resigning timestamp is a must have, hence the timestamp versioning depends on that requirement. A multiple of 1000 had proven its usefulness.
  • As for the targets/snapshot versioning, I'd opt for one of two things for now:
    • A. Keep it simple for now i.e. set version equal to whatever user specified (no multiples or additives). If we later allow users to manually re-sign existing (latest) targets - that's a simple version bump.
    • B. If we plan to implement auto-resigning, similar to how we manage timestamp, I'd add a multiple of e.g. 100 (not additive). But, that can be easily done at that later time in a backward-compatible way.

TBH I did not get how that addition of 10 solves any real-world problem. IMHO it only introduces some mental puzzle and potential code complication.

Comment thread storage/file_updates.go
Comment thread storage/tuf/tuf_data.go
Andy Doan and others added 3 commits July 1, 2026 12:54
Introduce a self-contained storage model for The Update Framework (TUF)
using the Foundries.io/ota-tuf metadata format.

- Define TUF metadata structs internally (no external TUF type imports):
  root, targets, snapshot and timestamp metadata plus AtsKey/Signature.
- Add TufFsHandle under <datadir>/tuf with:
  - InitTuf(): generate ed25519 keys for the root, targets, snapshot and
    timestamp roles, store them AES-256-GCM encrypted under a key derived
    from the HMAC secret, and create a signed initial root.json (20y).
  - LoadTuf(): decrypt and load role keys; errors if not initialized.
  - GetRoots(): return all root.json files ordered by version.
- Default expirations: root 20y, timestamp 7d, targets/snapshot 90d.
- Use go-securesystemslib/cjson for canonical JSON signing.

Co-authored-by: GitHub Copilot:claude-4-opus <noreply@github.com>
Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Add a tuf-init server subcommand to initialize TUF keys and root
metadata, and require TUF to be initialized before the server starts.

Co-authored-by: GitHub Copilot:claude-4-opus <noreply@github.com>
Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Add /v1/tuf/root.json (latest) and /v1/tuf/<n>.root.json (specific
version) endpoints backed by a new ReadRoot storage method.

Co-authored-by: GitHub Copilot:claude-4-opus <noreply@github.com>
Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Andy Doan and others added 10 commits July 1, 2026 22:53
This introduces a new API to properly add target to a given tag/update
and sign its TUF metadata. Target metadata logic works by:

 * Finding the latest targets TUF version and application version for a
   tag. We must know these values to ensure we are creating metadata a
   device will pull down (if the version isn't higher - it won't pull
   it).
 * Increment the TUF version by 10. This gives us some flexibility to
   resign a Targets meta in the event we need to use it longer than the
   default 90 day expiration.
 * Create Snapshot metadata that's basically the same as the Targets.
   Our configuration/usage allows for these files to follow each other's
   versions.
 * Create Timestamp metadata. This follows some clever logic in our
   current ota-lite backend for setting a version that will work across
   mulitple updates for a given update tag.

Targets/Snapshot get a 90 day expiry.
Timestamp get a 7 day expiry.

Co-authored-by: GitHub Copilot:claude-4-opus <noreply@github.com>
Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
This introduces an new API that allows us to refresh timestamp metadata
for any updates that may need it. When the update expiry is within one
day of expiration, it will bump the expiration by a week and resign the
metatdata.

Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
We will use this to help populate default data into a TUF target during
an Update upload

Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This feature probes the update content itself to help automatically
generate the TUF Target for the user removing the need for them to have
TUF set up outside of this project.

Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Co-authored-by: GitHub Copilot:claude-4-opus <noreply@github.com>
Add --version, --name, --ostree-hash, and --apps flags to the update
upload command so users can override the auto-generated TUF target
metadata via the new query parameters on the updates create API.

Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Co-authored-by: GitHub Copilot:claude-opus-4.8 <noreply@github.com>
Add a collapsible "Advanced options" section to the update upload form
with version, target name, ostree hash, and apps fields. These map to
the new query parameters on the updates create API to override the
auto-generated TUF target metadata.

Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Co-authored-by: GitHub Copilot:claude-opus-4.8 <noreply@github.com>
It makes more sense to show in the order timestamp,snapshot,target

Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Collect the de-duplicated set of hardwareIds and tags from across all
targets in targets.json and display them in their own fieldsets on the
update details page.

Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Co-authored-by: GitHub Copilot:claude-opus-4.8 <noreply@github.com>
Add an isExpired template helper and wrap expired root, timestamp,
snapshot, and targets expiration values in a <del> element on the update
details page.

Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
Co-authored-by: GitHub Copilot:claude-opus-4.8 <noreply@github.com>
Signed-off-by: Andy Doan <doanac@qti.qualcomm.com>
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.

2 participants