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

82 lines
3.7 KiB
Markdown

# Trust address format
A trust address is the textual form of an Ed25519 public key. It is the only
identifier the protocol has for an identity.
```
trust1qqak5faue6m2gttz5w5dq2n0p4ek2vs4wuw7ysax8tqy3gvtt8dzj0yfahr
\___/\__________________________________________________/\_____/
hrp payload (53 chars) checksum
```
## Encoding
```
hrp = "trust"
payload = version(1 byte) || ed25519 public key(32 bytes)
address = bech32m(hrp, convertbits(payload, 8 -> 5, pad = true))
```
- **Version** is currently `0x00`, denoting a raw 32-byte Ed25519 public key.
Because bech32 regroups the payload into 5-bit units, a leading zero byte
renders as `q`, which is why every current address reads as `trust1q...`.
- **Length** is exactly 65 characters for version 0.
- **Case** is lowercase only. bech32 permits an all-uppercase form, but two
spellings of one address would violate INV-8, so uppercase input is refused.
## Why bech32m rather than bech32
bech32m (BIP-350) differs from the original bech32 (BIP-173) only in the
checksum constant. The original has a known weakness when the final data
character can vary in a length-extending way, which is why BIP-350 exists.
A trust payload is preceded by a version byte that is intended to change over
time, so bech32m is the correct choice.
Decoding enforces this exactly. The bech32 constant is recognised and
**rejected** with a distinct error, so a downgrade to the weaker checksum or a
confusion with a foreign address family is not accepted silently.
## Validation performed on decode
Parsing an address is strict, and every rule removes either an alternative
spelling or an unusable key:
| Check | Rejected because |
|---|---|
| non-empty, length <= 65 | bounds work done on untrusted input |
| lowercase only | two spellings of one address (INV-8) |
| bech32m checksum verifies | corruption must never decode to another identity |
| checksum constant is bech32m | downgrade or foreign address family |
| hrp is `trust` | address belongs to another protocol |
| payload is exactly 33 bytes | malformed |
| version is `0x00` | unknown protocol version |
| 5 to 8 bit regrouping leaves no non-zero padding | alternative spelling |
| key is a canonical curve point | non-canonical encodings alias one point |
| key is not of small order | universal signature forgery, see below |
## Public key validation
The last two rows are a cryptographic requirement, not a formatting one.
`crypto/ed25519.Verify` performs no checks on the public key, as RFC 8032
specifies. When the attacker chooses the key this is exploitable: the order-1
point (`0100...00`) has a signature that verifies against **every** message.
Anyone who registered such a key would own an identity whose signatures are
valid on any claim or approval that anyone cares to construct.
Every key is therefore checked to be a canonically encoded point that is not
annihilated by multiplication by the cofactor. Honest keys always pass; the
degenerate ones can never become an address, and therefore can never become an
identity.
## Properties relied on elsewhere
- **Bijective.** Every valid key has exactly one address; every valid address
yields exactly one key. Verified by fuzzing in both directions.
- **Self-validating.** A non-zero `Address` value has already passed every
check above, so later code never has to re-validate.
- **Corruption-detecting.** Every single-character substitution and every
transposition in the data part is rejected. A mistyped address fails to
parse rather than resolving to a different identity.
- **Fail-closed zero value.** The zero `Address` denotes no identity, does not
compare equal to itself, and stringifies to the empty string.