Skip to content
TrustBeatPublic

About

Verifiable, append-only transparency log (RFC 9162): inclusion & consistency proofs, signed checkpoints, witnessing, offline proof bundles

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

merklon

A verifiable, append-only transparency log — a tamper-evident Merkle log with inclusion and consistency proofs that anyone can verify without trusting the server.

CI License Scala 3 RFC 9162 Maven Central Anchored — qualified EU timestamp

Certificate-Transparency-style verifiable log for the JVM: Merkle tree proofs (RFC 9162 / RFC 6962), signed checkpoints, N-of-M witness co-signing, RFC 3161 qualified timestamps, and offline-verifiable proof bundles — in pure Scala 3.

New to transparency logs? Start with the plain-language overview.


Why

Most "audit logs" are searchable, not provable. A transparency log gives you a cryptographic guarantee that the log is append-only — entries can be added but history can never be silently rewritten — and lets independent parties verify that guarantee for themselves.

The design follows the model proven by Certificate Transparency (RFC 9162, which obsoletes RFC 6962), Sigstore's Rekor, and the Go checksum database:

  • Inclusion proofs — prove a specific entry is in the log.
  • Consistency proofs — prove the log at size N₁ is a prefix of the log at size N₂ (nothing was reordered or removed).
  • Signed checkpoints — the log periodically signs (tree_size, root_hash).
  • Witnessing — independent witnesses co-sign checkpoints to detect a log that shows different histories to different clients (split-view / equivocation).
  • Offline proof bundles — a single self-contained document (entry, inclusion proof, signed+cosigned checkpoint, optional RFC 3161 timestamp) that verifies with no network.

Don't trust — verify

Correctness is the product. merklon ships an independent verifier (library + CLI) that checks every proof against the math, and a test suite pinned to the canonical RFC 6962 reference vectors (unchanged under RFC 9162). If the verifier disagrees with the server, the server is wrong.

Status

v0.1.0 — the full Layer-1 log is complete: Merkle core, Postgres persistence, Ed25519-signed checkpoints, HTTP serving, N-of-M witnessing (split-view detection), RFC 3161-sealed offline proof bundles, and a standalone independent verifier.

Quickstart (Java)

merklon is on Maven Central and directly usable from plain Java — the merklon-java facade exposes the core with java.util types only (no Scala type in any public signature; the Scala runtime rides along as two ordinary transitive jars). Requires JDK 17+.

Maven:

<dependency>
  <groupId>eu.trustbeat</groupId>
  <artifactId>merklon-java_3</artifactId>
  <version>0.1.0</version>
</dependency>

Gradle:

implementation 'eu.trustbeat:merklon-java_3:0.1.0'

Build a Merkle tree over your entries, prove one entry is in it, then append and prove the grown log still contains the old one unchanged:

import merklon.javadsl.Merkle;
import java.util.ArrayList;
import java.util.List;

List<byte[]> entries = new ArrayList<>(List.of(
    "user=42 action=delete".getBytes(),
    "user=7  action=login".getBytes(),
    "user=42 action=export".getBytes()));

byte[] root = Merkle.root(entries);
System.out.println("root: " + Merkle.toHex(root));

// Inclusion proof: entry 1 really is in the tree with this root.
List<byte[]> incl = Merkle.inclusionProof(1, entries);
boolean present = Merkle.verifyInclusion(
    1, entries.size(), Merkle.leafHash(entries.get(1)), incl, root);

// Consistency proof: after appending, the old log is a prefix of the new one —
// nothing was rewritten, reordered, or removed.
int oldSize = entries.size();
entries.add("user=9  action=login".getBytes());
byte[] newRoot = Merkle.root(entries);
List<byte[]> cons = Merkle.consistencyProof(oldSize, entries);
boolean appendOnly = Merkle.verifyConsistency(
    oldSize, entries.size(), root, newRoot, cons);

System.out.println("included: " + present + ", append-only: " + appendOnly);

merklon.javadsl.Checkpoints parses and verifies the log's signed checkpoint notes the same way.

Full walkthrough: Getting started from Java — embed the core, verify a live log's signed checkpoints, run the whole stack locally, and check the published jars against the RFC test vectors yourself.

Scala

libraryDependencies += "eu.trustbeat" %% "merklon-core"     % "0.1.0"
libraryDependencies += "eu.trustbeat" %% "merklon-verifier" % "0.1.0"  // independent verifier
import merklon.MerkleTree

val entries = List("a", "b", "c").map(_.getBytes("UTF-8"))
val root    = MerkleTree.root(entries)
println(MerkleTree.toHex(root))

Run the log server and append an entry:

sbt "server/runMain merklon.server.Main"   # ZIO HTTP log server (see modules/server for env vars)

curl -s -XPOST localhost:8080/entries --data-binary 'hello'   # → {"leaf_index":0,"tree_size":1}
curl -s localhost:8080/checkpoint                             # the signed (size, root) note

Verify independently — build the standalone verifier, then check a proof without trusting the server (the CLI shares none of the server's state):

sbt verifier/assembly   # → modules/verifier/target/scala-3.3.4/merklon-verify.jar

java -jar merklon-verify.jar --pubkey <LOG_PUBKEY_HEX> --url http://localhost:8080 \
  inclusion 68656c6c6f          # hex("hello") — index is looked up by leaf hash

merklon-verify also checks checkpoint signatures, consistency proofs, witness cosignature policies (--witness NAME=HEX --witness-threshold N), fully offline proof bundles (bundle FILE [--tsa-cert PEM]), and standard c2sp.org/tlog-proof documents (tlog-proof FILE DATA_HEX). Run it with no arguments for usage.

sbt test        # runs the full suite, including the RFC 6962 vector checks

Roadmap

All Layer-1 phases are complete as of v0.1.0:

  • Phase 0 — Merkle core (done): leaf/node hashing, Merkle Tree Hash, inclusion + consistency proofs, RFC 6962 test vectors.
  • Phase 1 — Persistence + checkpoints (done): Postgres storage backend, Ed25519-signed checkpoints, durable log key, sequencer with timed batching.
  • Phase 2 — Serving + verifier (done): ZIO HTTP API + standalone CLI verifier.
  • Phase 3 — Witnessing (done): c2sp tlog-witness service, N-of-M co-signing (cosignature/v1), split-view detection.
  • Phase 4 — Pluggable attestation (done): RFC 3161 qualified timestamps and self-contained, offline-verifiable proof bundles.

Next — post-quantum signatures: ML-DSA-44 cosignatures (the c2sp 0x06 type), planned for when the JDK 25 LTS becomes the project baseline — the JDK ships ML-DSA natively, so the core stays dependency-free. The SHA-256 Merkle proofs themselves are already post-quantum safe; see the post-quantum posture section in DESIGN.md.

Design

Hash-agnostic, dependency-light core with clean extension points (CheckpointAttestor, LeafCodec, StorageBackend, AppendAuthorizer) so the log can be embedded in different contexts without forking.

  • Overview — how the whole thing works, in plain language.
  • Design — architecture, actors, extension points, PQ posture.
  • Spec — wire formats: checkpoint note, proofs, bundles, HTTP API.

About

merklon is built and maintained by Trustbeat, makers of European timestamping and log-anchoring infrastructure. Proof bundles already carry RFC 3161 timestamps — if you need legally valid, eIDAS-qualified timestamps on your checkpoints and evidence, see trustbeat.eu.

Security

Found a vulnerability? Please follow SECURITY.md — do not open a public issue for security reports.

Contributing

See CONTRIBUTING.md. Contributions require a Developer Certificate of Origin sign-off (git commit -s).

License

Apache License 2.0. Copyright Trustbeat s.r.o.

About

Verifiable, append-only transparency log (RFC 9162): inclusion & consistency proofs, signed checkpoints, witnessing, offline proof bundles

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages