The Hiking World

Verify a trail record

Each published trail is signed with Ed25519. A valid signature means the facts, sources, and verified dates in the canonical payload were published by The Hiking World and have not changed since signing. An unverified figure stays unverified. The signature does not upgrade it.

What you need

  1. The trail response from /api/v1/trails/<slug>, including its provenance block.
  2. The public key at /.well-known/provenance-key.json.
  3. The same canonical JSON rules: object keys sorted, undefined fields omitted.

Worked record: gr20. Rebuild the payload from data.distanceKm, data.ascentM, data.descentM, data.highPointM, data.lowPointM, the permit rows, geometrySource, and gpsTrack. catalogVersion is the response version.

Node

import { createHash, createPublicKey, verify } from "node:crypto";
import { stableStringify } from "./canonicalize"; // sorted keys, drop undefined

const record = await fetch("https://www.thehikingworld.com/api/v1/trails/gr20").then((r) => r.json());
const key = await fetch("https://www.thehikingworld.com/.well-known/provenance-key.json").then((r) => r.json());
const data = record.data;
const payload = {
  slug: data.slug,
  name: data.name,
  country: data.country || null,
  catalogVersion: record.version,
  facts: {
    lengthKm: data.distanceKm,
    ascentM: data.ascentM,
    descentM: data.descentM,
    highPointM: data.highPointM,
    lowPointM: data.lowPointM,
  },
  permits: data.permits.map((row) => ({
    permitType: row.permitType,
    costLocal: row.costLocal,
    issuer: row.issuer,
    sourceUrl: row.sourceUrl,
    verifiedDate: row.verifiedDate,
    status: row.status,
    stale: row.stale,
  })),
  geometrySource: data.geometrySource,
  gpsTrack: data.gpsTrack,
};
const bytes = Buffer.from(stableStringify(payload), "utf8");
const hash = createHash("sha256").update(bytes).digest("hex");
const ok = verify(null, bytes, createPublicKey(key.publicKeyPem), Buffer.from(record.provenance.signature, "base64"));
console.log(ok, hash === record.provenance.canonicalSha256);

In the browser

This check uses the published public key in your browser. It does not ask this server whether the record is valid.