Skip to the content.

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:

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:

What it does not establish:

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.