- 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
138 lines
5.6 KiB
Markdown
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`.
|