- 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
5.6 KiB
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_hashcheck 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)) == xandEncode(Decode(b)) == bare 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.