Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

wesh.gno

8.09 Kb · 207 lines
  1// Package wesh implements the publishable, non-secret half of the Wesh
  2// protocol (weshnet, the network layer behind Berty) as a pure gno library.
  3//
  4// # What this is, and what it deliberately is not
  5//
  6// Wesh is an off-grid, peer-to-peer, end-to-end-encrypted messaging protocol.
  7// A public blockchain is the opposite of that in almost every dimension:
  8// globally replicated, permanent, publicly readable and totally ordered. So
  9// "put Wesh on chain" is the wrong goal, and this package does not pursue it.
 10//
 11// What a chain *is* good at is the one thing Wesh has no answer for: an
 12// authenticated, ordered, publicly auditable record that no single party owns.
 13// Wesh has three gaps of exactly that shape:
 14//
 15//  1. A Berty identity travels out of band, as a QR code or a
 16//     https://berty.tech/id# link. There is no way to look one up, and the
 17//     web prefix is a single host that can be blocked or spoofed.
 18//  2. Resetting the public rendezvous seed (ContactRequestResetReference)
 19//     silently invalidates every link ever shared. There is no revocation
 20//     channel, so a business card keeps pointing at a dead rendezvous point.
 21//  3. A device, once linked to an account, can never be revoked. The Wesh
 22//     protocol documentation states this outright. The account metadata log is
 23//     append-only and there is no authority that can mark an entry void.
 24//
 25// This package carries the pieces needed to address those on chain, and
 26// nothing else. Everything here is public by construction:
 27//
 28//   - [Contact], the publishable identity, weshnet's ShareableContact
 29//     (account public key + public rendezvous seed + display name).
 30//   - The Berty link codec, byte-compatible with berty's own MarshalLink.
 31//   - [RendezvousPoint], the rotating DHT address derivation.
 32//   - The canonical statements an account signs to bind itself to a gno
 33//     address, rotate its seed, and append to a device sigchain.
 34//
 35// # What must never reach a chain
 36//
 37// Group secrets (weshnet's Group.secret, Group.link_key), device chain keys,
 38// message keys, ciphertexts. Publishing a group secret hands the group to
 39// everyone; publishing ciphertext is permanent, expensive, and leaks the
 40// social graph through access patterns. This package has no type that can
 41// hold one, which is the point: there is no struct field here for a secret to
 42// accidentally land in.
 43//
 44// # The privacy trade-off, stated plainly
 45//
 46// Publishing a rendezvous seed is equivalent to printing your Berty QR code on
 47// a billboard. Anyone can then derive today's rendezvous point with
 48// [RendezvousPoint] and watch the DHT for who shows up. That is a real
 49// deanonymization surface, and it is why publication must be an explicit,
 50// opt-in act for an identity that *wants* to be found: a support line, a shop,
 51// a DAO's public channel.
 52//
 53// For an identity that does not want that, [SeedCommitment] publishes
 54// H(seed ‖ salt) instead. The chain then proves that a seed handed over out of
 55// band really does belong to the named account, without broadcasting the
 56// rendezvous point to the world.
 57//
 58// Live realm using this library: [r/moul/x/wesh](/r/moul/x/wesh/v0).
 59package wesh
 60
 61import (
 62	"crypto/sha256"
 63	"encoding/hex"
 64	"errors"
 65)
 66
 67const (
 68	// SeedLen is the length of a public rendezvous seed. Matches weshnet's
 69	// protocoltypes.RendezvousSeedLength.
 70	SeedLen = 32
 71
 72	// AccountPKLen is the length of an account public key. weshnet validates
 73	// it with libp2p's UnmarshalEd25519PublicKey, which accepts raw 32-byte
 74	// ed25519 public keys only.
 75	AccountPKLen = 32
 76
 77	// DevicePKLen is the length of a device public key: also a raw ed25519
 78	// public key.
 79	DevicePKLen = 32
 80
 81	// MaxDisplayNameLen bounds the display name so a link stays scannable and
 82	// gas stays predictable. Not a protocol constant.
 83	MaxDisplayNameLen = 64
 84)
 85
 86var (
 87	ErrBadSeedLen        = errors.New("wesh: public rendezvous seed must be 32 bytes")
 88	ErrBadAccountPKLen   = errors.New("wesh: account public key must be a raw 32-byte ed25519 key")
 89	ErrBadDevicePKLen    = errors.New("wesh: device public key must be a raw 32-byte ed25519 key")
 90	ErrDisplayNameTooLong = errors.New("wesh: display name exceeds MaxDisplayNameLen")
 91	ErrBadCommitmentLen  = errors.New("wesh: seed commitment must be 32 bytes")
 92	ErrBadHex            = errors.New("wesh: value is not valid hex")
 93)
 94
 95// Contact is the publishable identity of a Wesh account: weshnet's
 96// ShareableContact. It is everything a stranger needs to send a contact
 97// request, and it contains no secret.
 98//
 99// AccountPK is the raw ed25519 account public key. Seed is the public
100// rendezvous seed; together they derive the rotating rendezvous point the
101// account announces on, see [RendezvousPoint]. DisplayName is free-form app
102// metadata and is not authenticated by anything.
103type Contact struct {
104	AccountPK   []byte
105	Seed        []byte
106	DisplayName string
107}
108
109// Validate applies weshnet's own ShareableContact.CheckFormat rules: the seed
110// must be exactly SeedLen bytes, and the account key must be a raw ed25519
111// public key. It deliberately does not verify that AccountPK is a valid curve
112// point; that costs a scalar multiplication and the chain gets the same
113// guarantee for free the first time it verifies a signature under the key.
114func (c Contact) Validate() error {
115	if len(c.Seed) != SeedLen {
116		return ErrBadSeedLen
117	}
118	if len(c.AccountPK) != AccountPKLen {
119		return ErrBadAccountPKLen
120	}
121	if len(c.DisplayName) > MaxDisplayNameLen {
122		return ErrDisplayNameTooLong
123	}
124	return nil
125}
126
127// SeedCommitment returns H(seed ‖ salt), the value published instead of the
128// seed itself when an account wants the chain to attest the binding without
129// broadcasting its rendezvous point.
130//
131// The salt is not optional: a seed is 32 bytes of entropy, but a commitment
132// with no salt is a deterministic function of the seed, so two accounts that
133// (impossibly, but still) shared a seed would be linkable, and a seed later
134// disclosed out of band retroactively confirms every past commitment. The salt
135// keeps disclosure a deliberate act.
136func SeedCommitment(seed, salt []byte) ([]byte, error) {
137	if len(seed) != SeedLen {
138		return nil, ErrBadSeedLen
139	}
140	buf := make([]byte, 0, len(seed)+len(salt))
141	buf = append(buf, seed...)
142	buf = append(buf, salt...)
143	sum := sha256.Sum256(buf)
144	return sum[:], nil
145}
146
147// OpenCommitment reports whether commitment is H(seed ‖ salt).
148//
149// The comparison is constant-time over the digest: a short-circuiting compare
150// leaks, through gas, how many leading bytes of a guess were right.
151func OpenCommitment(commitment, seed, salt []byte) bool {
152	want, err := SeedCommitment(seed, salt)
153	if err != nil {
154		return false
155	}
156	return constantTimeEqual(commitment, want)
157}
158
159// constantTimeEqual compares two byte slices without an early return.
160func constantTimeEqual(a, b []byte) bool {
161	if len(a) != len(b) {
162		return false
163	}
164	var diff byte
165	for i := 0; i < len(a); i++ {
166		diff |= a[i] ^ b[i]
167	}
168	return diff == 0
169}
170
171// Every key this package handles is 32 bytes, so a single length-checking
172// helper cannot say which one was wrong. These four wrappers exist so the
173// error a realm surfaces names the field the caller actually got wrong.
174
175// DecodeAccountPK parses a hex-encoded account public key.
176func DecodeAccountPK(s string) ([]byte, error) {
177	return decodeFixed(s, AccountPKLen, ErrBadAccountPKLen)
178}
179
180// DecodeSeed parses a hex-encoded public rendezvous seed.
181func DecodeSeed(s string) ([]byte, error) {
182	return decodeFixed(s, SeedLen, ErrBadSeedLen)
183}
184
185// DecodeDevicePK parses a hex-encoded device public key.
186func DecodeDevicePK(s string) ([]byte, error) {
187	return decodeFixed(s, DevicePKLen, ErrBadDevicePKLen)
188}
189
190// DecodeCommitment parses a hex-encoded seed commitment.
191func DecodeCommitment(s string) ([]byte, error) {
192	return decodeFixed(s, DigestLen, ErrBadCommitmentLen)
193}
194
195// decodeFixed parses hex and enforces an exact byte length. Realms take keys
196// as hex strings because that is what a transaction argument can carry; this
197// is the single place the length rule is applied.
198func decodeFixed(s string, want int, badLen error) ([]byte, error) {
199	b, err := hex.DecodeString(s)
200	if err != nil {
201		return nil, ErrBadHex
202	}
203	if len(b) != want {
204		return nil, badLen
205	}
206	return b, nil
207}