hash-attestation 0.1.1

Sign and verify Kinetic Gain Protocol Suite documents using ed25519 over canonical JSON hashes. The missing 'this AEO actually came from the vendor' layer. Optional audit-stream-py integration via the `audit-stream` feature.
Documentation

hash-attestation

CI Rust License: MIT

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 hash_attestation::{Attestation, Attestor};
use ed25519_dalek::SigningKey;
use rand_core::OsRng;

let key = SigningKey::generate(&mut OsRng);
let attestor = Attestor::new(key.clone(), "https://acme.example/keys/aeo".to_string());

let body = serde_json::json!({
    "aeo_version": "0.1",
    "entity": { "id": "https://acme.example/#org", "name": "Acme" }
});

let signed: Attestation = attestor.sign(&body)?;
assert!(signed.verify(&key.verifying_key(), &body).is_ok());
# Ok::<_, hash_attestation::AttestationError>(())

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:

  1. The vendor signs the canonical hash with an ed25519 private key.
  2. The signature + key URL ride alongside the doc (or inline in it).
  3. The vendor publishes the matching public key at a well-known URL.
  4. 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(…)) — the key_url in 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_hash field is what this crate signs.
  • incident-correlation-rs — if an IncidentCard flags "we don't trust this vendor's AEO anymore", removing the vendor's key_url from 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

cargo bench

Bundled bench measures sign and verify separately so you can spot regressions in either path.


Tests

cargo test --all-targets
cargo test --doc
cargo clippy --all-targets -- -Dwarnings
cargo fmt --all -- --check

CI matrix: stable, beta, 1.86.0 (MSRV).


License

MIT. See LICENSE.