TimeWitness

You already sign your releases

The time they were built is the one field in them nobody can check. One line in your workflow puts a receipt beside it, with outside signatures anyone can check. Public repositories are free.

Your provenance already says who built it, from what, and with which key. The one field in it nobody can check is the time it was built. This is what it costs to put a receipt beside it, carrying outside signatures anyone can check.

Version 1, 2026-09-10

The whole of it

One step in a workflow file. There is no configuration file and nothing to store as a secret, and every input below has a default that works. Today the step links the repository to no account, and the key it signs with gets no certificate from anybody. A certificate for that key through GitHub's own workflow identity comes later and is not switched on in v0.8.2, so the step asks for no id-token permission and makes no call to the app unless send, the last input below, is set.

- uses: Fountech-ai-Limited/timewitness@v0.8.2 with: subject: dist/widget.tar.gz

This runs today, for anybody, on a GitHub-hosted ubuntu-latest runner, which is the only runner it has been run on: the repository is public and its current release is v0.8.2. It has run end to end where anybody can look. On 2026-09-14 the public repository Fountech-ai-Limited/timewitness-install-check installed it at v0 in one line and released an archive with its receipt, and a second run, holding no token, built the verifier from the public source and checked that receipt with no account.

The repository was made public on 2026-09-14, with the Apache-2.0 licence already in it. From v0.7 each release carries signed binaries for Linux alone, and the step does not use them: it builds the command on your runner. Publishing was the one act on this product that could not be undone, because from that moment somebody else holds a copy, which is why the licence went in first.

What the step does

It stamps whatever the step before it produced, then puts the receipt where the provenance you already ship can carry it.

Every input, and every default

All twelve from action.yml, in the order it lists them and in its own words. Where a description runs long, the sentences that carry a dated measurement or the arithmetic behind a default stay in action.yml and are not repeated here. Only the first has no default.

subjectrequired
The file to stamp. Whatever the step before this one produced: an archive, a binary, an image digest written to a file, an in-toto statement.
eventbuild
What happened, for the person reading the provenance later. One of build, test, signing or provenance, or anything else that describes the step.
outputtimewitness-receipt.cbor
The receipt to write.
keytimewitness-agent.key
A 32 byte agent key. One is generated on the runner where this file does not exist, which is what keeps the install to one line. A generated key proves that one agent signed this receipt and nothing about who that agent was. The log of our keys served at timewitness.dev/key-log.txt names our own agent key and not a generated one, so checked against that log a receipt signed with a generated key is refused as not ours. Checked without it, the receipt verifies on its own evidence as it always did. Supply your own, from a secret, where you want a key that persists across runs.
rounds16
How many times to poll the time sources before reading, and a longer wait. Below three the model cannot fit a line, so the scatter of its own measurements is never measured and never enters the width: the third round widens the bound and it narrows again from there. Sixteen is the default. No figure is given here for how long a count takes today or for what it buys in width, because both move with the runner and the sources it reaches.
max-width2000000000
The widest interval this agent will sign for, in nanoseconds. The default is two seconds, the narrowest interval a Roughtime corridor can state, so a wider bound says less than one signed corridor already does. Past it the Action fails rather than issuing a receipt whose bound says nothing.
deadline300
How long the whole stamping run may take, in seconds, before it answers by refusing. A healthy run takes seconds. Raise it if you also raise rounds.
previousempty
The receipt from the step before this one, where you are stamping several events in one workflow run. The new receipt links back to it.
sequence1
Where this receipt sits in that chain.
provenanceempty
An SLSA provenance or in-toto statement to write the receipt into, as a predicate field. The file is edited where it exists and left alone where it does not.
imageempty
A container image to label with the receipt. Where this is set the Action writes the label arguments to its `labels` output rather than touching a registry, so the build that owns the image applies them.
sendfalse
Also give a copy of the receipt to the app that keeps an organisation's receipts, as a step after the stamp. Off by default, and with it off the Action makes no call to the app at all. The app is asked first with no credential, and where it does not answer as the app that takes machines, or is not open yet, this step warns and the credential stays on the runner. A TIMEWITNESS_APP set in the workflow's environment names another address of the app, such as a test one. It reads a machine credential from the TIMEWITNESS_MACHINE_CREDENTIAL environment variable, which the workflow sets from a secret, and sends the receipt and the figures it states, never the file it stamps. A send that fails is reported as a warning and never fails the job: the receipt is already written, and it checks the same with the free verifier whether or not the app holds a copy.

And what comes back

receipt
The path to the signed receipt.
earliest-ns
The earliest UTC the interval allows, in nanoseconds since the Unix epoch.
latest-ns
The latest.
width-ns
How wide the interval is. This is the claim, and it is not an accuracy.
attestations
How many third-party attestations the receipt carries, of the three roles.
labels
Container image label arguments carrying the receipt, for the build that owns the image to apply. Empty unless `image` was set.

What a build runner can honestly reach

A runner is a worse machine for this than your laptop, and the reason is which time sources it can get to rather than anything about the hardware. Read the figures with the conditions attached or do not read them at all.

None of these is a figure for your runner. What sets it is which sources answer from inside your network, and that is a thing you can measure and we cannot. The default of two seconds on `max-width` is the narrowest interval a Roughtime corridor can state, and about seven times the widest a runner has given us, 287.147 ms wide on 2026-09-14 at sixteen rounds. The step fails past it rather than signing an interval that says less than one signed corridor.

A bound resting on third-party signatures rather than on our own model is what this is being built towards, and about one second is where that starts. That is a target rather than a measurement, and nothing outside the tests constructs it yet.

What this cannot do for you

Read this before you put it in a workflow rather than after. The full list is one click away.

What it cannot prove

Before you install it

Read one real receipt with its numbers pulled apart, and the command that checks it.

How a receipt works