- 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
286 lines
10 KiB
Go
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
|
|
}
|