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_test.gno

15.91 Kb · 380 lines
  1package wesh
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/moul/x/wesh/v0"
  7	"gno.land/p/nt/avl/v0"
  8	"gno.land/p/nt/testutils/v0"
  9	"gno.land/p/nt/uassert/v0"
 10)
 11
 12// Signature fixtures. Each was produced with Go's crypto/ed25519 over the exact
 13// statement wesh.BindStatement / wesh.DeviceStatement builds, for chain-id
 14// "dev" (what the test VM reports) and the address named in the constant.
 15// Regenerating them means re-signing: that is the point, since the whole
 16// scheme rests on the chain being unable to accept anything unsigned.
 17const (
 18	// alice, g1v9kxjcm9ta047h6lta047h6lta047h6lzd40gh
 19	aliceAccountPKHex = "2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"
 20	aliceSeed1Hex     = "abababababababababababababababababababababababababababababababab"
 21	aliceSeed2Hex     = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
 22	aliceCommitHex    = "c9744f547763321102debcf355a5652208604d53dba6c77e978ee8077ceb472a"
 23
 24	aliceBind1Sig = "1279475fc9f7423d13e3eeea96a1714754f74c77abbb5859ba899bd3f820f59d" +
 25		"2b0a16f2f6c436cdafd8e20b0d709a5e247a5bbbf4b34df38c1405aba3645905"
 26	aliceBind2Sig = "e915b181109936b1ef77ea8e31e512b56ac63a7e690c382e3613df3ef50472ba" +
 27		"23233442d789c22ad9401cb882f57be737a67e24db207a9856186eda61fdc108"
 28	aliceBind3Sig = "d04f8733f564ee45f141fb71ecb049078a23215b29a14dafeed6c1ced91511ec" +
 29		"47b8c3de120f0d23c534c7387e3273e86b50787280ff38a7f1bd31c23ab35f03"
 30	aliceCommitBindSig = "d467d806145c228ff7b784658f06f8f05e7fa19354ae0bad3a69b40a1a4cf518" +
 31		"ecb3a679674705017c6dfa1bf0bac3e1ec67a4f417c18a80faf77062b8c1e100"
 32	// signed by alice's key but naming bob's gno address
 33	aliceBindForBobSig = "7bac691e9da577c6474af269b690a7415f07c4a8e4df486deccf824b41061735" +
 34		"01cd22af6f1b9654603a866a3a46942ec020714226b01105700eab84cfc08700"
 35
 36	// bob, g1vfhkyh6lta047h6lta047h6lta047h6l03vdhu
 37	bobAccountPKHex = "332ebe8d27cb7323b3a401c1c13b5dd64bccc0e10ecda1c2b5d11a03779a85e5"
 38	bobBind1Sig     = "69653e1bef41150eed026b355d5ee9837455e3432003ca19102075078305fdbe" +
 39		"b440eedb421056a0e62a182a406cb3394c1225de768dcd114f9d0b9b3501b90f"
 40
 41	// alice's device sigchain: add dev1, add dev2, revoke dev1
 42	dev1Hex = "0707070707070707070707070707070707070707070707070707070707070707"
 43	dev2Hex = "0909090909090909090909090909090909090909090909090909090909090909"
 44
 45	devAdd1Sig = "259abde508b8880dd1f53b236d93b08aa1c7511c4308c2b16da2520d5f553a50" +
 46		"67c24876299b6c3e4d9fb88e043dec1978f8d9328f7ac13d80bfdab536893507"
 47	devAdd1Digest = "bc51799b5d012dc7ce806b4c63474b1e2db515e7d1622ecd5900b23e1d6a9519"
 48
 49	devAdd2Sig = "4799b276bf9b713d84855670198a86de8d38bc2644349a554fe0d4dab5fbea9b" +
 50		"7e90aecfe9b7da74345e61f5016361e92e7c0959fe09665a1f20f604015bea00"
 51	devAdd2Digest = "b0da03f83e5349e92bc524714862daa850750f756ca6fd47264333fbf2175c15"
 52
 53	devRevoke1Sig = "47433475efd00ed87769e7bb748a5af9be77cde56817ea9a1ddd9288533e2bf9" +
 54		"b783d85a025a632466ce8dcf637047c932c9d1a15131187213e1ae354e22df02"
 55	devRevoke1Digest = "ef0c1d0614420c7d660f8416b18651206118e49e8f46ac0360bef61ae6b9100d"
 56
 57	// signed for sequence 5 while the head is still at sequence 1
 58	devWrongSeqSig = "c65d9c48c441bf36dcf01c8c0e46fe73a9f7f8d8edffb07bd05f9ca4a8f875bc" +
 59		"6de7ebe30c1d5ae785fde8bfe7b48705605a6c247a62def9b03d37fb62f6f809"
 60
 61	genesisDigestHex = "0000000000000000000000000000000000000000000000000000000000000000"
 62)
 63
 64// reset clears realm globals: gno subtests do not roll them back.
 65func reset() {
 66	byName = avl.Tree{}
 67	byOwner = avl.Tree{}
 68	byAccount = avl.Tree{}
 69}
 70
 71// Two gno-specific rules shape every test below, and both cost a red run to
 72// learn:
 73//
 74//   - testing.SetRealm is scoped to the frame that calls it, so it has to be
 75//     called in the test body, not in a setUpAlice() helper.
 76//   - a helper whose first parameter is `realm` is itself a crossing function,
 77//     so routing a call through one makes THIS realm the Previous() the
 78//     callee sees, not the user. Crossing calls stay inline.
 79//
 80// Together they mean the two setup lines are repeated rather than factored.
 81func aliceAddr() address { return testutils.TestAddress("alice") }
 82func bobAddr() address   { return testutils.TestAddress("bob") }
 83
 84func TestRegister(cur realm, t *testing.T) {
 85	reset()
 86	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
 87	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
 88
 89	uassert.Equal(t, 1, Count())
 90	pk, payload, committed, ok := Resolve("alice")
 91	uassert.True(t, ok, "the name resolves")
 92	uassert.Equal(t, aliceAccountPKHex, pk)
 93	uassert.Equal(t, aliceSeed1Hex, payload)
 94	uassert.False(t, committed, "the seed is published, not committed")
 95	uassert.Equal(t, "alice", NameOf(aliceAddr()))
 96}
 97
 98// TestRegisterEmitsARealBertyLink is the payoff: the realm hands back a link a
 99// Berty client can actually open.
100func TestRegisterEmitsARealBertyLink(cur realm, t *testing.T) {
101	reset()
102	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
103	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
104
105	uassert.Equal(t,
106		"https://berty.tech/id#contact/oZBLFpzghxrATkepWvDPNX9pHYqi6BWgP45xGWhqxcwmqN2bnMMbU7UcwUuTaCcDyUvMjmWRMDWcP96bXAndcjNiAZ1Vz9X/name=Alice",
107		Link("alice"))
108
109	// and the rendezvous point anyone can check against the DHT
110	uassert.Equal(t,
111		"aad2926c39dacabaabbbde48522d043557c8945b6a2dc7b08499c112c72587da",
112		RendezvousPointAt("alice", 1789171200))
113}
114
115func TestRegisterIsCaseInsensitive(cur realm, t *testing.T) {
116	reset()
117	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
118	Register(cross(cur), "  ALICE  ", aliceAccountPKHex, aliceSeed1Hex, "", aliceBind1Sig)
119
120	_, _, _, ok := Resolve("Alice")
121	uassert.True(t, ok, "lookup is case-insensitive")
122	uassert.Equal(t, "alice", NameOf(aliceAddr()), "the handle is stored lowercased")
123}
124
125// TestRegisterRejectsAnUnsignedClaim is the attack the binding signature
126// exists to stop: bob publishing alice's account key next to a seed he
127// controls, and harvesting the contact requests that follow.
128func TestRegisterRejectsAnotherAccountsKey(cur realm, t *testing.T) {
129	reset()
130	testing.SetRealm(testing.NewUserRealm(bobAddr()))
131	uassert.AbortsContains(t, cur, "signature does not verify", func() {
132		Register(cross(cur), "notalice", aliceAccountPKHex, aliceSeed1Hex, "", aliceBind1Sig)
133	}, "alice's signature does not authorise bob's address")
134}
135
136// TestRegisterRejectsAReplayedBinding: the statement names the gno address, so
137// a signature harvested from alice's transaction is useless to anyone else.
138func TestRegisterRejectsAReplayedBinding(cur realm, t *testing.T) {
139	reset()
140	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
141	uassert.AbortsContains(t, cur, "signature does not verify", func() {
142		// this signature names bob's address, alice is calling
143		Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "", aliceBindForBobSig)
144	})
145}
146
147func TestRegisterRejectsDuplicates(cur realm, t *testing.T) {
148	reset()
149	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
150	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
151
152	testing.SetRealm(testing.NewUserRealm(bobAddr()))
153	uassert.AbortsContains(t, cur, "name is already taken", func() {
154		Register(cross(cur), "alice", bobAccountPKHex, aliceSeed1Hex, "", bobBind1Sig)
155	})
156	uassert.AbortsContains(t, cur, "already published under another name", func() {
157		Register(cross(cur), "alice2", aliceAccountPKHex, aliceSeed1Hex, "", aliceBind1Sig)
158	}, "one Wesh account, one directory entry")
159
160	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
161	uassert.AbortsContains(t, cur, "already owns an identity", func() {
162		Register(cross(cur), "alice2", aliceAccountPKHex, aliceSeed1Hex, "", aliceBind1Sig)
163	})
164}
165
166func TestRegisterValidatesInput(cur realm, t *testing.T) {
167	reset()
168	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
169
170	uassert.AbortsContains(t, cur, "between 3 and 32", func() {
171		Register(cross(cur), "ab", aliceAccountPKHex, aliceSeed1Hex, "", aliceBind1Sig)
172	})
173	uassert.AbortsContains(t, cur, "a-z, 0-9", func() {
174		Register(cross(cur), "al!ce", aliceAccountPKHex, aliceSeed1Hex, "", aliceBind1Sig)
175	})
176	uassert.AbortsContains(t, cur, "account key", func() {
177		Register(cross(cur), "alice", "abcd", aliceSeed1Hex, "", aliceBind1Sig)
178	})
179	uassert.AbortsContains(t, cur, "seed", func() {
180		Register(cross(cur), "alice", aliceAccountPKHex, "abcd", "", aliceBind1Sig)
181	})
182	uassert.AbortsContains(t, cur, "signature must be 64", func() {
183		Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "", "00")
184	})
185}
186
187func TestRegisterCommitted(cur realm, t *testing.T) {
188	reset()
189	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
190	RegisterCommitted(cross(cur), "alice", aliceAccountPKHex, aliceCommitHex, "", aliceCommitBindSig)
191
192	_, payload, committed, ok := Resolve("alice")
193	uassert.True(t, ok)
194	uassert.True(t, committed)
195	uassert.Equal(t, aliceCommitHex, payload)
196
197	uassert.Equal(t, "", Link("alice"), "a committed identity publishes no link")
198	uassert.Equal(t, "", RendezvousPointAt("alice", 1789171200),
199		"a committed identity does not disclose its rendezvous point")
200}
201
202// TestCommitmentOpensWithTheOutOfBandSeed: the chain attests the binding, and
203// the seed handed over privately checks against it.
204func TestCommitmentOpensWithTheOutOfBandSeed(cur realm, t *testing.T) {
205	reset()
206	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
207	RegisterCommitted(cross(cur), "alice", aliceAccountPKHex, aliceCommitHex, "", aliceCommitBindSig)
208
209	_, payload, _, _ := Resolve("alice")
210	commitment, err := wesh.DecodeCommitment(payload)
211	uassert.NoError(t, err)
212	seed, err := wesh.DecodeSeed(aliceSeed1Hex)
213	uassert.NoError(t, err)
214
215	uassert.True(t, wesh.OpenCommitment(commitment, seed, []byte("a-16-byte-salt!!")),
216		"the seed shared out of band matches what the chain attested")
217	uassert.False(t, wesh.OpenCommitment(commitment, seed, []byte("wrong-salt!!!!!!")))
218}
219
220func TestRotate(cur realm, t *testing.T) {
221	reset()
222	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
223	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
224
225	Rotate(cross(cur), aliceSeed2Hex, aliceBind2Sig)
226
227	_, payload, _, _ := Resolve("alice")
228	uassert.Equal(t, aliceSeed2Hex, payload)
229
230	id := lookup("alice")
231	uassert.Equal(t, 2, id.revision)
232	uassert.Equal(t, 2, len(id.history), "the superseded seed is kept so a stale link is recognisable")
233	uassert.Equal(t, aliceSeed1Hex, hexOf(id.history[0].payload))
234}
235
236// TestRotateRejectsAReplayedRevision is what a monotonic revision buys: an old
237// binding cannot be replayed to roll a rotation back.
238func TestRotateRejectsAReplayedRevision(cur realm, t *testing.T) {
239	reset()
240	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
241	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
242	Rotate(cross(cur), aliceSeed2Hex, aliceBind2Sig)
243
244	uassert.AbortsContains(t, cur, "signature does not verify", func() {
245		Rotate(cross(cur), aliceSeed1Hex, aliceBind1Sig)
246	}, "the revision-1 signature is dead once revision 2 exists")
247
248	// the correctly-signed revision 3 does go back to the first seed
249	Rotate(cross(cur), aliceSeed1Hex, aliceBind3Sig)
250	uassert.Equal(t, 3, lookup("alice").revision)
251}
252
253func TestRotateRejectsANoOp(cur realm, t *testing.T) {
254	reset()
255	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
256	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
257	uassert.AbortsContains(t, cur, "identical to the current one", func() {
258		Rotate(cross(cur), aliceSeed1Hex, aliceBind2Sig)
259	})
260}
261
262func TestRotateRequiresAnIdentity(cur realm, t *testing.T) {
263	reset()
264	testing.SetRealm(testing.NewUserRealm(bobAddr()))
265	uassert.AbortsContains(t, cur, "no identity registered", func() {
266		Rotate(cross(cur), aliceSeed2Hex, aliceBind2Sig)
267	})
268}
269
270// TestDeviceSigchain walks the sequence weshnet itself cannot express:
271// add a device, add a second, then revoke the first.
272func TestDeviceSigchain(cur realm, t *testing.T) {
273	reset()
274	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
275	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
276
277	uassert.Equal(t, genesisDigestHex, SigchainHead("alice"), "an empty chain starts at the genesis digest")
278	uassert.Equal(t, "unknown", DeviceStatus("alice", dev1Hex))
279
280	AppendDevice(cross(cur), genesisDigestHex, wesh.OpAdd, dev1Hex, devAdd1Sig)
281	uassert.Equal(t, devAdd1Digest, SigchainHead("alice"))
282	uassert.Equal(t, "active", DeviceStatus("alice", dev1Hex))
283
284	AppendDevice(cross(cur), devAdd1Digest, wesh.OpAdd, dev2Hex, devAdd2Sig)
285	uassert.Equal(t, devAdd2Digest, SigchainHead("alice"))
286
287	AppendDevice(cross(cur), devAdd2Digest, wesh.OpRevoke, dev1Hex, devRevoke1Sig)
288	uassert.Equal(t, devRevoke1Digest, SigchainHead("alice"))
289	uassert.Equal(t, "revoked", DeviceStatus("alice", dev1Hex), "revocation is public, ordered and attributable")
290	uassert.Equal(t, "active", DeviceStatus("alice", dev2Hex))
291	uassert.Equal(t, 1, activeDevices(lookup("alice")))
292}
293
294// TestDeviceSigchainRejectsAForkedChain: every entry chains to the digest of
295// the one before it, so the log cannot be reordered or have an entry dropped.
296func TestDeviceSigchainRejectsAForkedChain(cur realm, t *testing.T) {
297	reset()
298	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
299	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
300	AppendDevice(cross(cur), genesisDigestHex, wesh.OpAdd, dev1Hex, devAdd1Sig)
301
302	uassert.AbortsContains(t, cur, "does not match the sigchain head", func() {
303		AppendDevice(cross(cur), genesisDigestHex, wesh.OpAdd, dev2Hex, devAdd2Sig)
304	}, "the second entry must chain to the first")
305
306	uassert.AbortsContains(t, cur, "signature does not verify", func() {
307		AppendDevice(cross(cur), devAdd1Digest, wesh.OpAdd, dev2Hex, devWrongSeqSig)
308	}, "a signature made for another sequence number is not accepted at this one")
309}
310
311func TestDeviceSigchainRejectsMeaninglessOps(cur realm, t *testing.T) {
312	reset()
313	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
314	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
315	AppendDevice(cross(cur), genesisDigestHex, wesh.OpAdd, dev1Hex, devAdd1Sig)
316
317	uassert.AbortsContains(t, cur, "already active", func() {
318		AppendDevice(cross(cur), devAdd1Digest, wesh.OpAdd, dev1Hex, devAdd1Sig)
319	})
320	uassert.AbortsContains(t, cur, "not active, nothing to revoke", func() {
321		AppendDevice(cross(cur), devAdd1Digest, wesh.OpRevoke, dev2Hex, devAdd2Sig)
322	})
323	uassert.AbortsContains(t, cur, "add or revoke", func() {
324		AppendDevice(cross(cur), devAdd1Digest, "delete", dev2Hex, devAdd2Sig)
325	})
326	uassert.AbortsContains(t, cur, "device key", func() {
327		AppendDevice(cross(cur), devAdd1Digest, wesh.OpAdd, "abcd", devAdd2Sig)
328	})
329}
330
331func TestSetDisplayName(cur realm, t *testing.T) {
332	reset()
333	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
334	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
335
336	SetDisplayName(cross(cur), "Alice, support line")
337	uassert.Equal(t, "Alice, support line", lookup("alice").displayName)
338
339	testing.SetRealm(testing.NewUserRealm(bobAddr()))
340	uassert.AbortsContains(t, cur, "no identity registered", func() {
341		SetDisplayName(cross(cur), "hijacked")
342	})
343}
344
345func TestRelease(cur realm, t *testing.T) {
346	reset()
347	testing.SetRealm(testing.NewUserRealm(aliceAddr()))
348	Register(cross(cur), "alice", aliceAccountPKHex, aliceSeed1Hex, "Alice", aliceBind1Sig)
349	Release(cross(cur))
350
351	uassert.Equal(t, 0, Count())
352	_, _, _, ok := Resolve("alice")
353	uassert.False(t, ok)
354	uassert.Equal(t, "", NameOf(aliceAddr()))
355
356	// the name and the account key are both free again
357	testing.SetRealm(testing.NewUserRealm(bobAddr()))
358	Register(cross(cur), "alice", bobAccountPKHex, aliceSeed1Hex, "", bobBind1Sig)
359	uassert.Equal(t, 1, Count())
360}
361
362func TestReadersOnUnknownNames(cur realm, t *testing.T) {
363	reset()
364	uassert.Equal(t, "", Link("nope"))
365	uassert.Equal(t, "", RendezvousPointAt("nope", 0))
366	uassert.Equal(t, "", SigchainHead("nope"))
367	uassert.Equal(t, "unknown", DeviceStatus("nope", dev1Hex))
368	uassert.Equal(t, "unknown", DeviceStatus("nope", "not-hex"))
369	_, _, _, ok := Resolve("nope")
370	uassert.False(t, ok)
371}
372
373func hexOf(b []byte) string {
374	const digits = "0123456789abcdef"
375	out := make([]byte, 0, len(b)*2)
376	for _, c := range b {
377		out = append(out, digits[c>>4], digits[c&0x0f])
378	}
379	return string(out)
380}