# 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.