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

3.7 KiB

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.