Geometry Integrity Hash — Verification Specification
Owner: Ezinna Ohah (Founder) Version: 1.2 Effective: 19 September 2026 Next review: 30 January 2027 Classification: Public Status: Adopted
Specification version: agriops-geohash-1
Applies to: AgriOps EUDR Compliance Certificates and Supply Chain Traceability Certificates
This document may be shared with buyers, auditors and regulators without restriction.
What this document is for
Every AgriOps certificate prints a Geometry Integrity Hash — a single SHA-256 digest that anchors the GPS boundaries of every farm in the batch. The certificate states that altering any recorded boundary changes this digest and invalidates the document.
This page specifies exactly how that digest is computed, so that a buyer, auditor, or regulator can recompute it independently and confirm the claim rather than take our word for it.
A hash is only meaningful if the person reading it can reproduce it. That requires three things: the algorithm (below), the inputs (see Obtaining the inputs), and a record of when the digest was made (see Time).
Recomputing it yourself
A reference verifier is published alongside this specification:
github.com/Sirleroy/agriops-geohash
verify.py is a single file, Python 3.8 or later, standard library only. There is
nothing to install, and it is short enough to read in full before you run it.
python3 verify.py --self-test
python3 verify.py bundle.json --certificate-root <root printed on the certificate>
--self-test recomputes the worked example in §3 and compares it against the
digests printed there, so you can confirm the tool implements this document
before trusting it on a real bundle.
Passing --certificate-root is what makes the check meaningful. Verifying a
bundle against the root stated inside that same bundle is circular, and a
fabricated bundle passes it trivially. The check that establishes something is
the root recomputed from the bundle against the root printed on the certificate
you were sent separately.
You are not required to use it. The algorithm below is complete in itself, and an implementation you write from it is a stronger check than one we supply — a verifier from the party being verified proves less than your own.
1. Per-farm digest
Each farm boundary is stored as a GeoJSON geometry object. Its digest is:
digest = SHA-256( canonical_json( geometry ) )
Where canonical_json is JSON serialisation with:
| Rule | Value |
|---|---|
| Key ordering | Lexicographic (sort_keys=True) |
| Separators | , and : — no whitespace |
| Encoding | UTF-8 |
| Coordinate precision | As stored — 6 decimal places after normalisation |
| Digest output | Lowercase hexadecimal, 64 characters |
In Python this is exactly:
import hashlib, json
def farm_digest(geometry: dict) -> str:
canonical = json.dumps(geometry, sort_keys=True, separators=(',', ':'))
return hashlib.sha256(canonical.encode('utf-8')).hexdigest()
The object hashed is the geometry itself ({"type": ..., "coordinates": ...}),
not a GeoJSON Feature wrapper and not any surrounding metadata. No
properties, identifiers, or timestamps are included — only the shape.
2. Batch root hash
The value printed on the certificate is the root over every mapped farm in the batch:
root = SHA-256( concat( digest₁, digest₂, …, digestₙ ) )
Ordering and membership rules:
- Farms are sorted by ascending AgriOps farm ID.
- Only farms with both a stored boundary and a digest are included. A farm with no recorded polygon contributes nothing to the root.
- Digests are concatenated as their 64-character lowercase hex strings, with no separator, and the resulting string is UTF-8 encoded before hashing.
def batch_root_hash(digests_in_farm_id_order: list[str]) -> str:
return hashlib.sha256(''.join(digests_in_farm_id_order).encode('utf-8')).hexdigest()
Concatenating fixed-width 64-character digests is unambiguous, so the boundary between any two inputs is unique and the composition is collision-safe.
3. Worked example
Two farms, given here as they would appear in a verification bundle.
Farm A (lower ID, so it sorts first):
{"type":"Polygon","coordinates":[[[8.9,9.9],[8.91,9.9],[8.91,9.91],[8.9,9.91],[8.9,9.9]]]}
Canonical form:
{"coordinates":[[[8.9,9.9],[8.91,9.9],[8.91,9.91],[8.9,9.91],[8.9,9.9]]],"type":"Polygon"}
Digest:
f24cc617f23ac479519a50dd5908e70d964bad9eb883b9c445d14fdaa3568760
Farm B:
{"type":"Polygon","coordinates":[[[7.5,10.2],[7.51,10.2],[7.51,10.21],[7.5,10.21],[7.5,10.2]]]}
Digest:
65edbe89e0502e5f7fed758c5891034379bcd8d03e6ebe9096f56f074ec2e9ea
Root — note that the canonical form reorders coordinates before type,
which is what makes the digest reproducible across languages:
e0ba2fb6e018f6c7edee7f6a5844e205c665b1291dde4c8a707f8943e5d1f421
Complete reproduction:
import hashlib, json
farms = [
{"type":"Polygon","coordinates":[[[8.9,9.9],[8.91,9.9],[8.91,9.91],[8.9,9.91],[8.9,9.9]]]},
{"type":"Polygon","coordinates":[[[7.5,10.2],[7.51,10.2],[7.51,10.21],[7.5,10.21],[7.5,10.2]]]},
]
digests = [
hashlib.sha256(
json.dumps(g, sort_keys=True, separators=(',', ':')).encode('utf-8')
).hexdigest()
for g in farms
]
root = hashlib.sha256(''.join(digests).encode('utf-8')).hexdigest()
print(root) # e0ba2fb6e018f6c7edee7f6a5844e205c665b1291dde4c8a707f8943e5d1f421
4. Obtaining the inputs
The certificate itself deliberately prints centroids rather than full boundaries. Farm polygons are the commercially sensitive property of the producer, and under our data-protection model the producing cooperative is the data controller — AgriOps processes that data on their instruction and does not publish it unilaterally.
To verify a specific certificate, request a verification bundle from the
issuing company. It is a machine-readable JSON document containing, for every
farm in the batch: a stable reference, its digest, and — at the full
verification level — the exact geometry that was hashed, together with the
ordered list, the root, and the specification version. Releasing it is the
producer’s decision, not ours.
The issuing company downloads it from the batch page of their AgriOps account,
against the certificate reference printed on the document (AO-000123). The
bundle is built from the issuance snapshot, not from live records, so a boundary
corrected after issuance does not change what a past certificate is shown to
have asserted.
Verification levels
The bundle declares its own level in the verification_level field:
| Level | Contains | Lets you establish |
|---|---|---|
full |
Digests and geometry | Both checks below: each digest matches its shape, and the root matches the digests |
digests_only |
Digests, no geometry | Only that the root is correctly composed from the listed digests in the listed order |
A digests_only bundle is the default where boundaries have not been released.
It is a genuine but weaker check: it confirms the document is internally
consistent and that its root matches the certificate, and it cannot confirm that
any digest corresponds to a real parcel. Ask the issuer for a full bundle if
you need the stronger claim.
Order is part of the claim. The root hashes concatenated digests, so a
reordered farm list produces a different root. The bundle states its ordering
rule in farm_order; preserve it if you reserialise the document.
If a bundle you have been given does not reproduce the root printed on the certificate, treat the document as unverified and raise it with the issuer.
The reference verifier performs both checks; see Recomputing it yourself above.
5. Time
A digest proves that a given set of bytes produces a given value. It does not, by itself, prove when those bytes existed — a hash can be computed at any moment, over anything.
AgriOps records an immutable issuance snapshot at the moment each certificate is generated, capturing the root, every farm digest, the geometry hashed, the compliance verdict applied, and the issuing timestamp and user. That snapshot is the authoritative record of what was asserted and when, and it is retained independently of ordinary audit-log retention.
Cryptographic time-stamping (a detached signature over the bundle, and an RFC 3161 timestamp authority) is specified in ADR 016 and is not yet in effect. Until it is, the issuance timestamp is an AgriOps-held record rather than an independently verifiable one, and this page will state so plainly for as long as that remains true.
6. Scope and limits
What a matching root hash does establish:
- The boundaries in the bundle are byte-identical to those AgriOps held when the certificate was issued.
- No farm has been added to, or removed from, the batch since issuance.
- No coordinate has been altered, however slightly.
What it does not establish:
- That the boundary corresponds to the physical parcel it claims to. That rests on field capture procedure and, where applicable, GPS provenance evidence.
- That the commodity in a shipment physically originated within those boundaries. That rests on the procurement and batch records the certificate summarises.
- Anything about the accuracy or precision of the original GPS capture.
The hash is a tamper-evidence mechanism over recorded geometry. It is one component of a due-diligence file, not a substitute for one.
Questions
Technical questions about this specification: founder@agriops.io.
Specification changes are versioned. Certificates carry the spec_version under
which they were issued, and historical versions of this page remain valid for
verifying certificates issued under them.
Revision history
| Version | Effective | Change |
|---|---|---|
| 1.2 | 19 Sep 2026 | Adds Recomputing it yourself: a reference verifier is now published alongside this document, and this page is served from that public repository at docs.agriops.io/hash-verification. Until today the URL printed in every verification bundle redirected to a sign-in page, so a document classified Public could not in fact be read by the buyers it names. No change to the algorithm — agriops-geohash-1 is unchanged and every certificate issued under 1.0 and 1.1 verifies identically. |
| 1.1 | 12 Aug 2026 | §4 rewritten. The verification bundle described in 1.0 is now a document the issuing company can actually produce and send; verification levels (full / digests_only) and the ordering rule are specified. No change to the algorithm — agriops-geohash-1 is unchanged and every certificate issued under 1.0 verifies identically under 1.1. |
| 1.0 | 30 Jul 2026 | Initial specification. |