Docs
Security
The threat model, the mistakes that actually cause incidents, and what a reviewer should check first.
Who you are defending against
Ordered by how often each one actually shows up.
| Adversary | Capability | Primary defence |
|---|---|---|
| A legitimate member, curious | Valid credential, patience | Reciprocity budgets, logging, the rulebook |
| A stolen credential | Everything the member could do | Request signing, short freshness window, revocation |
| A crafted payload | Feeds a QR code or deeplink to a reader | Anchored parsing |
| A network operator | Sees searches; holds the index | Declared retention, audit, contract — commitments, not properties |
| A compromised resolver | Answers with the wrong destination | Signatures and commitments |
| A passive observer | Watches traffic | TLS, plus not leaking through timing |
The first row is the one implementations under-weight. The realistic directory attacker is not an anonymous outsider — it is an authenticated participant, or someone holding their key.
Mistakes that cause incidents
1. Unanchored URI parsing
Scanning a payload for a pattern anywhere and taking the first match. Append a crafted suffix to something innocuous and a payment goes to the attacker.
Anchor at both ends. Match the whole string. See the reading algorithm.
2. Symmetric signatures on public statements
An HMAC proves someone holding the shared secret produced the value — and the recipient holds it too. Useless for anything a third party has to rely on.
Ed25519 over canonical JSON for every statement that leaves the operator.
3. Non-canonical serialisation
JSON.stringify preserves insertion order. Two implementations produce different bytes for the same
object, no signature verifies across the boundary, and it works perfectly in testing because there
is only one implementation there.
RFC 8785. Test against vectors from an independent implementation.
4. Returning a profile from a lookup
The defect this protocol exists not to reproduce. A routing question gets a routing answer; returning the account holder's institution, KYC tier or name turns a lookup into a disclosure, and the hash was supposed to be what made lookups safe.
5. Distinguishable negatives
Different status, different body, different timing for "unknown" versus "not visible to you". That is an oracle for whether someone is a customer somewhere.
6. Credentials in client applications
A key shipped in a mobile app is public the moment someone opens the binary. Directory calls go from a server; the app talks to its own backend.
7. Caching authorisation status
A credential valid this morning may be revoked now. Check revocation at execution, not at issuance. "We checked recently" is not a defence.
8. Unbounded delegation
A token authorising "a payment" authorises every payment. Bind it to amount, currency and a commitment over the destination, and expose the counters.
Key management
One signing key per operator, published under a kid. Rotation is publish-then-switch: the new
key appears alongside the old, then new envelopes carry the new kid, and old envelopes stay
verifiable.
An unknown kid is not a bad signature. Report it distinctly and refetch the issuer's document
— it usually means a rotation you have not picked up.
Fetch keys from the issuer's own discovery document. Never from whoever served the envelope. An operator vouching for another operator's key rebuilds the hierarchy this design avoids.
The pepper
Server-side only. Never distributed to members.
Phone numbers are a small, dense, enumerable space. With the pepper, anyone can compute every possible hash offline and reverse the whole directory — retroactively, with no API call, so no budget and no log record it. A secret shared with fifty members needs to leak once, anywhere.
Store it in a secret manager, not in the database. The reference schema holds a reference to it and a version, never a value.
Argon2id rather than a bare hash raises the cost of a leak from seconds to an afternoon of rented compute. A speed bump, not a defence — worth doing because it costs milliseconds and cannot be changed later without re-registering every entry.
Request signing
- 01Path-Key-IdThe key id.
- 02Path-TimestampRFC 3339, inside the freshness window.
- 03Path-SignatureEd25519 over METHOD \n PATH \n TIMESTAMP \n SHA256(body).
A five-minute window by default. Include the body digest — without it, a captured signature authorises a different body on the same route.
The most common failure here is clock skew, not an attack. Check that first.
What to review first
If you are reviewing a PATH implementation and have an hour:
- Parse a URI with a crafted prefix and suffix. Rejected?
- Look up an unknown key and a hidden key. Identical responses, including timing?
- Verify a signature produced by another implementation. Canonicalisation agrees?
- Grep for the clear identifier. Does it reach any table, any log, the search log?
- Revoke a credential mid-flight. Does the next operation fail?
- Read a lookup response. Anything beyond capability and limits?
- Find the pepper. In a secret manager, or in the database?
Those seven cover most of what actually goes wrong.
Reporting
Security issues in the specification or the reference implementation: security@pathprotocol.dev.
Issues in a specific operator's deployment go to that operator — its contact is in its discovery document.