niko_trust/internal/address/address.go
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

286 lines
10 KiB
Go

// Package address implements trust addresses: a bech32m encoding of an
// Ed25519 public key together with a protocol version byte.
//
// Normative invariants enforced here:
//
// - INV-3: an address is derived exclusively from cryptographic material.
// It is never a database identifier and carries no server-assigned state.
// - INV-7: an address never contains an alias or any other human-chosen
// label. Aliases are not security-sensitive and must not round-trip
// through this package.
// - INV-8: the mapping public key <-> address is total and canonical. Every
// valid public key has exactly one valid address encoding, and every valid
// address decodes to exactly one public key.
// - INV-9: this package imports only the standard library and a vetted
// bech32 implementation. It knows nothing about claims, approvals,
// storage, transport or any application semantics.
//
// Encoding:
//
// hrp = "trust"
// payload = version(1 byte) || ed25519 public key(32 bytes)
// address = bech32m(hrp, convertbits(payload, 8 -> 5, pad=true))
//
// bech32m (BIP-350) is used rather than the original bech32 (BIP-173). The
// original checksum has a known weakness when the final data character can
// vary in a length-extending way; BIP-350 replaces the checksum constant to
// fix it. Because a trust payload is a fixed-length blob preceded by a version
// byte that we intend to extend over time, the bech32m constant is the correct
// choice. Decoding rejects the bech32 (Version0) constant outright, so a
// checksum-variant downgrade is not accepted.
package address
import (
"crypto/ed25519"
"errors"
"fmt"
"strings"
"github.com/btcsuite/btcd/btcutil/bech32"
)
// HRP is the human-readable part of every trust address.
const HRP = "trust"
// Version0 is the only protocol version currently defined. It denotes a
// payload consisting of a raw 32-byte Ed25519 public key.
//
// The version byte is the first byte of the 8-bit payload. Because bech32
// regroups the payload into 5-bit units, a leading zero byte renders as the
// character 'q', which is why version 0 addresses read as "trust1q...".
const Version0 byte = 0x00
// PubKeySize is the size of an Ed25519 public key in bytes.
const PubKeySize = ed25519.PublicKeySize
// payloadSize is the size of the decoded 8-bit payload: version || pubkey.
const payloadSize = 1 + PubKeySize
// EncodedLen is the exact character length of a version 0 trust address.
//
// len("trust") + len("1") + ceil(33*8/5) + len(checksum) = 5 + 1 + 53 + 6
const EncodedLen = len(HRP) + 1 + 53 + 6
// maxEncodedLen bounds the input accepted by Decode. It is deliberately a
// small constant rather than the bech32 limit of 90: no valid trust address is
// longer than EncodedLen, and refusing longer input early keeps the parser
// cheap to call on untrusted data.
const maxEncodedLen = EncodedLen
var (
// ErrEmpty is returned when decoding an empty string.
ErrEmpty = errors.New("address: empty")
// ErrTooLong is returned when the input cannot possibly be an address.
ErrTooLong = errors.New("address: too long")
// ErrNotLowercase is returned for input containing uppercase characters.
// bech32 permits an all-uppercase form, but permitting two spellings of
// one address would violate INV-8, so only lowercase is accepted.
ErrNotLowercase = errors.New("address: must be lowercase")
// ErrChecksum is returned when the bech32m checksum does not verify.
ErrChecksum = errors.New("address: invalid checksum")
// ErrNotBech32m is returned when the string carries a valid checksum, but
// computed with the original bech32 constant instead of bech32m. This is a
// downgrade attempt or a foreign address type and is always rejected.
ErrNotBech32m = errors.New("address: not bech32m")
// ErrWrongHRP is returned when the human-readable part is not "trust".
ErrWrongHRP = errors.New("address: wrong human-readable part")
// ErrPayloadSize is returned when the decoded payload is not exactly
// version || 32-byte public key.
ErrPayloadSize = errors.New("address: wrong payload size")
// ErrVersion is returned for an unknown protocol version byte.
ErrVersion = errors.New("address: unsupported version")
// ErrPadding is returned when the 5-bit to 8-bit regrouping leaves
// non-zero padding bits. Such a string is a second spelling of an address
// that already has a canonical form, so accepting it would violate INV-8.
ErrPadding = errors.New("address: non-canonical padding")
)
// Address is a validated, canonical trust address.
//
// The zero Address is invalid. Values of this type are only produced by
// Parse, FromPubKey or their variants, so a non-zero Address is always
// well-formed: its string form is canonical and its public key has already
// passed curve validation.
type Address struct {
// s is the canonical lowercase bech32m string.
s string
// key is the decoded public key. Stored as an array rather than a slice so
// that Address remains comparable and cannot be mutated through an alias
// of the caller's backing array.
key [PubKeySize]byte
// version is the protocol version byte.
version byte
}
// FromPubKey encodes an Ed25519 public key as a version 0 trust address.
//
// The key is validated as a curve point before encoding; see [ValidatePubKey].
// This means it is not possible to construct an Address for a small-order or
// non-canonically encoded key, which is what prevents such a key from ever
// entering the protocol as an identity.
func FromPubKey(pub ed25519.PublicKey) (Address, error) {
if err := ValidatePubKey(pub); err != nil {
return Address{}, err
}
return fromValidatedKey(Version0, pub)
}
// fromValidatedKey encodes a key that has already passed validation.
func fromValidatedKey(version byte, pub []byte) (Address, error) {
payload := make([]byte, 0, payloadSize)
payload = append(payload, version)
payload = append(payload, pub...)
conv, err := bech32.ConvertBits(payload, 8, 5, true)
if err != nil {
return Address{}, fmt.Errorf("address: convert bits: %w", err)
}
s, err := bech32.EncodeM(HRP, conv)
if err != nil {
return Address{}, fmt.Errorf("address: encode: %w", err)
}
a := Address{s: s, version: version}
copy(a.key[:], pub)
return a, nil
}
// Parse decodes and validates a trust address.
//
// Parse is strict by design. It rejects uppercase input, the bech32 checksum
// constant, unknown versions, wrong payload lengths, non-zero padding bits and
// public keys that are not valid curve points of the prime-order subgroup.
// Every rejection removes an alternative spelling or an unusable key, which is
// what makes the address space canonical (INV-8).
func Parse(s string) (Address, error) {
switch {
case s == "":
return Address{}, ErrEmpty
case len(s) > maxEncodedLen:
return Address{}, ErrTooLong
}
// Reject uppercase before handing the string to the bech32 decoder, which
// would otherwise normalise it and accept a second spelling.
if strings.ToLower(s) != s {
return Address{}, ErrNotLowercase
}
hrp, data, version, err := bech32.DecodeNoLimitWithVersion(s)
if err != nil {
// Collapse the library's error taxonomy: distinguishing "bad
// character" from "bad checksum" tells an attacker nothing useful and
// invites callers to branch on parse failure modes.
return Address{}, fmt.Errorf("%w: %v", ErrChecksum, err)
}
// Enforce bech32m exactly. DecodeNoLimitWithVersion accepts either
// checksum constant and reports which one matched; anything other than
// VersionM is a different address family or a downgrade attempt.
if version != bech32.VersionM {
return Address{}, ErrNotBech32m
}
if hrp != HRP {
return Address{}, ErrWrongHRP
}
payload, err := convertFromBech32(data)
if err != nil {
return Address{}, err
}
if len(payload) != payloadSize {
return Address{}, ErrPayloadSize
}
if payload[0] != Version0 {
return Address{}, ErrVersion
}
pub := payload[1:]
if err := ValidatePubKey(pub); err != nil {
return Address{}, err
}
a := Address{s: s, version: payload[0]}
copy(a.key[:], pub)
return a, nil
}
// convertFromBech32 regroups 5-bit data into 8-bit bytes, rejecting any
// encoding that carries non-zero padding bits or a trailing incomplete group
// that a canonical encoder would never emit.
func convertFromBech32(data []byte) ([]byte, error) {
// pad=false makes ConvertBits reject leftover bits that are non-zero or
// wider than 4, which is exactly the canonical-form requirement.
out, err := bech32.ConvertBits(data, 5, 8, false)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrPadding, err)
}
return out, nil
}
// MustParse is Parse for constants and test fixtures. It panics on error and
// must never be used on untrusted input.
func MustParse(s string) Address {
a, err := Parse(s)
if err != nil {
panic("address: MustParse: " + err.Error())
}
return a
}
// String returns the canonical bech32m encoding.
func (a Address) String() string { return a.s }
// IsZero reports whether a is the unset zero value.
func (a Address) IsZero() bool { return a.s == "" }
// Version returns the protocol version byte.
func (a Address) Version() byte { return a.version }
// PubKey returns a copy of the Ed25519 public key. A copy is returned so that
// a caller cannot mutate the key held inside a validated Address.
func (a Address) PubKey() ed25519.PublicKey {
out := make(ed25519.PublicKey, PubKeySize)
copy(out, a.key[:])
return out
}
// KeyBytes returns the public key as a fixed-size array.
func (a Address) KeyBytes() [PubKeySize]byte { return a.key }
// Equal reports whether two addresses denote the same public key.
//
// Comparison is on the key rather than the string so that the result stays
// correct if a future version byte changes the textual form of the same key.
func (a Address) Equal(b Address) bool {
return a.key == b.key && a.version == b.version && !a.IsZero() && !b.IsZero()
}
// MarshalText implements encoding.TextMarshaler.
func (a Address) MarshalText() ([]byte, error) {
if a.IsZero() {
return nil, ErrEmpty
}
return []byte(a.s), nil
}
// UnmarshalText implements encoding.TextUnmarshaler. Decoding runs the full
// validation path, so an Address obtained from JSON is as trustworthy as one
// obtained from Parse.
func (a *Address) UnmarshalText(b []byte) error {
parsed, err := Parse(string(b))
if err != nil {
return err
}
*a = parsed
return nil
}