// 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 }