Network authority family

Directory State Receipt: proving what a key directory said at one instant, after it has changed

The Directory State Receipt is a signed statement that a named key directory, retrieved at a declared instant inside a bound drift against an external time reference, held a committed set of key identifiers, and that one queried key identifier was present in or absent from that set, with the verdict recomputed rather than supplied. Payment schemes, merchants, site protection providers and agent operators need it, because agent traffic is admitted or blocked on the basis of a key directory that rotates and keeps no history.

What makes it different from a log is that it is written by an observer who does not publish the directory, and it opens nothing. The directory content, every key identifier, every key material digest and the queried identifier stay confidential. What the receipt carries is a salt keyed commitment per key, a digest over the sorted set, and one recomputed presence verdict. Absence is a valid receipt, because absence is usually the fact a dispute turns on.

The problem

Agent traffic gets admitted or refused on the strength of a signature, and the signature is checked against a public key fetched from a directory on the web. That is the standard design across several specifications at once.

The Visa Trusted Agent Protocol specification, published at https://developer.visa.com/capabilities/trusted-agent-protocol/trusted-agent-protocol-specifications, publishes its keys at https://mcp.visa.com/.well-known/jwks and states the consequence plainly: "If the public key cannot be retrieved or the public key has expired, the message should be blocked". The Verifiable Intent specification, at https://verifiableintent.dev/spec/, puts a Credential Provider JWKS on the same footing. The IETF web bot authentication architecture draft, at https://www.ietf.org/archive/id/draft-meunier-web-bot-auth-architecture-05.html, describes the pattern for automated clients on the open web. The A2A specification, at https://a2a-protocol.org/latest/specification/, points at a key set through a jku header. The Mastercard agentic commerce guide, at https://developer.mastercard.com/merchant-cloud/documentation/tutorials-and-guides/agentic-commerce-guide/21/index.md, sits in the same shape.

Every one of those designs makes an admission decision depend on a document that changes without notice and keeps no history.

Now the dispute. A site protection provider blocked an agent's traffic for a day. The agent operator says its requests were correctly signed by a key that was published the whole time. The provider says the key was not in the directory when it looked. The publisher rotated its key set twice that week and serves only the current version. There is no earlier version to point at.

Both parties are describing a document that no longer exists in the form either of them saw. Each has its own logs, written by a party with an interest in the answer, about somebody else's document. Nobody can produce the document.

The reverse case is more common. A payment is disputed. The merchant says it verified the agent's signature against the scheme's key directory before accepting the order. The scheme says the key was revoked and out of the directory by then. If the merchant read the directory before the rotation landed, the merchant is right. If it cached past the rotation, the scheme is right. Telling the two apart needs what the directory served at the instant the merchant read it, and that is what nobody kept.

Autonomy makes this constant rather than occasional. A human sees a blocked page and retries. An agent fleet makes millions of signed requests, and a directory read stale by ninety seconds affects a batch of them at once, in a way nobody notices for weeks.

There is a second failure behind the first, which is time. Any party can write down the instant at which it read a document, and any party can write down a different instant. If the read instant is the reader's own clock, unbound, the record is only as good as the reader's honesty about time.

The third failure is privacy, and it is why the obvious fix does not ship. That fix is for an observer to archive the directory and publish the archive. Key directories carry key identifiers that map to customers, and an archive of who had which key when is a customer list with a timeline attached. No scheme will publish that.

What the receipt binds

The schema is at https://thehiveryiq.com/.well-known/schemas/directory-state-v1.json. The signed body has receipt_type, schema, the boundary, and one object, directory_state.

FieldMeaning
observation_idIdentifier matching ds_ plus 16 to 64 lowercase hex characters.
recorded_atThe UTC instant this record was written.
directory.directory_idIdentifier for the directory observed, matching dy_ plus hex.
directory.directory_urlThe https url read, up to 512 characters.
directory.directory_classOne of jwks_well_known, payment_network_key_directory, signature_agent_directory or credential_provider_jwks.
observed_atThe instant the directory was read.
time_reference.reference_idIdentifier for the external time reference, matching tr_ plus hex.
time_reference.reference_digestDigest of the confidential time reference snapshot.
time_reference.drift_bound_msThe declared drift bound in milliseconds, from 1 to 86400000.
observed_drift_msThe widest offset measured against that reference, from 0 to 86400000.
drift_classEither within_declared_bound or exceeds_declared_bound.
content_digestDigest of the whole retrieved content record.
key_identifier_countHow many key identifiers the directory held, from 0 to 4096.
key_commitmentsOne entry per key, each with an identifier commitment, a key commitment and a key class.
key_identifier_set_digestDigest over the sorted commitment set.
queried_key_commitmentCommitment to the one key identifier being asked about.
presence_verdictEither queried_key_present or queried_key_absent.
observer_roleThe fixed label non_publishing_observer.
evidence_access_classOne of confidential_independent, issuer_only or public_commitment_only.
salt_commitmentCommitment to the 32 byte salt.

Each key commitment entry carries a key_class drawn from ed25519_verification, ecdsa_p256_verification, rsa_verification and unrecognised. The last one matters. An observer that could only record key types it recognised would drop the entries a dispute is most likely to be about.

What is deliberately not in the signed body

No directory content, no key identifier, no key material, no key material digest, no queried key identifier, no time reference snapshot, no salt. The receipt carries digests, one url, one count, enumerated classes and two instants.

Five constructions in src/typed/directory-state.js do the work. The salt commitment is SHA-256 over the UTF-8 label directory.state/salt/v1, a zero byte, and the raw 32 salt bytes.

The content digest is SHA-256 over the canonical JSON of an object carrying the domain directory.state/content/v1 and the retrieved content record. That record must hold exactly directory_id, retrieved_url, key_count and entries, and each entry exactly key_identifier, key_material_sha256 and key_class. Duplicate identifiers are refused, and the record has no field that could carry raw key material.

Per key, two keyed digests are computed with HMAC-SHA256 under the raw salt bytes. The identifier commitment uses the label directory.state/key-identifier/v1 over the canonical JSON of the key identifier alone. The key commitment uses the label directory.state/key-material/v1 over the canonical JSON of the whole entry. Splitting them makes rotation legible. When a directory rotates the material behind a stable key identifier, the identifier commitment stays the same and the key commitment changes, and a later reader sees that without learning either value.

The commitment set is then sorted ascending by identifier commitment, so the order the directory served its keys in carries nothing into the receipt. The set digest is SHA-256 over the canonical JSON of the domain directory.state/key-identifier-set/v1 and the sorted entries.

Canonical JSON is the shared rule: keys sorted recursively, arrays kept in order, no whitespace, and every code unit above U+007F escaped as a lowercase \uXXXX sequence, then digested as lowercase hex SHA-256 over the UTF-8 bytes.

The time reference snapshot is confidential too. It must hold exactly reference_id, a source_class drawn from roughtime, ntp_stratum_one, rfc3161_authority and gnss_receiver, a retrieval offset and a completion offset. The observed drift is the widest absolute value of those two offsets, which is the conservative choice: a read that started close to the reference and finished far from it is reported at its worst point.

The receipt envelope uses the shared Ed25519 construction over the ASCII string hive-receipt <receipt_id> <payload_sha256> <ts>, with hive-receipt as the domain separation label.

The gates

Verification runs in this order and stops at the first failure.

  1. SCHEMA. Validates the envelope against the directory state v1 schema, including the boundary constant, the four directory classes, the four key classes and the drift bounds. A failure means the artifact is malformed.
  2. ISSUER_KEY_MATCH. Resolves envelope.key_id in the trusted keyring and checks the key was authorised for this receipt type at the envelope timestamp. A failure means the signer is not trusted here.
  3. PAYLOAD_DIGEST_VALID. Recomputes the canonical digest of signed_body against payload_sha256. A failure means the body was altered after signing.
  4. SIGNATURE_VALID. Requires Ed25519 and verifies the envelope signature with the resolved key. A failure means that key did not produce it.
  5. OBSERVATION_INSTANT_ORDERED. Requires observed_at and recorded_at to be valid UTC instants with the observation at or before the record. A failure means the record describes a read that had not happened yet.
  6. SALT_BINDING. Requires the disclosed salt to be 64 lowercase hex characters and to recompute to salt_commitment. A failure means none of the keyed digests can be checked.
  7. DIRECTORY_REFERENCE_DECLARED. Requires the directory identifier, an https url and a declared directory class. A failure means the receipt does not say which document was read.
  8. TIME_REFERENCE_INTEGRITY. Requires the snapshot to hold exactly its four allowed fields with a declared source class, and recomputes its digest. A failure means the time evidence supplied is not the time evidence committed.
  9. OBSERVED_DRIFT_RECOMPUTE. Recomputes the drift as the widest absolute offset in the snapshot. A failure means the reported drift was supplied rather than measured.
  10. DRIFT_BOUND_SATISFIED. Recomputes the drift class against the declared bound and fails outright when the drift exceeds it. A failure means the read instant is not tied tightly enough to the reference to be evidence.
  11. CONTENT_DIGEST_RECOMPUTE. Recomputes the digest of the whole retrieved content record. A failure means the content shown is not the content observed.
  12. KEY_IDENTIFIER_SET_INTEGRITY. Rebuilds every commitment entry from the digested content, entry by entry and position by position, and checks the count. A failure means the committed set does not come from the digested content.
  13. KEY_IDENTIFIER_SET_DIGEST_RECOMPUTE. Recomputes the set digest over the sorted commitment set. A failure means the set was reordered or edited after commitment.
  14. CONTENT_KEY_SET_AGREEMENT. Checks the content record's own declared directory identifier, retrieved url and key count against the signed body. A failure means the content came from a different directory or url.
  15. QUERIED_KEY_COMMITMENT_BINDING. Recomputes the identifier commitment for the queried key identifier under the same salt. A failure means the question was changed after the fact.
  16. PRESENCE_VERDICT_RECOMPUTE. Recomputes the verdict as membership of the queried commitment in the committed set. Absence passes. A failure means the verdict was asserted rather than computed.
  17. OBSERVER_ROLE_DECLARED. Requires the observer role to be the fixed label non_publishing_observer. A failure means the observing role was edited.
  18. NO_DIRECTORY_CONTENT_LEAK. Walks every string in the signed body against its approved opaque form: identifier patterns, 64 character hex digests, enumerated classes, the one url. A failure means something readable reached an opaque field.
  19. DIRECTORYSTATE_BOUNDARY_PRESENT. Requires the boundary text to equal the fixed string. A failure means the non attestation was edited or removed.

Mint refuses every field this list recomputes, from the reference digest through to the salt commitment. It also refuses to sign a drift over the declared bound, a content record whose count disagrees with its entries, a retrieved url that does not match the directory, a duplicate key identifier, and a snapshot for a different time reference.

The boundary

This receipt attests only that a named key directory, retrieved at a declared instant within a bound drift of an external time reference, held a committed set of key identifiers, and that one queried key identifier was present in or absent from that set as recomputed by this service from the committed set rather than supplied by the caller. It does not attest that the directory content is correct. It does not attest that the publisher is entitled to publish it. It does not attest that any key in it is validly issued or under any party's custody. It does not attest that a key absent from it does not exist elsewhere. It does not attest that another observer saw the same content at the same instant. It does not attest that any signature under any key was authorised. It does not decide whether any message should have been blocked.

That string is a schema constant inside the signed bytes and it is checked again by its own gate.

The last clause is the expensive one, and it is deliberate. The specification that started this paper says a message should be blocked when a key cannot be retrieved. It would be easy for this receipt to claim it settles that call, and that claim would be the fastest way to make the instrument useless. Whether a message should have been blocked depends on policy, on the caching rules in force, and on what the reader was entitled to rely on. Those are the arguments the parties are having. A receipt that stays out of them can be handed over by either party without conceding anything.

The clause about another observer is the most honest one in the set. Directories are served from many edges and can differ between them. This receipt is one observer's read, and it says so in the signed bytes rather than in a footnote. That is what makes several receipts from several observers worth collecting, because they can disagree and the disagreement is the finding.

The clause about content correctness draws the line in the same place. The receipt fixes what was served. If a directory published a key it should have revoked, this receipt proves the key was there.

Adversarial cases

These are the real cases in test/directory-state.test.mjs, which holds 56 tests, all passing.

A content digest that does not cover the committed key set fails KEY_IDENTIFIER_SET_INTEGRITY. Four more cases fail the same gate: a forged key count, a dropped entry, a rewritten per key commitment and a rewritten key class. Every commitment is rebuilt from the digested content, so no entry survives an edit.

A forged presence verdict fails PRESENCE_VERDICT_RECOMPUTE, in both directions. Claiming presence for a key that is not in the committed set fails, and claiming absence for one that is fails too.

Edited retrieved content fails CONTENT_DIGEST_RECOMPUTE. Content read from another url fails CONTENT_KEY_SET_AGREEMENT, and so does a content record naming a different directory and one whose self declared count disagrees.

A drift over the declared bound fails DRIFT_BOUND_SATISFIED, and mint refuses that case outright. A forged drift value fails OBSERVED_DRIFT_RECOMPUTE, because the drift is recomputed as the widest offset in the snapshot. A snapshot for a different time reference fails TIME_REFERENCE_INTEGRITY, and so does an edited snapshot.

A rewritten queried key commitment fails QUERIED_KEY_COMMITMENT_BINDING, so the question cannot be swapped once the answer is known. A substituted salt fails SALT_BINDING. A raw key identifier in a signed field fails NO_DIRECTORY_CONTENT_LEAK, and a boundary mismatch fails DIRECTORYSTATE_BOUNDARY_PRESENT.

Five positive cases carry the instrument. A directory holding the queried key verifies with verdict queried_key_present. A directory without it verifies with queried_key_absent, the case a dispute usually needs. An empty directory verifies with a zero length commitment set, the state a failed publish leaves behind. A signature agent directory and a credential provider directory each verify on their own class.

Structural tests confirm the design. The minted envelope carries no key identifier, no key material digest, no queried identifier and no salt. Identifier commitments are salt scoped, so two observations under different salts share nothing. Rotated key material changes the key commitment and leaves the identifier commitment alone. The set is sorted, so directory order carries nothing. The classifiers are pure functions of their inputs, and the class lists match the schema enums.

Performance

From hive-verifier-api/benchmarks/results-latest.json, measured at 2026-08-10T08:00:47.634Z at commit a22b4d88f6e041d6f593381b6e310a8d527d5b4b over 200 iterations:

MeasureValue
Mint p503.032 ms
Mint p956.546 ms
Verify p502.113 ms
Verify p952.547 ms
Artifact size3131 bytes

Local harness measurement on one machine and one Node version. The run recorded Node v20.20.1 on linux/x64 with two Intel Xeon cores at 2.90 GHz. It is a reproducible measurement of this code, not a production latency guarantee. One caveat is specific to this type. The benchmark case observes a small directory. Verify rebuilds one identifier commitment and one key commitment per entry, so the cost grows with the number of keys observed, up to the schema limit of 4096. The receipt grows with it too, since it carries one commitment pair per key.

How to run it

This type has no mint route. It is produced as an export and only verified here. The verify route takes no credentials.

The verify route is live on the production host. It is open, needs no account, and costs nothing to call.

  • Verify route: /verify/directory-state
  • Demonstration route: /demo/directory-state, carrying the keys key_present, key_absent, rotated_directory and substituted_content
curl -sS -X POST https://thehiveryiq.com/v1/verify/directory-state -H 'content-type: application/json' -d '{"receipt": <a receipt of this type>}'

The rotated_directory case is a real positive: a rotated payment network directory that no longer carries the queried key. The substituted_content case fails at KEY_IDENTIFIER_SET_INTEGRITY, and because mint refuses it, it is built by minting the honest case, editing the body and re-signing with the same demonstration key.

Where it sits in the canon

Family: network-authority. Category: typed receipt contract.

It is the observation layer under the instruments that reason about keys. The Authority Revocation Receipt settles when an authority was withdrawn and who was told. This one settles what a directory said at one instant, which is the fact a revocation argument runs on when the directory is the only place the withdrawal was expressed. The Admission Binding Receipt fixes what an admitted party was admitted as, and the Knowledge Timestamp Receipt fixes when a party could first have known something. The Screening Attestation Receipt is its nearer relative in shape, since both bind an external time reference with a drift bound and a digest of a named version of a retrieved artifact. This one is simpler: no ruleset version, no supplemental lists, no quorum, and one outcome, which is membership.

It does not replace directory monitoring and it does not decide an admission dispute. It produces the document nobody kept.

Patent pending.

Status

production_deployed.

References for this instrument
Canon entry
Directory State Receipt in the Hive canon
JSON Schema
https://thehiveryiq.com/.well-known/schemas/directory-state-v1.json
Verify route
POST /verify/directory-state
Canonical type
directory.state
Private by design. Hive does not store your prompts. Every request is already receipted by a one-way SHA-256 fingerprint, not the words. Proof, not surveillance.