niko_trust/docs/INVARIANTS.md
Niko Marmeladkov 9d66003689
Initial commit: signed-object trust relay, verifier, and docs
- server: relay storing signed objects (PUT/GET), per-IP rate limiting,
  per-subject quota (1000), one-response-per-request, pagination,
  /v1/healthz /v1/readyz /v1/metrics
- verify: signature-verifying trust evaluator; every object is checked via
  env.Verify(), approvals via VerifyApprovalResponse, revocations via
  VerifyRevocationOf; k-of-n approval quorum
- docs: TRUST-MODEL.md and API.md describing issuer-anchored signatures and
  the endpoint/status-code contract
- tests: server, verify, and ratelimit packages
2026-08-12 22:36:49 +03:00

138 lines
5.6 KiB
Markdown

# Protocol invariants
These are normative. Every stage of the implementation is checked against
them, and several are enforced by tests that inspect the source tree so that a
violation breaks the build rather than merely contradicting a comment.
The single sentence the whole design reduces to:
> Cryptography establishes who said something.
> The application decides whether it trusts them and what their statement means.
## The invariants
**INV-1. A trust server compromise must never allow forging an identity,
claim, approval or authorization response.**
The server holds no signing key. Signing capability lives exclusively in
`internal/identity/signer`, which server packages do not import. An attacker
with full database and process control can read public data, delete it, delay
it, censor it and deny service, but cannot produce a signature.
*Enforced by* `TestNoSigningOutsideSigner`, `TestProtocolDoesNotImportSigner`.
**INV-2. trust.n1ko.dev is never an issuer of claims or approvals.**
There is no server identity, no root key and no certificate authority. The
server never appears as the `issuer` of a claim or the `sender` of a request.
The only random material it produces is authentication challenge nonces, which
are not protocol objects and carry no signature.
**INV-3. Database identifiers are never security identifiers.**
Security identity is always cryptographic: an Ed25519 public key, or a
content-addressed hash of canonical object bytes. No autoincrement column, row
id or server-assigned handle ever appears in a signed object or in an
authorization-relevant comparison.
**INV-4. An ApprovalResponse is valid only when its `request_hash` matches the
exact canonical ApprovalRequest bytes.**
The response commits to the full request, not to a sender-chosen label.
Verification takes both objects, so there is no API that can check a response
in isolation and no way to reuse a signed decision under a different request.
**INV-5. Applications must perform their own authorization decisions.**
The protocol authenticates statements and approvals. It never answers "is this
allowed". No API returns a permission verdict, and no package contains a
notion of role, capability or permission.
*Enforced by* `TestNoApplicationSemantics`.
**INV-6. No implicit transitive trust.**
If A trusts B, that says nothing about identities B trusts. The server never
traverses a trust graph, never joins claims across issuers and never derives a
statement that no one signed.
**INV-7. Aliases are never security-sensitive.**
An alias is a self-asserted display label. It is not unique, not verified, and
must not participate in signature verification or authorization. Aliases are
excluded from the canonical encoding of claims and approvals. User interfaces
must never display an alias without its address.
*Enforced by* `TestAliasNeverAffectsVerification`, `TestDisplayAlwaysShowsAddress`.
**INV-8. Every security-sensitive object has a precisely defined canonical
representation, specified before it is implemented.**
Signatures are computed over canonical bytes, never over JSON. Each object has
exactly one valid encoding; any input with two possible spellings is either
normalised to one or rejected.
**INV-9. Protocol packages must not import application-specific packages.**
The protocol layer depends only on the standard library and vetted
cryptographic primitives. Storage, transport and application semantics stay
outside it.
*Enforced by* `TestProtocolLayerImports`.
**INV-10. Property and fuzz tests for all parsers, canonicalization, address
decoding, TCE decoding and signature verification.**
Every parser that touches untrusted input has a fuzz target asserting
totality (no panic, no hang, no unbounded allocation) and the specific
correctness properties of that parser.
## The canonical pipeline
```
canonical object
|
v
TCE deterministic, length-prefixed binary encoding
|
v
SHA-256
|
v
object ID content address; identifies and references the object
|
v
Ed25519 signature computed over the TCE bytes, not over the ID
```
Consequences that follow from this pipeline and are treated as binding:
- The signature covers the TCE bytes. The hash is used for identification and
reference only, never as the signing input.
- The object ID is a content address, so INV-3 holds by construction and the
`request_hash` check in INV-4 is a byte comparison.
- The TCE encoding of an object excludes its own signature field.
- Canonicalization is total and rejecting: duplicate map keys, invalid UTF-8,
unknown type tags and non-normalizable numbers are all refused rather than
repaired.
- `Decode(Encode(x)) == x` and `Encode(Decode(b)) == b` are both fuzz
properties. The second rules out malleability, where two distinct byte
strings decode to the same object.
## Additional rule adopted during Stage 1
**Public keys must be canonical points of the prime-order subgroup.**
`crypto/ed25519.Verify` follows RFC 8032 and performs no checks on the public
key. That is not sufficient when the attacker chooses the key. The order-1
point admits a single signature that verifies against every message, which
would give its holder an identity whose signature is valid on any claim or
approval in existence: a direct break of INV-1.
Every public key entering the system is therefore validated as a canonically
encoded, non-small-order curve point before it can become an address. The
check happens once, at address construction, so that any `Address` or
`Identity` value elsewhere in the program has already passed it.
*Enforced by* `TestRejectsSmallOrderKeys`, `TestDegenerateKeyWouldForgeSignatures`,
`TestParseRejectsEmbeddedSmallOrderKey`.