Rekor Anchoring¶
Available from v2.9.0.
BIJOTEL's HMAC chain proves that the bytes haven't been altered.
bijotel verify answers "is this chain intact?" exactly. What it
cannot answer on its own is when the chain reached a given
state — the operator runs the writer, so an operator could in
principle backdate everything they control.
Rekor anchoring closes that gap. It records, in a public Sigstore-run transparency log, that operator X attested chain head Y at time T. Rekor is append-only, produces inclusion proofs against a published merkle root, and is run by the Linux Foundation — so an operator cannot retroactively change which head_hash they attested once it's in the log.
| Tool | Answers | Trust anchor |
|---|---|---|
bijotel verify |
"the chain bytes have not been altered" | operator's HMAC secret |
bijotel anchor publish |
"operator X attested head Y at time T (witnessed)" | Sigstore Rekor + operator's Ed25519 key |
The two are complementary. A complete audit story uses both.
Why bother¶
Without a public anchor, an auditor only has your word that the chain reached state Y at time T. With one, they have:
- The Rekor entry's
integratedTime(Linux-Foundation-stamped) - The chain's
head_hashat that moment - Your Ed25519 signature over that hash
- An inclusion proof against Rekor's signed merkle root
If you ever rewrite history, the old Rekor entry is still there. Anyone holding a copy of an old anchor can re-fetch by log_index and prove the divergence. The "operator runs the writer" trust gap collapses to "operator runs the writer and owns Sigstore Rekor", which they don't.
Usage¶
Generate a keypair once (or reuse the one from bijotel keygen):
bijotel keygen --output-dir ./keys
# keys/bijotel_ed25519_private.pem
# keys/bijotel_ed25519_public.pem
Anchor the current chain head:
bijotel anchor publish \
--db chain.db \
--sign-key keys/bijotel_ed25519_private.pem \
--output anchor.json
# === Rekor anchor PUBLISHED ===
# rekor: https://rekor.sigstore.dev
# log_index: 1511534821
# log_uuid: 24296fb24b8...
# integrated_time: 1716730000
# head_seq: 6421
# head_hash: a3f9c8d2e7b1...
# public url: https://rekor.sigstore.dev/api/v1/log/entries?logIndex=1511534821
# sidecar: anchor.json
Verify the anchor weeks/months later:
bijotel anchor verify anchor.json \
--public-key keys/bijotel_ed25519_public.pem
# === Rekor anchor MATCH (logIndex=1511534821) ===
# integrated_time: 1716730000
# hash_matches: True
# pubkey_matches: True
# signature_valid: True
Exit codes:
0— match1— operational error (DB/sign-key/anchor file missing)2— argument error3— mismatch (replay, tamper, wrong key)
REST? Not yet.¶
There's no /anchor/* endpoint in v2.9.0 — anchoring is an
operator-driven snapshot action, not a per-request flow. Adding a
REST endpoint that does network I/O to Rekor on every call would
both slow the API and tie its uptime to Sigstore's. The Python /
CLI surface is the v2.9 way.
If you want to run anchoring on a cron, that's exactly the right pattern:
# Every 6h: anchor the chain head + keep the last 30 sidecars
0 */6 * * * cd /opt/bijotel && \
bijotel anchor publish --db /data/chain.db \
--sign-key /keys/private.pem \
--output /anchors/anchor-$(date +\%s).json && \
ls -t /anchors/anchor-*.json | tail -n +31 | xargs rm -f
Honest scope notes¶
The library and CLI are real. The unit tests are real. The
live-public-Rekor upload path has a known compatibility gap with
Rekor 1.4+'s Ed25519 hashedrekord verifier that needs sigstore
PyPI library integration to resolve cleanly.
What does work today:
- ✓ The full library API (
anchor_chain_head/verify_rekor_anchor) - ✓ The CLI (
bijotel anchor publish/verify) - ✓ The HTTP client (
RekorClient.upload/fetch) - ✓ 15 unit tests covering hash, signature, pubkey, mismatch paths via a mocked Rekor HTTP server inside the test
- ✓ A custom Rekor instance (self-hosted) accepting the upload, if the operator wants to run their own log
What's a v2.10 follow-up:
- ⚠ Live
rekor.sigstore.devupload of Ed25519 signatures returns HTTP 400 withfailed to verify signature: ed25519: invalid signature. The signature-encoding interpretation across Rekor 1.4+'shashedrekordanddssetypes is more nuanced than a straighted25519.sign(sha512(data))— the canonical path goes through thesigstorePyPI library which handles the bundle format. v2.10 will swap our rawurllibupload forsigstore.transparency.Rekor, which is purpose-built for this.
If you want to anchor today against the public Rekor while v2.10 is
in flight, use sigstore-python directly to sign + upload the
anchor.head_hash and store the returned UUID in your own sidecar.
The verify path (bijotel anchor verify) can read any Rekor entry
regardless of how it was uploaded.
How verification works (mechanics)¶
The verifier checks three independent things, all of which must pass:
- hash_matches: Rekor's stored hash equals SHA-512 of the head_hash bytes we expect.
- pubkey_matches: When
--public-keyis given, the Rekor entry's public key equals the operator's expected key (PEM-equal, strip-whitespace). Without--public-keythe check falls back to "anchor's pubkey matches Rekor's pubkey" — useful but weaker. - signature_valid: The Ed25519 signature in the anchor still
verifies against
SHA-512(head_hash_bytes).
The reason field in AnchorVerifyResult names which check failed
when match is False, so the CLI's error messages are precise.
Public API¶
from bijotel.anchoring import (
anchor_chain_head, # sign + upload
verify_rekor_anchor, # fetch + check
RekorAnchor, # frozen dataclass with .to_dict()
AnchorVerifyResult, # frozen dataclass with .to_dict()
REKOR_PUBLIC_URL,
)
Each is re-exported from bijotel directly: