Skip to content
bucker

Witness onboarding

Who this is for: an operator or auditor who wants Bucker's evidence log to be hard for Bucker to rewrite.


Why a witness is the whole point

Every proof-ledger entry is signed, and the log's heads are published as signed checkpoints. A signature proves who wrote the bytes. It proves nothing about history.

An issuer who controls every byte can produce a perfectly consistent log for a verification that never ran. Re-signing a rewritten log costs nothing when you hold the key. That is not a hypothetical about Bucker specifically; it is the reason transparency logs exist as a genre, and it is stated in the specification itself (docs/proof-ledger-spec, §10.2) rather than hidden.

The fix is that somebody who is not us holds a copy of a head. Once a third party has recorded "at tree size 4,182 the root was abc…", we can no longer produce a different history for those 4,182 entries without that party's copy contradicting us. The log stops being a promise and becomes a commitment.

So the honest reading of an un-witnessed log is:

This log proves internal consistency and authorship, and nothing about history.

That sentence is what GET /orgs/:orgSlug/proof-ledger/witnesses returns in its disclosure field when independentlyWitnessed is false, and it is what the Verification screen prints at the top of the witness panel. It is meant to be uncomfortable until you fix it.


What counts as a witness, and what does not

Independent? What it buys you
A bucket you control, with Object Lock yes (declared) A rewrite is contradicted by an object we cannot delete
An endpoint you run (SIEM, notary, your own store) yes (declared) Same, plus alerting on your side
A store Bucker operates no Durability. Nothing else

A copy the issuer holds cannot corroborate the issuer. The API computes independent from the declared custody rather than storing it as a flag, precisely so nobody can register our own bucket and call the log witnessed:

independent: witness.custody === 'CUSTOMER'

Custody is declared, not verified

We cannot verify that a URL or a bucket is really yours. An endpoint is an endpoint. Every witness response therefore carries custodyUnverified: true, always, and the UI repeats it. What is verifiable is the receipt trail, and — where the witness countersigns — its signature over the exact checkpoint bytes.

Do not read "CUSTOMER custody" as a Bucker attestation. Read it as your claim, recorded with an author and a timestamp.


Onboarding, end to end

1. Choose a transport

OBJECT_STORE — checkpoints are written as immutable objects under a key prefix. The intended production shape is S3 with Object Lock in COMPLIANCE mode.

WEBHOOK — checkpoints are POSTed to an endpoint you run.

Object store is the stronger default: retention is enforced by the bucket, so a witness that is merely offline for a week loses nothing, and there is no service for you to keep up.

2. Register it

curl -X POST https://api.bucker.io/orgs/<org>/proof-ledger/witnesses \
  -H "authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{
        "name": "acme-s3-objectlock",
        "kind": "OBJECT_STORE",
        "custody": "CUSTOMER",
        "endpoint": "bucker-checkpoints/",
        "config": { "bucket": "acme-audit", "region": "us-east-1",
                    "objectLockMode": "COMPLIANCE", "retainDays": 2555 },
        "expectsCountersignature": false
      }'

Requires admin on the org. For a webhook, add a secret of at least 16 characters — that is the HMAC key we sign deliveries with, and it is stored sealed and never returned.

config is non-secret provider settings only. Note what the receipt will say about objectLockMode: we record the write, not the retention. Retention is enforced by your bucket, and if you have not actually turned Object Lock on, nothing in our receipt will notice.

3. Verify a delivery signature (webhook only)

Each POST carries:

x-bucker-timestamp: 2026-08-29T19:00:00.000Z
x-bucker-signature: t=<unix seconds>,v1=<hex>

The signature header is a comma-separated key=value list. Take t and v1 from it and check:

expected = hex(hmac_sha256(secret, t + "." + rawBody))
valid    = constant_time_equals(expected, v1) and abs(now - t) <= your_tolerance

Four things that are easy to get wrong, and that we get asked about:

  • t is the timestamp that was signed — not x-bucker-timestamp. That header is a readable convenience for your logs. It is not covered by the MAC, and signing its contents will fail every delivery.
  • Take the FIRST t= if the value somehow carries two. Last-wins lets anyone who can append to the header shadow the timestamp we actually signed.
  • Sign over the raw body bytes, not a re-serialized parse.
  • Compare in constant time, and reject a t outside your tolerance — 300 seconds is what our own reference verifier uses. The timestamp is inside the signed string specifically so a captured delivery cannot be replayed at your endpoint later.

A future algorithm will arrive as an additional v2= beside v1= in the same header, so a verifier that keeps reading v1 keeps working while one that has migrated can prefer the newer. Ignore keys you do not recognise rather than refusing the delivery.

Changed 2026-09-10. Deliveries previously carried x-bucker-signature: sha256=<hex> over <isoTimestamp>.<body>, with the time in x-bucker-timestamp. The material signed is the same in substance — the timestamp was always inside the MAC — but it is now spelled in unix seconds under t=, and the value is the t=,v1= list above rather than a sha256= prefix. This is the same grammar every other Bucker webhook uses; one name now means one thing. A verifier written against the old shape will reject every delivery rather than accept a forgery.

4. Countersign, if you can

Set expectsCountersignature: true and supply your Ed25519 public key as SPKI PEM. Then answer a delivery with:

{ "countersignature": "<base64 Ed25519 over the exact checkpoint bytes>",
  "algorithm": "Ed25519" }

This is the difference between two very different claims, and the receipt records which one you made:

  • stored — you received a head. A delivery record.
  • attested — you signed that you received these exact bytes. Evidence.

GET /orgs/:orgSlug/proof-ledger/witnesses/:witnessId/receipts reports both counts separately (attestedHeads, storedHeads) rather than adding them together, because adding them together would launder the first into the second.

5. Deliver, and check

# Publish a checkpoint now, then push it to every enabled witness.
curl -X POST .../orgs/<org>/proof-ledger/checkpoints      -H "authorization: Bearer $TOKEN"
curl -X POST .../orgs/<org>/proof-ledger/checkpoints/<id>/deliver -H "authorization: Bearer $TOKEN"

# What came back.
curl .../orgs/<org>/proof-ledger/witnesses/<witnessId>/receipts -H "authorization: Bearer $TOKEN"

A receipt is PENDING, CONFIRMED or FAILED, with attempts and the raw receipt blob (HTTP status and response size, or object key and version id). Delivery retries up to five attempts; a witness that is down does not block issuance, and the backlog is retried by retryPendingDeliveries.

6. Confirm it took

In the app: Verification → Who co-signs our log heads. The panel prints the independentlyWitnessed headline first, then each witness with its custody, whether it countersigns, and its last receipt.

The headline flips to positive only when a witness is enabled, has CUSTOMER custody, and has actually received a head (deliveredCount > 0). Registering a witness is not witnessing.


What you should do with the copies

Holding checkpoints is only half of it. To actually detect a rewrite:

  1. Keep them. Object Lock, or append-only storage on your side. A copy we could ask you to delete is a copy we control.

  2. Compare them. Periodically fetch GET /orgs/:orgSlug/proof-ledger/consistency-proof?oldSize=<a size you hold> and check that the tree you were shown then is a prefix of the tree we show now. bucker-verify does this offline, with no account and no network:

    npx bucker-verify consistency --old checkpoint-4182.json --new checkpoint-9001.json
  3. Alert on a gap. If deliveries stop, that is a signal. A witness nobody reads is a witness that cannot testify.

Step 2 is the one people skip, and skipping it makes the whole arrangement decorative.


Failure modes worth knowing

A checkpoint at a given tree size is immutable. A second write at the same object key with different contents is refused rather than overwritten, and the receipt records the refusal. If you see that error, something is wrong — investigate rather than clearing it.

Object Lock is yours to configure. Our receipt says a write happened. It cannot say retention was enforced.

A non-JSON 200 still counts as received. A witness that acknowledges without a parseable body has the head; it simply has not attested to it. The receipt distinguishes the two.

Disabling a witness does not delete its receipts. History is not editable from the witness roster, deliberately.


  • docs/proof-ledger-spec/ — the published format, its versioning policy, and worked examples
  • code/tooling/bucker-verify/README.md — the offline verifier: no account, no network, no runtime dependencies
  • /verify — the same rule set in a browser tab, for a certificate or a share link
  • /transparency — the public miss-rate register and its own Merkle log