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}