An open, federated protocol for cryptographically verifiable provenance and supply chain evidence.
Website: https://openwaymark.org/ · License: Apache-2.0 (libraries) and AGPL-3.0-only (servers) · Status: early development, the format is not yet stable
For any good — an egg, a diamond, a battery cell — OpenWaymark answers four questions in a way that can be checked rather than believed:
- Where does it come from?
- Which stations has it passed through?
- Who claimed that, and how well verified is that person or body?
- Was the cold chain kept, is it certified organic, is it conflict-free?
The answer consists of signed entries in append-only logs, of proofs a client recomputes for itself, and of the ability to contradict a node that lies.
- What OpenWaymark is — and what it is not
- Try it in five minutes
- How it works
- Running your own node
- Repository layout
- Status and roadmap
- Documentation
- Security
- Contributing
- Releases and versioning
- License
| Federated | Every node is authoritative for its own data. There is no global state everybody has to agree on. Comparable to email or DNS, not to a blockchain. |
| A transparency log, not a blockchain | Every node keeps a local, append-only Merkle log modelled on Certificate Transparency (RFC 6962). Tamper evidence comes from signed tree snapshots and mutual observation, not from consensus. |
| Erasable | Erasure under the GDPR is a core requirement, not an afterthought. The log stores only a salted commitment; payload and salt live off-chain and can genuinely be deleted — without a single historical proof becoming invalid. |
| Post-quantum from day one | ML-DSA (FIPS 204) and ML-KEM (FIPS 203) exclusively. No RSA, no ECC, no hybrid transition scheme. |
| Industry-agnostic | The core knows no industry schema. What may appear in a payload is laid down by an interchangeable schema profile; the first one is food.v1. |
| Not a financial product | No tradable cryptocurrency, no token with a market price, no exchange. The planned incentive scheme is a closed, non-tradable deposit system. |
| Not a proof of truth | OpenWaymark cannot stop anyone from lying at the point of first capture. It makes the lie tamper-evidently documented after the fact, attributable and economically unattractive. See the threat model. |
Requires Go 1.25 or newer. Nothing else — no Docker, no outbound network, no database to set up.
git clone https://gitlab.jens-schendel.com/jagottsicher/openwaymark.git openwaymark
cd openwaymark
go run ./demoThe demonstration builds owmnode, starts the node in a throwaway directory on two free ports on
127.0.0.1, plays through a complete food chain and cleans everything up afterwards. The client
takes the node's word for nothing: it recomputes every identifier and checks every signature,
every commitment and every proof itself.
5. Reading the chain back: the client checks everything itself
#7 handover Molkerei Alpenrand -> Feinkost Brunner e.K. (DA-2026-08-11-0093)
assertion, logged 05:45:09, proof 3 nodes
#6 processing pasteurise and add rennet -> Mountain cheese, 12 months, by ...
assertion, logged 05:45:09, proof 3 nodes
ok 8 entries: signature, commitment and inclusion proof verified
ok public keys fetched over the public API
6. Cold chain: what the sensor says against what was promised
promised on the freight papers: 2.0 to 6.0 C (Spedition Kühlfracht)
09:35 7.9 C BREACH
10:05 8.4 C BREACH
blocked 2 of 7 readings outside the promised range
7. Erasure under Art. 17 GDPR
ok payload and salt erased, tombstone appended
blocked payload no longer retrievable: HTTP 410 erased
ok the proof issued before the erasure still verifies, the tree is unchanged
ok consistency proof 8 -> 9: appended only, nothing rewritten
8. Tampering attempts
blocked one byte flipped in the entry -> signature invalid
blocked altered leaf against the same proof -> does not match the root
blocked STH signed with a foreign key -> signature does not match the node
blocked split view detected: owm/log: split view: two roots for the same tree size
Nine sections, explained one by one in demo/README.md.
Whoever claims something writes an entry: serialised deterministically as CBOR
(RFC 8949 §4.2), signed with ML-DSA, addressed by the hash of its content. An entry names its
subject (the good), its issuer, its profile, its parent entries — and, instead of the payload,
only the payload's salted commitment H(salt ‖ payload).
From that the node appends a leaf to its Merkle log. Over the tree it periodically issues a Signed Tree Head: log identifier, tree size, root hash, timestamp, signed. Two proofs follow from the tree, and every client can recompute both for itself:
- Inclusion proof — this leaf sits in exactly this tree.
- Consistency proof — the new tree is the old one plus additions; nothing was rewritten.
Personal data is never in the leaf, only ever in the off-chain blob. An erasure removes the payload and the salt and appends a tombstone. The tree stays unchanged — which is why every STH and every inclusion proof ever issued remains valid unchanged. Without the salt the payload cannot be reconstructed even if its value range is small and the attacker guesses the plaintext; that is the difference from a bare hash.
The historical counter-example is in CLAUDE.md §2: the old SKS keyserver network, append-only without any way to delete, rendered practically unusable in 2019 by poisoned entries.
There is no double-spending problem, and therefore no reason for an expensive consensus mechanism. Every node is responsible for its own participants and accepts entries only from keys in its own directory. Nodes are found over DNS:
_openwaymark.example.com. IN TXT "v=owm1; node=https://provenance.example.com"
The central attack on a log like this is the split view: a node shows two observers two
different trees of the same size. Both signatures are valid — it can only be noticed by someone
who sees both. Two things are planned against it: targeted gossip between actual supply chain
partners, and STH gossip to independent monitors (monitor/, not built yet). The
detection logic itself already exists in log.CheckSTHPair.
The core knows no industry. A profile lays down by JSON Schema what may appear in a payload, and
is referenced through the profile identifier in the entry. The first profile, food.v1, mirrors
the events of GS1 EPCIS 2.0 — production, aggregation, transport, measurement, processing,
handover — so that industry can connect without a translation layer.
A profile version never changes: were food.v1 different today from yesterday, an entry from
yesterday would be invalid today without anyone having touched it. Changes appear as food.v2.
| Signatures (nodes, entities) | ML-DSA-65 — 1952 B public key, 3309 B signature |
| Signatures (sensors, bulk entries) | ML-DSA-44 — 1312 B public key, 2420 B signature |
| Encryption (planned, E5) | ML-KEM via crypto/mlkem from the standard library |
| Hash | SHA-256, everywhere with domain separation (OWM/1 entry, OWM/1 commit, …) |
| Serialisation | deterministic CBOR, RFC 8949 §4.2 |
Size is not a side issue but a design constraint: an entry in the demonstration weighs 3407 bytes on average, its payload 492 bytes. The lion's share is the signature — which is why sensors use ML-DSA-44 and why batch signing is planned.
go build -o owmnode ./node/cmd/owmnode
./owmnode init -config owm.json \
-operator "Hof Sonnenblick" -contact privacy@example.com \
-base-url https://provenance.example.com
./owmnode show -config owm.json
./owmnode serve -config owm.jsoninit creates configuration and identity and never overwrites an existing one — overwriting
an identity would mean continuing the log under a new identifier, and every STH issued so far
would be from a key nobody has any more.
The node opens two interfaces, both bound to 127.0.0.1 by default:
| Default | For whom | |
|---|---|---|
Public API /owm/v1 |
127.0.0.1:8480 |
the world, behind a TLS-terminating reverse proxy |
Administration /admin/v1 |
127.0.0.1:8481 |
the operator, and nobody else |
Day-to-day operation — enrolling keys, erasing payloads, issuing STHs — goes through the administration interface rather than through further subcommands: two processes on the same SQLite file would be a fine way to take the database apart.
Storage is modernc.org/sqlite, pure Go without cgo. That allows binaries for ARM to be built
without a cross toolchain — the precondition for a node genuinely being operable on
Raspberry-Pi-class hardware.
Full interface description: OWM-7.
A single Go module openwaymark.org/owm with subpackages. It will be split up only once somebody
wants to pull in core/ on its own.
| Directory | Content | License |
|---|---|---|
spec/ |
protocol specification, normative | Apache-2.0 |
core/ |
entry types, deterministic CBOR, ML-DSA, commitments | Apache-2.0 |
log/ |
Merkle log, STH, inclusion and consistency proofs, erasure path | Apache-2.0 |
profiles/ |
schema profiles, starting with food/ |
Apache-2.0 |
node/ |
node server and owmnode |
AGPL-3.0-only |
monitor/ |
independent log monitor (planned) | AGPL-3.0-only |
client/ |
WASM verifier and web app (planned) | Apache-2.0 |
demo/ |
end-to-end demonstration against a real node | Apache-2.0 |
testdata/ |
test vectors for third-party implementations | Apache-2.0 |
The test vectors are part of the specification, not mere test scaffolding: anyone implementing OpenWaymark in another language checks themselves against them.
Early development. The protocol is not stable yet, the data format may change. No version so far is meant for production use.
| Stage | Content | Status |
|---|---|---|
| E0 | foundation, specification drafts, CI | done |
| E1 | core data model, cryptography, test vectors | done |
| E2 | Merkle log, STH, proofs, erasure path | done |
| E3 | node server, HTTP API, profile food.v1 |
done |
| E4 | federation: DNS discovery, gossip, monitor/ |
next |
| E5 | trust levels, attestations, sensor certificates | open |
| E6 | web app and WASM verifier | open |
| E7/E8 | deposit system and dispute resolution | deliberately deferred |
E7/E8 wait until at least two independently operated nodes carry real data. Only then can cap heights and time windows be calibrated against measurements instead of guessed. The core is deliberately built so that it does not depend on them.
| Document | Content |
|---|---|
| OWM-0 | protocol overview, terms, identifiers, crypto parameters, discovery |
| OWM-2 | log, Merkle tree, signed tree heads, proofs, erasure path |
| OWM-3 | keys, node identity, directory, rotation |
| OWM-4 | profile mechanism and the food profile food.v1 |
| OWM-7 | node API: submitting, reading, proofs, administration |
| OWM-9 | threat model, limits of the system |
| CLAUDE.md | concept document: fundamental decisions with reasoning, rejected approaches |
Package-level explanations live in the README files of the directories
(node/, profiles/, demo/) and in
the comments; the normative version is always the specification.
The threat model describes what OpenWaymark protects against and what it expressly does not. The three most important limits:
- The oracle problem remains. Whoever lies at the point of first capture is not made honest by any signature. The protocol makes the lie attributable and tamper-evidently documented after the fact — it does not replace spot-check physical audits.
- A split view is only noticed if somebody looks. As long as no monitor runs, a node can sign two truths. That is why E4 is the next stage.
- The administration interface is unprotected. It does not belong on the open internet.
Please do not report security vulnerabilities as public issues, but confidentially through the repository's private security reporting (on GitHub: Security tab → Report a vulnerability). Until a fix exists, neither the report nor the reporter is mentioned; afterwards both are named in the release notes, if desired.
Contributions are welcome — bug reports, criticism of the specification, code, profiles for further industries, implementations in other languages.
Go 1.25 or newer (CIRCL requires it). Nothing else.
go build ./...
test -z "$(gofmt -l .)"
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...
go test -race ./...
go run ./demoThat is exactly what the pipeline checks too (.gitlab-ci.yml), plus a short
fuzz run over the parsers. Parsing is the only place where foreign bytes enter the system —
whoever changes anything there should let the fuzzer run for longer:
go test ./core/ -run '^$' -fuzz FuzzParseEntry -fuzztime 5m- Specification first. Every change to the wire format, to identifiers, to domain separators
or to the API changes the specification under
spec/first and the code second. A format that exists only in the code is not a protocol. - Update the test vectors. If the serialisation changes, the golden data in
testdata/vectorsis regenerated:go test ./core/ -update. The diff belongs in the same commit and is the real touchstone — it shows whether the change makes old data unreadable. - Never put personal data in the leaf in the clear. Everything personal lives in the off-chain blob. That rule is in the specification, not just in the code.
- Profile versions are immutable. Changes to
food.v1are not changes butfood.v2. - An SPDX header in every new file, matching the area (see License). The
repository follows the REUSE specification;
REUSE.tomlcovers the exceptions. - No keys, no operational data in the repository.
.gitignorecovers the usual cases; it does not replace thinking.
Everything in this repository is in English: specification, README files, source comments, commit messages, program output and error messages.
Program output is additionally plain text: no ANSI colours, no boxes, no emoji, no typographic
arrows — the output should copy unchanged into a file, a ticket or a mail and look the same in
every terminal. Error texts start with the package prefix (owm:, owm/log:, owm/node:,
owm/profiles:) and continue in lower case — as staticcheck requires (ST1005), and so that they
nest.
Proper nouns in test and demonstration data stay as they are: the fictional companies and farms
have German names, and identifiers from the real world (for instance the organic control number
DE-ÖKO-006) are quoted, not translated.
- Open an issue before starting larger work — especially for protocol changes.
- Branch from
main, one topic per branch. - Commit subject as a short declarative sentence, the why in the body, not the what (that is in the diff).
- Merge/pull request with a green pipeline.
Two things are versioned separately:
- The protocol carries its version in the wire format and in the domain separators (
OWM/1). It only rises when the format breaks. - The software follows SemVer. As long as the major version is
0, anything can change in any minor version — the wire format included.
A release is an annotated Git tag vX.Y.Z on main with a green pipeline. The release notes name,
in this order:
- Format changes — what is no longer backwards compatible in entries, leaves, STHs or the API, and what operators of existing logs have to do.
- Security-relevant matters.
- Everything else.
Builds are made from the tag:
git clone --branch v0.1.0 https://gitlab.jens-schendel.com/jagottsicher/openwaymark.git openwaymark
cd openwaymark
CGO_ENABLED=0 go build -trimpath -o owmnode ./node/cmd/owmnodeowmnode version prints commit and Go version, because go build embeds both from version
control. Binaries for linux/amd64 and linux/arm64 are attached to the release — without cgo,
so that a Raspberry Pi runs the same file a server runs.
A release always contains the matching test vectors. Third-party implementations check themselves
against testdata/vectors of exactly that tag.
Split licensing, so that the libraries stay freely embeddable and server operators give their changes back:
| Area | License |
|---|---|
spec/, core/, log/, client/, profiles/, testdata/, demo/ |
Apache-2.0 |
node/, monitor/ |
AGPL-3.0-only |
Whoever operates a node as a service gives their changes back to the network they run it in.
Whoever merely embeds core/ or log/ in their own software — a client, a scanner, a
third-party implementation — is unaffected by that.
Every file carries an SPDX identifier; the full license texts are in LICENSES/. The
repository follows the REUSE specification.