niko_trust/docs/IMPLEMENTATION_NOTES.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

128 lines
5.8 KiB
Markdown

# Implementation notes
Non-negotiable properties of the Go implementation of TCE and the protocol
objects. These restate, in implementation terms, what
[PROTOCOL.md](PROTOCOL.md) requires. Where this file and PROTOCOL.md appear to
disagree, PROTOCOL.md wins and the disagreement is a bug to be reported, not
resolved by changing the specification.
Each property is followed by how it is enforced in code, because a property
that is only written down is a property that will eventually be broken.
---
### 1. TCE is the signed message. SHA-256(TCE) is only the content ID.
The Ed25519 signature is computed over the canonical TCE bytes. The object ID
is a SHA-256 of those same bytes, used to name and reference the object. It is
never the signing input.
*Enforcement.* `Sign` and `Verify` accept TCE bytes. No function in the tree
signs or verifies a hash. `ObjectID` returns a distinct type, `tce.ID`, that
has no method accepting a signature, so a hash cannot be passed where a
message is expected.
### 2. Signature verification must use the exact TCE bytes.
The bytes that arrived are the bytes that are verified. An object is decoded
*from* the verified bytes; a decoded object is never re-encoded in order to
verify it.
*Enforcement.* The decoded object type retains the original byte slice, and
`Verify` uses that slice rather than re-encoding. `TestVerifyUsesReceivedBytes`
constructs a byte string that decodes successfully but is not what the encoder
would emit, and asserts that verification uses the received form. Because the
decoder is strict (property 5), such a byte string must in fact be rejected
outright; the test asserts rejection rather than silent re-encoding.
### 3. `request_hash` must equal SHA-256 of the exact canonical ApprovalRequest TCE.
A response commits to a full request, not to a label the sender chose.
*Enforcement.* There is no exported function that verifies a response on its
own. `VerifyResponse(req *SignedRequest, resp *SignedResponse)` requires both,
and compares `SHA-256(req.TCE)` with `resp.RequestHash` in constant time
before anything else. Omitting the request is a compile error, not a runtime
oversight.
### 4. AuthAssertion audience binding must be mandatory and exact.
An assertion produced for one server must never authenticate a connection to
another.
*Enforcement.* `VerifyAuthAssertion` takes the expected audience as a required
parameter and compares it with `subtle.ConstantTimeCompare`. There is no
default, no empty-means-any case, and no substring or suffix matching. An
empty expected audience is an error.
### 5. Malformed input is rejected, never repaired.
Unknown object tags, unknown object versions, unknown or reserved value tags,
non-minimal uvarints, truncated fields, trailing bytes, duplicate map keys,
unsorted map keys, invalid UTF-8, control characters, non-canonical numbers,
out-of-range timestamps and over-long fields are all errors.
The decoder never skips a field it does not understand. Skipping would mean
two implementations compute different meanings for the same signed bytes while
both see a valid signature.
*Enforcement.* Every case in the §12.3 table of PROTOCOL.md has a test. The
frozen `rejects` vectors are executed as a table test.
### 6. Revoked and missing claims are distinct states.
`Revoked` means a signed revocation by the claim's issuer exists and has been
verified. `Missing` means nothing was returned, which may be because the claim
never existed, expired, was withheld by a hostile relay, or was lost.
Absence is never denial. The protocol layer has no function that converts
"not found" into a negative answer, because a relay can withhold anything and
a consumer that treats silence as denial can be manipulated by censorship.
*Enforcement.* Claim status is a three-valued type: `StatusActive`,
`StatusRevoked`, `StatusExpired`. There is no `StatusDenied`, and no API
returns a boolean verdict for a claim. Whether an absent claim matters is a
decision for the consuming application (INV-5).
### 7. Aliases, transport metadata, server IDs, receipt timestamps and
signatures remain outside TCE.
*Enforcement.* The encoder builds each object from an explicit field list in
the order given by PROTOCOL.md §8. The alias appears only in
`IdentityRegistration`. No encoder function accepts a server-assigned
identifier, a receipt time or a signature. `TestNoAliasInSignedBytes` searches
the canonical bytes of a claim and an approval for an alias string and
requires it to be absent.
### 8. No JSON canonicalization, and no second signing representation.
There is exactly one signing format. JSON is a transport and display syntax
only.
*Enforcement.* The protocol package does not import `encoding/json`. JSON
handling lives in a separate wire package that can produce and parse the
transport envelope but cannot sign or verify. An import-graph test enforces
the separation, so a future contributor cannot add a JSON-based signing path
without the build failing.
---
## Additional implementation rules adopted for safety
**Bounded allocation.** A length prefix is checked against the remaining input
and the field's maximum before any allocation. A hostile 10-byte varint cannot
cause a large allocation.
**No mutation of caller data.** Decoded objects hold copies of the byte slices
they expose, so a caller cannot alter an object after it has been verified.
**Constant-time comparison** for hashes, nonces, audiences and signatures,
using `crypto/subtle`. These comparisons are not obviously timing-sensitive,
but the cost is negligible and the analysis needed to prove any individual
case safe is not worth repeating.
**Errors carry no attacker-controlled data.** Decode failures return a small
set of sentinel errors with a field name, never a fragment of the input.
**Determinism.** Encoding the same object twice yields identical bytes; this is
asserted by fuzzing rather than assumed.