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