DEV Community

Craig Solomon
Craig Solomon

Posted on

Your timestamp is an upper bound, not a date

Pick an artifact you shipped a while back. A release tarball, a model checkpoint, a PDF you sent a client. Now answer one question about it: does your evidence for its date run forward or backward?

I mean that literally. Does the evidence say "this existed no later than X," or does it say "this did not exist before X"? Those are different claims, and the second one is much harder to get than most setups assume. Worth knowing which one you're holding before you need it.

Most of what you have is a claim, not evidence

Start with the file itself.

touch -d "last year" release.tar.gz
stat -c %y release.tar.gz
Enter fullscreen mode Exit fullscreen mode

mtime is a writable field. That's the whole story. Same for ctime in any practical sense, same for the creation dates embedded in PDFs and Office files.

Git feels sturdier. It isn't, not for this:

GIT_COMMITTER_DATE="last year" GIT_AUTHOR_DATE="last year" \
 git commit -m "shipped ages ago, honest"
Enter fullscreen mode Exit fullscreen mode

Commit and author dates are strings inside the commit object. They're covered by the commit hash, so you can't quietly edit one after the fact without rewriting history. But you choose them at write time, and nothing checks them. Sign the tag and you've signed the same chosen string. A GPG signature proves who, not when.

Object storage timestamps and CI logs are a real step up, because a third party wrote them and you didn't. But it's a third party you pay, whose retention you configure, and whose records you can delete. That's fine for your own bookkeeping. It's thin the moment somebody has a reason to doubt you.

The obvious fix, and the place it stops

So you anchor. Hash the bytes, put the digest somewhere you don't control, keep the receipt.

sha256sum release.tar.gz
# a1b2... (64 hex chars, this is the only thing that needs to leave your machine)
Enter fullscreen mode Exit fullscreen mode

Send that digest to a public chain, get back a transaction in a block, and now the block time is attested by consensus instead of by your laptop. Nobody has to trust your clock.

Here's the part that gets skipped.

That block time is an upper bound. Only an upper bound. Your proof says the bytes existed no later than that block. It says exactly nothing about whether they existed the day before, the year before, or were composed forty seconds earlier in a hurry.

Which sounds like a limitation and is actually the whole design instruction. A timestamp is an exclusion tool. It rules things out. If a hash was anchored before some event, then that event cannot have been an input to those bytes. That's the shape of every argument you'll ever build on a timestamp:

  • The disclosure was anchored before the vendor's advisory, so the advisory wasn't the source.
  • The training corpus manifest was anchored before the license changed, so the change wasn't retroactively worked around.
  • The build artifact was anchored before the CVE was published, so nobody patched history to look clean.

Notice what none of those require. None of them need the anchor to be near the file's creation. They need it to be on the correct side of the event you care about.

And that flips the operational question. It's not "how do I timestamp accurately." It's "what am I going to want to exclude, and have I already anchored past it." Since you usually can't know the answer in advance, the practical move is to anchor early and anchor a lot. The boundary you need is the one you set before you knew you'd need it.

Which is why per-proof pricing is poison for this. If each anchor costs you, you triage, and triage means you're predicting which artifact becomes contested. You will predict wrong. That's the reason Polygon anchoring in ProofLedger is free and unlimited on every plan, including the free one, with no lifetime proof cap: the correct behavior is to anchor everything, and a per-proof charge trains the opposite behavior. Bitcoin anchoring does cost per anchor, because the transaction has a real cost, so that one is an escalation you choose for the small number of artifacts that need it.

What the digest does not carry

Second thing people load onto a timestamp that it can't hold: a hash binds to bytes. That's all.

It doesn't say you wrote the file. It doesn't say the contents are true. It doesn't say it's the only copy or the first copy. If two people anchor the same bytes, both proofs are valid, and the earlier block wins on time and on nothing else.

It's also byte-exact, which bites people. Re-export the PDF, normalize line endings, let a CI step recompress a tarball with a different gzip level, and the digest is unrelated to the one you anchored. Not "close." Unrelated. So anchor the exact artifact you'd hand over later, and store the digest next to it. Hash the file you'd hand over, not the file you'd regenerate.

The verification side, briefly

An upper bound is only worth something if the person doubting you can check it without going through you. So the check has to be public and it has to work offline.

curl "https://proofledger.io/api/v1/verify?hash=<sha256>"
Enter fullscreen mode Exit fullscreen mode

Public, no auth, rate limited to 120 requests per hour per IP. Opposing counsel or an auditor can hit it without an account and without telling me they did. There's a verify-proof package on PyPI that does the same check locally, and a GitHub Action for wiring it into CI. The whole REST API is three endpoints, documented at proofledger.io/api.html with an OpenAPI spec at /openapi.json.

Straight talk on the anchor itself: OpenTimestamps gives you Bitcoin timestamping for free, and the proof it produces is cryptographically just as valid as mine. If a raw Bitcoin timestamp is what you need, use it. What I'm building is the platform around the proof, dual anchoring to Polygon and Bitcoin so the record is independently checkable on two chains, an API you can call from CI, and the organization and export layers on top. Not a better proof. There isn't such a thing.

Other honest limits: your granularity is block time, not milliseconds. Anchor time is not creation time and you should never describe it as such. And a proof that the bytes existed is not a proof that the bytes are what they claim to be.

The test, run it today

Take your oldest artifact that you'd have to defend a date for. Then ask, of every piece of date evidence you have for it:

Can somebody who thinks I'm lying check this, using only data I have no ability to edit or delete?

Walk the list. mtime, no. Commit date, no. Signed tag, no, that signs a date you typed. Your storage bucket, only if the person doubting you believes your retention policy more than they doubt you. CI logs, same problem plus an expiry.

If the answer comes back no for every line, you don't have a date. You have an assertion with good manners. And the fix is cheap enough that there's no reason to be in that position twice: one sha256sum, one anchor, keep the digest.

If it's useful, that's what I've been building at https://proofledger.io. Either way, run the test on your own stack first. The interesting part is usually how short the list gets.

Top comments (1)

Collapse
 
_firelinks profile image
Mike Dabydeen

The test at the end is the right test. I would add a second one, because the half that decays is not the anchor.

An anchor is durable by construction. The bytes are not. Three years later what you have to produce is the artifact that hashes to the digest you anchored, and in most shops that artifact is not stored, it is regenerated by whatever system owns it. You cover the mechanics of this with gzip levels and PDF re-exports, but the consequence is larger than a hashing hygiene note. Anchor everything and you accumulate a pile of proofs, an unknown fraction of which already have no preimage you can produce, and nothing about the proof tells you which ones.

So the second test is not a one-off. Re-hash your stored artifacts against their anchors on a schedule and report the fraction that still match. In enterprise logistics the document someone asks for years later is almost never the one that was filed. It is the one the reporting system rebuilds on request, and the rebuild comes out byte-different every time the reporting stack changes underneath it. That is the common case rather than the exotic one.

One note on dual anchoring, since you were straight about everything else. Two chains give you two consensus guarantees, but what you need at verification time is that a stranger can still read the chain and parse your proof format years from now. That is retrievability, not consensus. Worth stating as two different guarantees, because a reader who treats them as redundant copies of one will choose on price, which is the opposite of what you want them doing.