hash-attestation
Sign and verify Kinetic Gain Protocol Suite documents using ed25519 signatures over the same canonical-hash convention every other Suite repo already uses (sha256:<hex> over sorted-keys, no-whitespace JSON).
The missing "this AEO actually came from the vendor" layer.
use ;
use SigningKey;
use OsRng;
let key = generate;
let attestor = new;
let body = json!;
let signed: Attestation = attestor.sign?;
assert!;
# Ok::
Why
Today a consumer fetches an AEO doc (or agent-card, or decision-card) over HTTPS and trusts the bytes came from the published origin. That covers typo-grade tampering and not much else: a misconfigured CDN, a route hijack, a developer with write access who shouldn't have had it — none of them are visible to the consumer.
This crate adds a detached signature layer:
- The vendor signs the canonical hash with an ed25519 private key.
- The signature + key URL ride alongside the doc (or inline in it).
- The vendor publishes the matching public key at a well-known URL.
- The consumer fetches the doc, recomputes the hash, fetches the public key, and verifies.
The signature commits to the canonical hash, not the bytes the consumer received. So whitespace, key ordering, and CDN re-encoding don't break verification — but a single character change inside any field does.
What's in the box
| Type | Purpose |
|---|---|
canonical_hash |
sha256:<hex> over canonical JSON. Identical convention to procurement-decision-api + aeo-validator-service — same input bytes, same hash, across the portfolio. |
Attestor |
Wraps a SigningKey with the public key URL so every produced Attestation is self-describing. |
Attestation |
Serde-serialisable envelope: algorithm, signed_hash, signature (base64 ed25519), key_url, signed_at. Drop it next to the doc as <doc>.sig.json or fold it inline. |
Verifier |
A trust set — key_url -> VerifyingKey. Register keys up-front, verify by URL lookup. |
End-to-end shape
vendor side consumer side
----------- -------------
SigningKey Verifier (trust set)
│ │
▼ ▼
Attestor::new(key, key_url) Verifier::trust(key_url, public_key)
│ ▲
▼ │
.sign(doc) → Attestation ───── published ─────► .verify(attestation, doc)
returns Ok or AttestationError
When a Verifier::verify call returns:
Ok(())— the doc is unmodified vs. the moment the vendor signed it AND the signature checks out against the trusted public key.Err(HashMismatch { … })— the doc has changed since it was signed.Err(BadSignature)— the signature doesn't match the key.Err(UntrustedKey(…))— thekey_urlin the attestation isn't in your trust set.Err(UnsupportedAlgorithm(…))— v0.1 only knows ed25519.
Composes with
- aeo-validator-service — verifies the attestation alongside drift; tamper events surface as a structured issue.
- procurement-decision-api — every Decision Card can be paired with a signature so downstream policy bundles can prove provenance.
- aeo-graph-explorer-rs — same canonical-hash convention means the explorer's
content_hashfield is what this crate signs. - incident-correlation-rs — if an
IncidentCardflags "we don't trust this vendor's AEO anymore", removing the vendor'skey_urlfrom the verifier is one atomic update away.
Algorithm note
v0.1 is ed25519-only. The algorithm field is included on every attestation so a future v0.2 can add (e.g.) ECDSA-P256 without breaking existing verifiers. Unknown algorithms fail closed.
Bench
Bundled bench measures sign and verify separately so you can spot regressions in either path.
Tests
CI matrix: stable, beta, 1.86.0 (MSRV).
License
MIT. See LICENSE.