- 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
128 lines
5.8 KiB
Markdown
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.
|