OpenResidency Docs

What not to break

The security and privacy invariants that are the whole point of OpenResidency — do not relax them to make something work; narrow the config instead.

In plain terms. The rules that must not be bent. When something does not work, the fix is to change the configuration — never to relax one of these. Each one exists because relaxing it would expose somebody's identity.

Some checks look like defensive clutter and are in fact the whole point. Do not relax any of these to make a wallet or an integration work — narrow the jurisdiction config instead. Each has a test asserting it is rejected for the right reason; if you change one, the test changes with it and the PR says why.

The invariants

  • The raw national ID is never stored — only a tokenized subjectRef (an HMAC under a deployment pepper). The credentials and id-tokens never carry the national ID.
  • Holder key proof is mandatory at issuance — a credential with no holder binding is a bearer token: whoever holds the bytes can use it.
  • Nonces are single-use, consumed with a conditional DELETE. A read-then-delete races, and the race is the replay attack.
  • Presentations check holder binding, nonce, and audience — drop any one and a stolen credential presented under someone else's key succeeds.
  • The presentation path fails closed on revocation. Offline credential verification is deliberately permissive (a field officer with no connectivity still gets a useful answer); online presentation is not.
  • JSON-LD contexts are pinned and never fetched, and canonicalization runs in safe mode — without it, a term missing from the @context is silently dropped from what the signature covers, so the credential still verifies while attesting less than it appears to.
  • Applicant→identity binding is recorded on every credential — a bare foundational lookup binds nothing (applicantBinding.method: none); a jurisdiction whose policy sets required: true refuses to issue on a lookup alone.
  • Each relying party gets a pairwise subject — a different opaque user id per service, so two services cannot join their records on a citizen. Privacy is an architectural constraint here, not a policy promise.
  • Residence is separate from origin/indigeneity — origin may be captured but is never admissible as proof of residence, and by default never enters the credential.

The rule of thumb

If a wallet or integration "needs" one of these relaxed, the answer is almost always to narrow the jurisdiction config — not to loosen the invariant. These are the properties that make the platform trustworthy infrastructure rather than a liability.

Never commit

Secrets, real national ID numbers, or personal data — including in tests or fixtures. Use the MOCK provider and synthetic values (in the demo config, an even last digit verifies).

Source: CONTRIBUTING.md → "What not to break" and the assertions in scripts/*.ts.

On this page