Skip to content

Repository files navigation

OpenWaymark

CI develop CI main

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.

Contents

What OpenWaymark is — and what it is not

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.

Try it in five minutes

Requires Go 1.25 or newer. Nothing else — no Docker, no outbound network, no database to set up. GitLab and GitHub carry the identical history; either works.

git clone https://gitlab.jens-schendel.com/jagottsicher/openwaymark.git openwaymark
# or: git clone https://github.com/jagottsicher/OpenWaymark.git openwaymark
cd openwaymark
go run ./demo

The 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.

Try the browser verifier against a real node

go run ./demo -serve

Runs the same demonstration, then keeps the node up and prints a ready link — build the WASM verifier once (client/wasm/build.sh), open client/web/index.html, paste the link's node URL and subject in, and watch the same checks the demo just ran happen again independently, this time in the browser rather than the CLI. Details, including what the page deliberately does not trust: client/README.md, OWM-8.

How it works

Entry, leaf, tree

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.

Why erasure and proof do not contradict each other

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: the old SKS keyserver network, append-only without any way to delete, rendered practically unusable in 2019 by poisoned entries.

Federation instead of a global chain

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. Against it: targeted gossip between actual supply chain partners (built into the node itself), and STH gossip to independent monitors (monitor/). Both poll GET /owm/v1/sth through gossip/ and run the same detection primitive, log.CheckSTHPair; discovery/ resolves a partner's base URL from its domain.

Profiles

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.

Further profiles reuse the same mechanism, each interoperating with the regimes that already govern its industry rather than inventing new ones:

Profile Status Covers Concrete use case Interoperates with
food.v1 done farm to consumer cold-chain breach detection, organic certification GS1 EPCIS 2.0
pharma.v1 done starting material to dispensing counterfeit/diverted drugs, cold chain DSCSA (US), EU FMD/GDP, ICH Q7, GS1's own DSCSA↔EPCIS guideline
meddevice.v1 done implants and capital equipment (CT, MRI, X-ray) gray-market device reuse, maintenance history EU MDR/UDI/EUDAMED, FDA UDI/GUDID, IMDRF, ISO 13485
aviation.v1 done aircraft parts, back-to-birth counterfeit parts (the AOG Technics case) FAA 8130-3 / EASA Form 1, ATA Spec 2000 ch. 15/16
vehicle.v1 done used cars/motorcycles, incl. classic-car provenance odometer rollback, title washing US TIMA/NMVTIS, EU End-of-Life Vehicles Regulation
electronics.v1 done components (RAM, SSDs) to finished devices counterfeit parts, recycled-content claims IPC-1782, EU ESPR/Digital Product Passport, WEEE
minerals.v1 done ore/3TG through smelting to a manufacturer conflict-mineral due diligence EU Conflict Minerals Regulation, OECD Due Diligence Guidance, EU Critical Raw Materials Act
seafood.v1 done vessel to plate illegal, unreported and unregulated fishing EU CATCH, US Seafood Import Monitoring Program
eudr.v1 done timber, cocoa, coffee, palm oil, soy, rubber, cattle deforestation-free due diligence EU Deforestation Regulation
diamonds.v1 done mine through cutting/polishing to a retailer conflict diamonds, lab-grown fraud Kimberley Process, US FTC lab-grown disclosure
eu/battery.v1 done portable, LMT, EV, industrial, SLI batteries carbon footprint, second-life tracking EU Battery Regulation, Digital Battery Passport

All eleven profiles above are fully implemented, tested and merged into develop — none is a draft or a research note; those live as open points in the individual spec files instead. Every normative spec lives under spec/owm-4-<name>.md; each profile's own README has the details.

Trust levels and attestation

Two separate dimensions, never collapsed into one number: how verified is the entity behind a key (0, unverified, through 6, a state body itself), and how forgery-resistant is a product's physical-digital binding (a printed QR code through a PUF-backed chip). A level is never self-declared — trust/ computes it by walking attestation entries back to a locally recognised accreditation root, the same trust-anchor idea as a browser's root-CA store, kept local to each operator and never gossiped. The overall trust of a supply chain is the minimum across every participant and binding involved: one weak link drags the whole chain down to its own level. Full description: OWM-6.

Cryptography

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 (optional payload confidentiality) ML-KEM-768 via crypto/mlkem, hybrid with AES-256-GCM — seal/
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.

Running your own node

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.json

init 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

⚠️ The administration interface has no authentication, and that is deliberate. Access control belongs in the environment: local binding, a Unix socket behind a proxy, a VPN. Whoever reaches this interface can enrol keys and erase payloads. A home-grown token scheme in the application code would be weaker than what the operating system and a grown-up proxy can do anyway — and it would pretend the question had been settled.

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.

Repository layout

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 — see the table above Apache-2.0
discovery/ DNS discovery of a node's base URL and description Apache-2.0
gossip/ fetch, verify and poll STHs — the split-view detection client Apache-2.0
trust/ entity trust levels from attestation chains Apache-2.0
seal/ optional payload confidentiality (ML-KEM hybrid encryption) Apache-2.0
node/ node server and owmnode AGPL-3.0-only
monitor/ independent log monitor AGPL-3.0-only
client/ WASM verifier and web app 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.

Status and roadmap

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/ done
E5 trust levels, attestations, sensor certificates done
— ten further schema profiles across other industries (see the table above) done
E6 web app and WASM verifier done
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.

Documentation

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-5 federation: DNS discovery, gossip, the independent monitor's contract
OWM-6 trust levels, attestation entries, sensor certificates
OWM-7 node API: submitting, reading, proofs, administration
OWM-8 client and verifier: fetch-then-verify contract, CORS, addressing
OWM-9 threat model, limits of the system

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.

Security

The threat model describes what OpenWaymark protects against and what it expressly does not. The three most important limits:

  1. 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.
  2. A split view is only noticed if somebody looks. monitor/ exists now, but running one cannot be compelled, only made attractive — coverage stays uneven by construction (OWM-9 §6).
  3. 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.

Contributing

Contributions are welcome — bug reports, criticism of the specification, code, profiles for further industries, implementations in other languages.

Branching model

develop is where active work lands; main reflects what is actually released and stays the default branch, so a first-time visitor sees the current, stable state rather than work in progress. Branch a feature off develop, open the merge/pull request back into develop — not into main. main only ever receives merges from develop, at release time; on GitLab, CI enforces this directly — a merge request into main from any other source branch fails.

The full cycle, tied to what the pipeline actually does at each step:

  1. Branch off develop for anything more than a one-line fix — feature/<name> for a feature, docs/<name> for documentation-only work. A small fix can go straight to develop; nothing about the workflow requires a branch for every single commit.
  2. Merge request back into develop. Once it lands — whether through a merge request or a direct commit — a passing pipeline deploys to the dev node automatically, no button to press. That is the point of having it: check the actual change against a real, publicly reachable node before it goes anywhere near production.
  3. Merge request develop → main, once develop is where it should be. This is the only path into main there is; CI rejects any other source branch for a merge request targeting it. Landing on main by itself deploys nothing.
  4. Tag the release — git tag -a vX.Y.Z -m "…" && git push origin vX.Y.Z on main. The tag is what triggers the community node deploy, automatically, once the same checks that run on every pipeline pass on it too.

Prerequisites

Go 1.25 or newer (CIRCL requires it). Nothing else.

Get this green locally before every contribution

go build ./...
test -z "$(gofmt -l .)"
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...
go test -race ./...
go run ./demo

That 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

What to watch out for when changing things

  • 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/vectors is 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.v1 are not changes but food.v2.
  • An SPDX header in every new file, matching the area (see License). The repository follows the REUSE specification; REUSE.toml covers the exceptions.
  • No keys, no operational data in the repository. .gitignore covers the usual cases; it does not replace thinking.

Language

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.

Workflow

  1. Open an issue before starting larger work — especially for protocol changes.
  2. Branch from main, one topic per branch.
  3. Commit subject as a short declarative sentence, the why in the body, not the what (that is in the diff).
  4. Merge/pull request with a green pipeline.

Releases and versioning

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:

  1. Format changes — what is no longer backwards compatible in entries, leaves, STHs or the API, and what operators of existing logs have to do.
  2. Security-relevant matters.
  3. Everything else.

The tag also gates the community node deploy: pushing a tag runs the same checks as any other pipeline, and only once those pass does it deploy to the community node automatically — a plain push to main deploys nothing by itself, only a tag does.

Builds are made from the tag:

git clone --branch v0.1.0 https://gitlab.jens-schendel.com/jagottsicher/openwaymark.git openwaymark
# or: git clone --branch v0.1.0 https://github.com/jagottsicher/OpenWaymark.git openwaymark
cd openwaymark
CGO_ENABLED=0 go build -trimpath -o owmnode ./node/cmd/owmnode

owmnode 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.

License

Split licensing, so that the libraries stay freely embeddable and server operators give their changes back:

Area License
spec/, core/, log/, client/, profiles/, discovery/, gossip/, trust/, seal/, 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.

About

Federated, post-quantum-secure protocol for cryptographically verifiable supply chain provenance — track goods from producer to consumer without a blockchain, a token, or a central authority. Certificate Transparency-style Merkle logs, GDPR-compliant erasure, written in Go.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages