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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}