Skip to content

Contract and identity

Part of core components.

Signing lives with whoever holds a key; core's half is verification. This page describes how each verifying piece works.

Signed payloads

Every signed operation reduces to the same primitive: build a canonical byte string, verify an Ed25519 signature over exactly those bytes. The payloads are colon-joined text under a domain tagfunduq-kyok-call:{token}:{timestamp}:{sha256 of body}, funduq-connect-provider:{pinned funduq key}:{ticket}:{provider nonce}, and so on for the three families — the tag first, so a signature captured for one purpose is meaningless for another. Timestamped families are accepted within a ±60s freshness window of funduq's clock.

A provider's roster acts are not among them. Registering and deleting were four families once, one per roster and verb, each re-proving with a self-chosen timestamp that the caller held a key that had already been proved when its link opened. They are operations on the open link now (see the handshake), and nothing signs them.

Connect authentication cannot ride a timestamp (self-chosen freshness is replayable for its whole window), so it answers a ticket funduq issued. issue_ticket(public_key) mints a random single-use value and remembers it with the key it admits and its issue time; the connecting provider signs funduq-connect-provider:{pinned funduq key}:{ticket}:{provider nonce} — the first field names the recipient, so a proof one funduq coaxes out cannot be relayed to attach at another.

Verification matches the ticket's key against the connecting key first, then destroys it, then checks the signature against a payload built with this funduq's own key. That order is load-bearing twice over: a leaked ticket is useless to anyone but the key it names, and a stranger cannot spend someone else's admission with a garbage proof — which a version that destroyed first allowed, as a denial needing no key at all.

funduq answers in kind: attach returns its own signature — the FunduqIdentity keypair over funduq-connect-funduq:{ticket}:{provider nonce} — for the transport to relay, and hands it to any connection exposing confirm_connect before the link is recorded open, which is where a provider checks it against the funduq key it pinned and refuses an imposter. In-process connections go through the identical ceremony automatically: ticketed, verified, and answered. A funduq with no identity configured answers nothing — it cannot prove itself, and only a pinning provider treats that as a failure.

Issuing a ticket is not an operation on a connection and cannot become one: one fetched over the link would mean the link existed before anything authorised it.

Actor chain verification

A chain is a list of JWTs. The verifier walks it in order: for each hop, read the signer's public key out of the (not yet trusted) payload, verify the JWT's signature under exactly that key, then check the hop's prevHash equals the sha256 of the previous hop's full text, and that the subject never changes. Expiry is enforced on the last hop only — earlier hops are provenance, not standing authorization. The result is the subject plus each signer's key in order; funduq additionally resolves each key against the roster to name registered agents, which the SDK's twin verifier deliberately does not (it has no roster).

Envelope models

Every structure funduq puts on a wire is a pydantic model with camelCase aliases: model_dump(by_alias=True) is the frame a transport carries, model_validate rebuilds it. models.py declares the refs and the claimed-run shape; props.py declares the verified-caller props model and assembles both forwarded-props additions in one place for both caller doors (the KYOK props model itself lives with its mechanism in kyok.py). Translation from funduq's internal objects to the delivered forms is stated once, as classmethods on the delivered models, read by attribute so no import crosses the boundary.

The twins and the guards

Everything above is deliberately implemented twice: the provider SDKs carry independent twins — payload builders, a chain verifier, the delivered envelopes and props models — and neither side imports the other, so each suite is a second opinion on the bytes rather than an echo. Agreement is pinned by contract-vectors.json: inputs → exact payload bytes → deterministic signatures under a published test key, plus a fixed-time chain and the canonical wire frames, consumed by all three test suites and replayable in any language. Two CI guards keep the set complete — every domain tag must have a vector family, and the SDKs must validate every structure funduq puts on the wire — because both gaps were shipped and paid for before the guards existed.