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

5.8 KiB

Implementation notes

Non-negotiable properties of the Go implementation of TCE and the protocol objects. These restate, in implementation terms, what 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.