aics-tlog
Verifier/builder for C2SP transparency logs: pure Python, so an AI agent can mechanically check that a message log is append-only, that a message is included, and that nothing was silently deleted.
Implements: tlog-tiles (tile layout, height 8,
.p/<W> partials, tile/entries/ bundles),
tlog-checkpoint,
signed-note (Ed25519, key hashes),
tlog-cosignature (cosignature/v1) and
the RFC 9162 §2.1.1–2.1.4 inclusion/consistency verification algorithms
(independent of the generators). Hashing is RFC 6962 SHA-256
(H(0x00||d) / H(0x01||l||r)).
Only Ed25519 signature types are supported (0x01 note signatures, 0x04 checkpoint cosignatures); ML-DSA-44 (0x06) is not implemented.
Note rules follow AICS-1 §6.2: two signature lines with the same key name
and key ID reject the note when that key is recognized (the log key or a
configured witness); lines under unknown keys are ignored, repeated or not,
so a note a witness cosigned with a stray foreign line still verifies.
verify_consistency follows AICS-1 §14.2 step 1 at size 0: from the empty
tree only the empty-tree root SHA-256("") and an empty proof are accepted,
and two size-0 heads are consistent only when both carry that root.
Usage example (10 lines)
from aics_tlog import (MerkleTree, Checkpoint, Note, sign_checkpoint,
sign_cosignature, verify_checkpoint, verify_leaf_inclusion,
verify_consistency, vkey_from_ed25519, cosignature_vkey)
tree = MerkleTree([b"msg-1\n", b"msg-2\n", b"msg-3\n"]) # 1. the log's messages
note = sign_checkpoint( # 2. log signs its head
Checkpoint("log.example/msgs", 3, tree.root()), log_key, "log.example/msgs")
_, cosig = sign_cosignature( # 3. a witness cosigns
wit_key, "wit.example/w1", 1767000000, "log.example/msgs", 3, tree.root())
ver = verify_checkpoint( # 4. quorum of witnesses
Note(text=note.text, signatures=(cosig, *note.signatures)),
vkey_from_ed25519("log.example/msgs", log_key.public_key()),
[cosignature_vkey("wit.example/w1", wit_key.public_key())], quorum=1)
assert ver.ok
assert verify_leaf_inclusion(b"msg-2\n", 1, 3, tree.inclusion_proof(1), tree.root())
assert verify_consistency(2, 3, tree.root(2), tree.root(3), tree.consistency_proof(2, 3))
Serving and reading tiles
from aics_tlog import (DirectoryTileStore, IncrementalTileWriter, HttpTileReader,
VerifiedTileReader, inclusion_proof_from_tiles, audit_tiles)
# Writer (one process owns the store): appends touch only the right edge.
store = DirectoryTileStore("/srv/tiles") # served as the tlog-tiles URL prefix
writer = IncrementalTileWriter(store, size=published) # resume at the last published size
assert writer.root() == root_from_leaf_table # boot check (audit_tiles walks every tile)
writer.append(new_entries) # new full tiles once, new .p/<W> partials
store.write_checkpoint(sign_checkpoint(Checkpoint(origin, writer.size, writer.root()), key, origin).to_bytes())
# Reader: log first, then mirrors; only authenticated tiles reach the cache.
reader = HttpTileReader(["https://log.example/", "https://mirror.example/tiles/"], cache_dir="~/.cache/aics")
vr = VerifiedTileReader(reader, cp.size, cp.root) # cp verified with an out-of-band key
proof = inclusion_proof_from_tiles(vr, index, cp.size)
entry = vr.entry(index) # proven entry bytes
IncrementalTileWriternever writes a path twice with different bytes (TileConflictError); full tiles and full entry bundles are written once, right-edge partials at new.p/<W>paths, and partials of a tile are pruned once its full tile exists (prune_partials=True, allowed by tlog-tiles). Writes are atomic (temporary file + hard link) and fsynced by default. A repeated append after a crash is a no-op for what was already written.HttpTileReadertries each URL prefix in order and moves on after a 404, error, timeout, redirect (refused, as the spec requires), oversized or malformed response. It sendsAccept-Encoding: gzip, identityand caps sizes before and after decompression.file://prefixes read local copies.VerifiedTileReaderproves every tile it returns against a trusted(size, root): the right edge by recomputing the root, full tiles against their parent tile, entry bundles against their level 0 tile. A copy that fails is rejected and fetched from the next mirror; only proven copies are cached.audit_tiles(reader, size, root)walks the whole tree.inclusion_proof_from_tiles/consistency_proof_from_tiles/tree_root_from_tileswork over anyTileReaderfor anyindex < size <= tile_sizeandold <= new <= tile_size.
Freshness and witness policies
policy = WitnessPolicy.from_json(open("policy.json").read())
# {"quorum": 1, "witnesses": ["wit.example/w1+<hex>+<b64>"], "max_checkpoint_age": 3600}
res = verify_checkpoint(note, log_vkey, policy=policy) # quorum >= 1, fresh cosignatures only
A cosignature older than max_age_seconds (or a policy's
max_checkpoint_age) does not count toward the quorum, so a replayed old
checkpoint fails. With a freshness bound, cosignatures more than 30 s in the
future (max_future_seconds, default DEFAULT_MAX_FUTURE_SECONDS = 30) do
not count either: every counted timestamp satisfies
now - max_checkpoint_age <= timestamp <= now + 30 (AICS-1 §6.3). A public
key counts once, whatever name it appears under. A policy always requires
quorum >= 1.
Bundles (offline verification)
Bundles use the AICS-1 §14.3 wire form, exactly these six members:
{"entry_bytes": "<U64>", "index": 523, "inclusion_proof": ["<B64>", "..."],
"checkpoint_note": "<U64 of the complete signed note>", "envelope": {"...": "..."},
"body_or_ct": "<U64>"}
U64 is canonical unpadded base64url and B64 canonical padded base64
(AICS-1 §2); checkpoint_note encodes the whole note, signature lines and
final LF included. Missing or unknown members, duplicate JSON names and
non-canonical encodings are refused. bundle_from_tiles and
verify-inclusion --bundle-out write this form (the envelope defaults to the
one the entry embeds); Bundle.from_dict / from_json, verify_bundle
(which also takes the wire object or JSON text directly) and verify-bundle
read it. The pre-0.2 form (entry_bytes_b64, note text, padded base64 body)
is refused with a message saying so. envelope may be null only for entries
that embed no envelope (logs other than AICS-1); such a bundle proves
inclusion only.
verify_bundle(bundle, log_vkey) checks the checkpoint signature (and a
witness policy, if given) and the inclusion of entry_bytes at index. For
an AICS-1 entry (entry v2 whose payload embeds envelope) it then requires
the entry bytes to be JCS with exactly the entry v2 members, the payload to
be the envelope plus an exact copy of its payload (§5.1), the bundle's
envelope to equal the embedded one (§14.3 step 1), and body_or_ct to hash
to body_hash, ct_hash or document_hash with the entry's size (§14.3
step 4). For other entries the entry must commit to envelope (verbatim, or
by the SHA-256 of its canonical JSON in a hash-named field) and a hash-named
field to body_or_ct. It reports where each binding was found
(aics1_entry says which rules applied). The envelope's own signature, the
key chain and the other §14.3 steps are checked by aics-identity / aics,
not here. Keys never come from a bundle.
CLI
$ uv run aics-tlog --help
usage: aics-tlog [-h] [--version]
{verify-checkpoint,verify-inclusion,verify-consistency,verify-bundle,build-tiles,append-tiles} ...
The log key is always given by the caller: --vkey-file (a local file
obtained out of band, i.e. the DNS TXT record, the aics PyPI package or the
public README) or --log-key. A key file that is a URL, or that lies inside a
tile directory being verified, is refused. A key file may hold several keys,
one per line, labelled log <vkey> / policy <vkey>.
--tiles takes a URL prefix or a directory and may be repeated for mirrors.
Without --checkpoint, the checkpoint is fetched from every source and each
is verified. The largest valid one is used, and the others must be consistent
with it; otherwise the command fails with both notes as evidence of a split
view.
# the log's signed head, its witnesses and its tiles
aics-tlog verify-checkpoint --tiles https://wesupport-ai.com/ --tiles https://mirror.example/tiles/ \
--vkey-file aics.keys --policy-file policy.json --save-checkpoint last.note
# entry 42 is in the log: proof computed from authenticated tiles, bundle saved
aics-tlog verify-inclusion --tiles https://wesupport-ai.com/ --vkey-file aics.keys \
--index 42 --bundle-out msg42.json
# today's log extends the checkpoint saved last time
aics-tlog verify-consistency --tiles https://wesupport-ai.com/ --vkey-file aics.keys \
--old-checkpoint last.note --save-checkpoint last.note
# offline: only the AICS-1 §14.3 wire bundle and the out-of-band key
aics-tlog verify-bundle --bundle msg42.json --vkey-file aics.keys --offline
# classic proof-file modes still work
uv run aics-tlog verify-checkpoint --note checkpoint.note --log-key '<vkey>' \
--witness-key '<witness vkey>' --quorum 1 --max-age 3600 --origin log.example/msgs
uv run aics-tlog verify-inclusion --leaf-line 2 --leaves-file leaves.txt \
--index 1 --size 3 --root '<base64 root>' --proof proof.b64
uv run aics-tlog verify-consistency --old-size 2 --new-size 3 \
--old-root '<base64>' --new-root '<base64>' --proof proof.b64
# build tiles in one pass, or append to a tile directory incrementally
uv run aics-tlog build-tiles --leaves leaves.txt --out ./tiles
uv run aics-tlog append-tiles --out ./tiles --size 3 --leaves more.txt
Proof files carry one 32-byte hash per line (base64, or 64 hex chars);
- reads stdin. Every subcommand prints exactly one JSON object and exits
0 on success, 1 on failure.
Tests
cd packages/aics-tlog
uv run pytest -q # property tests, spec vectors, local HTTP mirrors, live sumdb check
uv run aics-tlog --help
The property tests cross-check the tile-based root against an independent
recursive RFC 6962 reference for every size 1..600, verify every inclusion
proof for sampled sizes, consistency proofs for sampled old<=new pairs,
and require any single-bit tamper in proof/leaf/root/tiles to fail. The
incremental writer is checked against a full rebuild after every append for
random batch sequences (with restarts, simulated crashes and both pruning
modes, up to level 2 tiles), and never rewrites a path. Proofs from every
reader (rebuild, incremental, authenticated) are compared with the in-memory
tree for random (index, size) and (old, new). HTTP tests run against local
servers that fail, redirect, corrupt, compress or oversize responses. The
network test verifies https://sum.golang.org/latest against the real
sum.golang.org+033de0ae key and skips when offline.
Metadata
Release files for aics-tlog 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aics_tlog-0.2.0.tar.gz | 59.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aics_tlog-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 128.1 kB
Release files / aics_tlog-0.2.0.tar.gz
| Download URL | aics_tlog-0.2.0.tar.gz |
|---|---|
| Size | 59.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ef18a316e8f658810fc33d213a4259331cd09b51353350da94e59b51125c979a
|
|
BLAKE2b-256 checksum How to use checksums |
23e48f8186446a7104b7b85b7f18c6bf2b5c1cba3f094f5d07aab2aa4eac5ae7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / aics_tlog-0.2.0-py3-none-any.whl
| Download URL | aics_tlog-0.2.0-py3-none-any.whl |
|---|---|
| Size | 68.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c5db69f451191b46165abc16034d091226896a3459bb60d900a728f99209bd6d
|
|
BLAKE2b-256 checksum How to use checksums |
ea1be2bbad4ecbeb1761363f0231f0e7b4a1214e965d3d8b151ef26842508c5e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency log