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}