- 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
82 lines
3.7 KiB
Markdown
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.
|