GitHub

Learn

Trust and signatures

Why nearly every answer in PATH is signed, what a signature buys that TLS does not, and how key rotation and revocation work.

The rule

Anything a counterparty may quote back is signed.

Resolver answers, directory answers, payment requests, settlement receipts, holder confirmations, attestations. If a statement might matter later, it carries a signature.

What a signature buys that TLS does not

TLS proves you talked to the right server. It says nothing about the statement that server handed you, and it leaves nothing behind.

A signed envelope is different in three ways:

It survives the connection. Six months later, in a dispute, "the resolver told us to send USDC on Base" is a document rather than a recollection.

It is checkable by someone who was not there. An auditor, a regulator, a counterparty's counterparty. That is what makes a mobile-money receipt worth the same as an on-chain proof: not the rail, the verifiability.

It survives a compromised intermediary. A hostile proxy can drop or delay a response. It cannot produce a different one that verifies.

Asymmetric, not a shared secret

Signatures are Ed25519. Not an HMAC, and the reason is worth stating because symmetric MACs are a common shortcut here.

A shared secret proves that someone holding the secret produced the value — and the recipient holds it too, so the recipient could have produced it. That is fine for a session cookie and useless for a statement a third party has to rely on. If a payer needs to show an auditor what it was told, a MAC the payer could have forged proves nothing.

Canonical bytes

Everything signed is serialised as RFC 8785 canonical JSON before signing: keys sorted, no insignificant whitespace, undefined dropped, numbers in shortest round-trip form.

This is not fastidiousness. JSON.stringify preserves insertion order, so two implementations sending the same object produce different bytes, therefore different signatures, and neither can verify the other. Worse, it works in testing — where one implementation talks to itself — and fails the day a second one appears.

Canonicalisation is the piece where a version skew is silent. Every signature fails at once and the error points at the key rather than at the serialiser. If you port PATH to another language, port this first and test it against the vectors before anything else.

What the envelope covers

{
  "kind": "resolver_answer",
  "address": "path:4a91c2f7e8d3",
  "accepts": [{ "asset": "USDC", "chain": "base" }],
  "protocol_version": "0.1.0",
  "signed_at": "2026-09-09T10:00:00Z",
  "kid": "op_example_2026_01",
  "signature": "…"
}

The signature covers the payload and protocol_version, signed_at and kid. Leaving those outside would let anyone restate a genuine answer under a different version or timestamp: the bytes would still verify and the meaning would have changed.

Keys, rotation and revocation

Every operator publishes its keys at /.well-known/path-keys, each with a kid. A verifier reads the kid from the envelope and fetches the matching key from the issuer's own discovery document — never from a third party, and never from whoever happened to serve the envelope.

That last point is what keeps the trust graph flat. An operator vouching for another operator's key quietly recreates the hierarchy the protocol avoids.

Rotation is publish-then-switch: the new key appears alongside the old, then new envelopes start carrying the new kid. Old envelopes stay verifiable because their key is still published. An unknown kid is reported as such rather than as a bad signature — it usually means a rotation the verifier has not picked up, which is a very different problem from a forgery.

Revocation is a network-level list: keys, members and objects that must stop being honoured everywhere, quickly. It is published and signed, like a certificate revocation list. A compromised member has to be cut off across the network in minutes, not at the pace of contract amendments.

Commitments

Resolver answers carry a commitment — a digest over the answer's substance — published separately from the answer itself.

A payer compares the two and refuses to send on divergence. A compromised resolver can lie once; it cannot lie consistently against a commitment it does not control. In v0.1 the commitment travels in the answer; anchoring it publicly is specified for a later version, and the field exists now so that change is an extension rather than a migration.

Holder confirmation

A network may require the holding member to co-sign directory answers: yes, this key is mine, for this requester, at this time.

It buys three things. The answer stops resting on the network's word alone. The member gets a per-request veto under its own policy. And the index is allowed to be stale without the answer being wrong — someone who left last week produces a "no" rather than a misdirected payment.

What it costs is a second round trip, the holder having to be reachable, and a shift in who observes — the holder now learns that someone is looking for its customer, which the network-only path did not reveal to it. That is defensible, since the holder is that person's data controller, but it is a move rather than a removal.

The confirmation signs the key, the member, the requester, a timestamp and a nonce — never the capability. Routing that through the network would hand it exactly what the two-step split exists to withhold. Without the requester and the nonce in the signature, a confirmation would be replayable by anyone who saw it once.


Next: Glossary — every term, defined once.

On this page