Skip to content

[Hackathon] vladimirputkov: verified_registry — signature-verified registration with typed rejections and a Sybil scenario#67

Open
VladimirPutkov wants to merge 1 commit into
projnanda:mainfrom
VladimirPutkov:hackathon/vladimirputkov-registry-integrity
Open

[Hackathon] vladimirputkov: verified_registry — signature-verified registration with typed rejections and a Sybil scenario#67
VladimirPutkov wants to merge 1 commit into
projnanda:mainfrom
VladimirPutkov:hackathon/vladimirputkov-registry-integrity

Conversation

@VladimirPutkov

Copy link
Copy Markdown

[Hackathon] vladimirputkov: verified_registry — signature-verified registration with typed rejections and a Sybil scenario

Branch: hackathon/vladimirputkov-registry-integrity · Base: main · Layer: registry (one-layer scope)

Motivation

Problem 06 and merged PR #24 made the registry distributed and partition-honest — discovery
converges via gossip instead of pretending a shared dict is a network. But both the default
in_memory registry and the gossip registry share one trust model: register(card) believes
anyone.
Any agent can register an AgentCard carrying another agent's agent_id — silently
overwriting the victim's card and poisoning every subsequent lookup — or advertise capabilities
it never proved it holds. The identity layer already ships two signing plugins (did_key,
ed25519_rotating), yet no registration ever consults them. The docs/layers/registry.md
wishlist covers discovery mechanics; registration authenticity is untouched by any open or
merged PR.

Design

Plugin — registry/verified.py (registry: verified). A drop-in Registry implementation
that routes registration through the identity layer. A card is admitted only when its metadata
carries a signature, by the card's own agent_id, over the card's canonical bytes
(agent_id, name, order-independent capabilities, endpoint; metadata excluded — the
signature rides there). Rejections raise RegistrationRejectedError with a typed reason:

Attack Reason
no / malformed signature missing_signature
signed card claiming someone else's agent_id signer_mismatch
forged value, unknown signer, or card mutated after signing bad_signature

Verification is delegated to whatever Identity plugin is configured — the registry implements
no cryptography. sign_card passes key_id/signed_at through when present, so
ed25519_rotating composes unchanged. Rejected registrations mutate nothing: no store, no
subscriber notification. Registered as ("registry", "verified") in nest_core/plugins.py,
exactly like gossip.

Scenario — scenarios/registry_integrity.yaml (7 agents, seed 42, no failures). Three honest
registrants sign and register their own cards; three Sybil agents attempt the three attacks a
registration gate must refuse — impersonate (victim's id, attacker's key), unsigned
(victim's id, no proof), tamper (own card mutated after signing to grant itself the victim's
capability). An auditor then looks up every honest agent's capability and records whether
discovery still resolves to the authentic owner. Every attempt is emitted as a reg: trace line
with actor, claimed id, attack kind, verdict, and typed reason.

Adversarial validators — VALIDATORS["registry_integrity"] (4).
registry_unauthenticated_rejected (every adversarial attempt rejected; fails vacuous traces),
registry_honest_admitted (anti-deny-everything guard), registry_rejection_reasons (each
attack rejected with the right typed reason, not just rejected), and
registry_authentic_discovery (no honest agent missing from discovery or resolving to a
poisoned card).

Differential proof. The same YAML with one line flipped to registry: in_memory runs
without crashing: all three attacks are accepted, honest-0's card is overwritten by the
impersonator (present=1, authentic=0 — a poisoned lookup), and the validators fail. Under
registry: verified all four pass. Both runs are asserted end-to-end in tests.

Tests

  • Hypothesis (100 examples each): completeness — any generated card signed by its own
    identity is admitted and discoverable; soundness — any post-signing mutation of any
    signature-covered field is rejected with the expected typed reason and mutates nothing.
  • Unit: protocol conformance (isinstance(…, Registry)), all three rejection reasons, the
    spoofed-signer variant, unknown-signer rejection, rejection-mutates-nothing, subscriber
    notification, canonical-bytes order/metadata independence, key_id pass-through from
    ed25519_rotating.
  • Validator unit tests on synthetic traces: honest pass, accepted-attack fail, wrong-reason
    fail, poisoned-lookup fail, non-vacuousness, no double-counting of receive copies, registry
    dispatch.
  • End-to-end: verified run passes all validators (with all three attack kinds and all
    three typed reasons present in the trace); in_memory run fails them without crashing; same
    seed ⇒ byte-identical trace.

Determinism

No wall-clock, no unseeded RNG; per-agent identities derive from fixed string seeds; canonical
card bytes use sorted keys, sorted capabilities, and fixed separators; the auditor's single
audit: pulse is the only scheduled event. Same seed ⇒ byte-identical trace (asserted).

Tradeoffs / residuals (deliberate, documented in the plugin docstring)

  • deregister(agent) carries no authentication material in the Registry protocol, so eviction
    is unauthenticated — fixing that is a protocol change, out of one-layer scope.
  • The verifier trusts whatever key material the identity layer holds; key distribution is the
    identity layer's problem (and ed25519_rotating's rotation windows compose here unchanged).
  • Signature freshness/replay across runs is out of scope for a single-run registry; key_id/
    signed_at are passed through so a follow-up can add as-of checks like the identity
    validators do.

Reproduce

uv sync && uv run ruff check . && uv run ruff format --check . && uv run pyright && uv run pytest -v

uv run pytest packages/nest-plugins-reference/tests/test_verified_registry.py \
              packages/nest-plugins-reference/tests/test_verified_registry_properties.py \
              packages/nest-core/tests/test_registry_integrity.py -v

# passing run
uv run nest run scenarios/registry_integrity.yaml
uv run python -c "
from pathlib import Path
from nest_core.validators import validate_trace
for r in validate_trace(Path('traces/registry_integrity.jsonl'), 'registry_integrity'):
    print('PASS' if r.passed else 'FAIL', r.name, '-', r.detail)"

# failing run: edit scenarios/registry_integrity.yaml -> registry: in_memory, re-run both commands

…nd Sybil scenario

Add a VerifiedRegistry plugin that gates registration through the identity
layer — a card is admitted only when its metadata carries a valid signature
by the card's own agent_id over canonical bytes. Rejections carry typed
reasons (missing_signature, signer_mismatch, bad_signature).

Includes a 7-agent adversarial scenario (3 honest + 3 Sybil attackers +
1 auditor), 4 trace validators, Hypothesis property tests for completeness
and soundness, and full E2E + determinism tests.

Layer: registry
Plugin: registry/verified
Scenario: registry_integrity
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant