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.gzThis 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.
It builds the command on your runner and stamps the file
From source, in your job, because a step that downloaded a binary from somewhere would be a step whose trust rests on that somewhere. That holds from v0.7, which carries signed Linux binaries the step does not download. The one-shot command polls the time sources, fits its model, and takes a local read over the hash of your artefact. It is not the resident agent: the model is built and thrown away inside the job.
It writes the receipt beside your build output
A COSE signature over CBOR, self-contained, carrying the reading, the interval, the evidence and the hash of the thing it stamps. Anybody can check it with no account and no network.
It puts the receipt into what already ships
Name an SLSA provenance or in-toto statement and the receipt goes in as a predicate field, editing the file where it exists and leaving it alone where it does not. Name a container image and the label arguments come back on the step's `labels` output for the build that owns the image to apply, rather than the Action touching a registry.
It writes what it cannot establish into the job summary
Every run, in words of its own that end by naming the shipped limitation list, timewitness cannot-prove, as the whole of it. A receipt that arrives without them is a number somebody will over-read.
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.
222.628 ms
from a GitHub runner, installed in one line
Measured 2026-09-25 at 11:48 UTC by the public install check, the Action at v0.4 at the sixteen rounds it ships, nine servers behind six operators. Ours, on that runner, on that day, and the receipt is committed in the repository. A bound of 211.3 ms a runner read on 2026-09-09 rests on a receipt taken before receipts named their operators, and timewitness verify now refuses that receipt on the operator floor.
16.219 s
from a GitHub runner reaching only Roughtime, before the operator floor
Measured 2026-09-08 at the four rounds the Action shipped that day. That runner reached three Roughtime operators, and from 2026-09-09 the shipped floor of four refuses such a round, so the step now fails there rather than issuing a receipt this wide.
153.875 ms
on an ordinary desktop
Measured 2026-09-09 at 18:41 UTC, at sixteen rounds, nine servers of three kinds behind six operators. The ceiling it was signed under was 30 s until 2026-09-15. On the receipt committed in the repository, and readable part by part.
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.
It records. It does not prevent. A refusal means TimeWitness declined to sign. In this Action a refusal also fails the step, and what a failed step means for the rest of the job is your workflow's own setting, which by GitHub's default ends the job there.
It carries no legal weight and no compliance claim. Legal standing comes from accreditation, not from engineering, and no regulation we have checked requires cryptographic proof. FINRA Rule 6820 does set a clock synchronisation tolerance, and TimeWitness claims no part in meeting it or any other rule.
A generated key proves one agent signed the receipt and nothing about who that agent was. A log of our keys is served at timewitness.dev/key-log.txt and names the keys of our two Roughtime servers and the one agent key we vouch for, which signs our own receipts from 2026-09-24, with the window it is ours in. A generated key is not in it, so checked against that log your receipt is refused as not ours. Checked without the log, it verifies on its own evidence as it always did.
Order is checked two receipts at a time, from v0.2. timewitness order says which moment came first where the two intervals sit clear of each other, and which receipt was signed first where one names the other by hash. Nothing walks a whole chain.
On outside signatures, a receipt carries whichever of the three roles were gathered for it, which can be fewer than three or none, and says what each one established. A role is left out where its party was not asked, gave no usable answer in time, or gave one that was set aside as too far from the reading to bear on it, and timewitness verify lists the ones the receipt carries. On both timestamp authorities this product uses the token states no accuracy of its own, so it bounds nothing in UTC, and what is left, where a beacon round is carried, says the receipt was not made before that round. The narrower interval beside the reading is our own model's claim. It does not establish that the build did what it says.
Before you install it
Read one real receipt with its numbers pulled apart, and the command that checks it.
How a receipt works