test_support.mx raw
1 //:build !wasm
2
3 package mls
4
5 import (
6 "errors"
7 "git.smesh.lol/nostr/pkg/crypto/chacha20poly1305"
8 )
9
10 // Validators for the RFC 9420 vectors carried by this package. The suite is
11 // parked with the DM feature; these stay exported so a vector harness can call
12 // them. Only the tree-math one runs under the native test harness (vectors_test.mx)
13 // - the crypto-basics ones cross the wasm crypto bridge (subtle/x25519), whose
14 // native build is a jsbridge panic, and suite 0x0001 is not implemented at all
15 // (see ciphersuite.mx).
16
17 // TestTreeMath validates tree math against RFC 9420 Appendix C test vectors.
18 // Returns nil on success, error describing first failure.
19 func TestTreeMath() (err error) {
20 treeMathVectors := []treeMathVector{
21 {
22 nLeaves: 1, nNodes: 1, root: 0,
23 left: []int32{-1},
24 right: []int32{-1},
25 parent: []int32{-1},
26 sibling: []int32{-1},
27 },
28 {
29 nLeaves: 2, nNodes: 3, root: 1,
30 left: []int32{-1, 0, -1},
31 right: []int32{-1, 2, -1},
32 parent: []int32{1, -1, 1},
33 sibling: []int32{2, -1, 0},
34 },
35 {
36 nLeaves: 4, nNodes: 7, root: 3,
37 left: []int32{-1, 0, -1, 1, -1, 4, -1},
38 right: []int32{-1, 2, -1, 5, -1, 6, -1},
39 parent: []int32{1, 3, 1, -1, 5, 3, 5},
40 sibling: []int32{2, 5, 0, -1, 6, 1, 4},
41 },
42 {
43 nLeaves: 8, nNodes: 15, root: 7,
44 left: []int32{-1, 0, -1, 1, -1, 4, -1, 3, -1, 8, -1, 9, -1, 12, -1},
45 right: []int32{-1, 2, -1, 5, -1, 6, -1, 11, -1, 10, -1, 13, -1, 14, -1},
46 parent: []int32{1, 3, 1, 7, 5, 3, 5, -1, 9, 11, 9, 7, 13, 11, 13},
47 sibling: []int32{2, 5, 0, 11, 6, 1, 4, -1, 10, 13, 8, 3, 14, 9, 12},
48 },
49 }
50 for _, tc := range treeMathVectors {
51 n := numLeaves(tc.nLeaves)
52 if w := n.width(); w != tc.nNodes {
53 return errors.New("width mismatch")
54 }
55 if r := n.root(); uint32(r) != tc.root {
56 return errors.New("root mismatch")
57 }
58 for i, want := range tc.left {
59 x := nodeIndex(i)
60 got, ok := x.left()
61 if want < 0 {
62 if ok {
63 return errors.New("left should be nil")
64 }
65 } else {
66 if !ok || int32(got) != want {
67 return errors.New("left mismatch")
68 }
69 }
70 }
71 for i, want := range tc.right {
72 x := nodeIndex(i)
73 got, ok := x.right()
74 if want < 0 {
75 if ok {
76 return errors.New("right should be nil")
77 }
78 } else {
79 if !ok || int32(got) != want {
80 return errors.New("right mismatch")
81 }
82 }
83 }
84 for i, want := range tc.parent {
85 x := nodeIndex(i)
86 got, ok := n.parent(x)
87 if want < 0 {
88 if ok {
89 return errors.New("parent should be nil")
90 }
91 } else {
92 if !ok || int32(got) != want {
93 return errors.New("parent mismatch")
94 }
95 }
96 }
97 for i, want := range tc.sibling {
98 x := nodeIndex(i)
99 got, ok := n.sibling(x)
100 if want < 0 {
101 if ok {
102 return errors.New("sibling should be nil")
103 }
104 } else {
105 if !ok || int32(got) != want {
106 return errors.New("sibling mismatch")
107 }
108 }
109 }
110 }
111 return nil
112 }
113
114 // TestCryptoBasics validates suite 0x0003 crypto primitives against
115 // RFC 9420 crypto-basics test vectors (cipher_suite: 3).
116 // Returns nil on success, error describing first failure.
117 func TestCryptoBasics() (err2 error) {
118 cs := CipherSuite0x0003
119
120 // AEAD round-trip sanity (not a vector, but isolates AEAD from HPKE).
121 {
122 var k [32]byte
123 var n [12]byte
124 for i := 0; i < 32; i++ {
125 k[i] = byte(i)
126 }
127 for i := 0; i < 12; i++ {
128 n[i] = byte(i + 1)
129 }
130 pt := []byte("Hello, MLS!")
131 ct := chacha20poly1305.Seal(k, n, pt, nil)
132 got, ok := chacha20poly1305.Open(k, n, ct, nil)
133 if !ok {
134 return errors.New("aead roundtrip: Open returned !ok")
135 }
136 if !bytesEqual(got, pt) {
137 return errors.New("aead roundtrip: plaintext mismatch")
138 }
139 }
140
141 // RFC 8439 ยง2.8.2 full ChaCha20-Poly1305 test vector.
142 {
143 key := hexb("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
144 nonce := hexb("070000004041424344454647")
145 aad := hexb("50515253c0c1c2c3c4c5c6c7")
146 pt := hexb("4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e")
147 var k [32]byte
148 var n [12]byte
149 copy(k[:], key)
150 copy(n[:], nonce)
151 expCT := hexb("d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116")
152 expTag := hexb("1ae10b594f09e26a7e902ecbd0600691")
153 expSealed := expCT | expTag
154 got := chacha20poly1305.Seal(k, n, pt, aad)
155 if !bytesEqual(got, expSealed) {
156 return errors.New("rfc8439 AEAD Seal mismatch")
157 }
158 dec, ok := chacha20poly1305.Open(k, n, got, aad)
159 if !ok {
160 return errors.New("rfc8439 AEAD Open failed")
161 }
162 if !bytesEqual(dec, pt) {
163 return errors.New("rfc8439 AEAD Open mismatch")
164 }
165 }
166
167 // refHash
168 {
169 out, err := cs.refHash(
170 []byte("RefHash"),
171 hexb("4f0c86f9c82fba0a896bd7eecf79a29856e98a7e4f13b9f841ae285d70ed8b68"),
172 )
173 if err != nil {
174 return err
175 }
176 if !bytesEqual(out, hexb("f11019703c8b630060839b12a475fd39c6a30f8a866790ff46a35f9c65e1df3c")) {
177 return errors.New("refHash mismatch")
178 }
179 }
180
181 // expandWithLabel
182 {
183 out, err := cs.expandWithLabel(
184 hexb("55aa3ae5242564782567ce097beafe19510230660008b2cc064a78387fa16f36"),
185 []byte("ExpandWithLabel"),
186 hexb("2e07148f4340c62a55e7608c20d73fddf1f3b8dafb2c7ef24eceb70e136c0d8c"),
187 32,
188 )
189 if err != nil {
190 return err
191 }
192 if !bytesEqual(out, hexb("1df5ba7996a34f75d717916a094a14083c03a75e80f0330a8095f5f11cfe1e1f")) {
193 return errors.New("expandWithLabel mismatch")
194 }
195 }
196
197 // deriveSecret
198 {
199 out, err := cs.deriveSecret(
200 hexb("cae460c779ebaa3e81c061a371486dff1ed1ff273bea369cc0fc46550b83c407"),
201 []byte("DeriveSecret"),
202 )
203 if err != nil {
204 return err
205 }
206 if !bytesEqual(out, hexb("aad859818ca5f2a9896d4d3ee2dccc0cefcd69b666bdb16b52f1de15fb1a5567")) {
207 return errors.New("deriveSecret mismatch")
208 }
209 }
210
211 // deriveTreeSecret
212 {
213 out, err := deriveTreeSecret(cs,
214 hexb("c994e257b53f726087ddd7121876f558f1fbd6f807e5ff010830d618d7bab6f2"),
215 []byte("DeriveTreeSecret"),
216 2694881440,
217 32,
218 )
219 if err != nil {
220 return err
221 }
222 if !bytesEqual(out, hexb("2095d6a81ab87095d1df26f6bdf012ec06f197e418381c1795a7b758603c936d")) {
223 return errors.New("deriveTreeSecret mismatch")
224 }
225 }
226
227 // signWithLabel: verify reference signature
228 {
229 ok := cs.verifyWithLabel(
230 hexb("18275f892ee0ca6f4687ff26c990776387502646ff658c3f572b324faecb05c5"),
231 []byte("SignWithLabel"),
232 hexb("df308cf2dbf471edf2c29d30e3daf161b5b87d350ee3b2c715c298ec3d10d432"),
233 hexb("4f56851c2c47f5115a61ff0ab6121b4a4732d4e94805fc7135a5132f87d5ca5f1dc7408816c1ea4f25887725cf5914b48c427a52cabcfeb746a2b8a12e821f08"),
234 )
235 if !ok {
236 return errors.New("signWithLabel: reference signature verify failed")
237 }
238 }
239
240 // signWithLabel: sign + verify round-trip
241 {
242 sig, err := cs.signWithLabel(
243 hexb("4e312160ee4981358db479aa877412847abc7f7054b5605511256c395404d054"),
244 []byte("SignWithLabel"),
245 hexb("df308cf2dbf471edf2c29d30e3daf161b5b87d350ee3b2c715c298ec3d10d432"),
246 )
247 if err != nil {
248 return err
249 }
250 ok := cs.verifyWithLabel(
251 hexb("18275f892ee0ca6f4687ff26c990776387502646ff658c3f572b324faecb05c5"),
252 []byte("SignWithLabel"),
253 hexb("df308cf2dbf471edf2c29d30e3daf161b5b87d350ee3b2c715c298ec3d10d432"),
254 sig,
255 )
256 if !ok {
257 return errors.New("signWithLabel: round-trip verify failed")
258 }
259 }
260
261 // decryptWithLabel: decrypt reference ciphertext
262 {
263 pt, err := cs.decryptWithLabel(
264 hexb("9d122ad4638fcb301b6eb5f4073414afb44bb34d37b4ddee9975b2941d700edb"),
265 []byte("EncryptWithLabel"),
266 hexb("0d6a5cf9ee88b1f8c79d8512477d9bfc5496c207c8173f8dcac0368b4dba7407"),
267 hexb("f26e9e5a94396a90f85a5f72eedf3dacfb1b7f4164e0573edeb9c6c912e1cb49"),
268 hexb("40dd09ad4c5dc29d373f814bf054c9359cb75a468bc4d2c8bbcffb072a73105c4d9416ebd4fafeb62e59a9dea55da3cd"),
269 )
270 if err != nil {
271 return errors.New("decryptWithLabel error: " | err.Error())
272 }
273 expected := hexb("1dd4c1904996ce7d42cee7de68881459fa7a345da59a02040ade37103505baf6")
274 if !bytesEqual(pt, expected) {
275 return errors.New("decryptWithLabel mismatch")
276 }
277 }
278
279 return nil
280 }
281
282 // TestCryptoBasics0x0001 validates suite 0x0001
283 // (DHKEM(X25519) + AES-128-GCM + Ed25519) against the RFC 9420 crypto-basics
284 // test vectors for cipher_suite=1. 0x0001 shares KEM and signature with 0x0003;
285 // only the AEAD and hpkeSuiteID differ.
286 func TestCryptoBasics0x0001() (err2 error) {
287 cs := CipherSuite0x0001
288
289 // refHash (uses SHA-256 - same for both suites, but verify dispatch works).
290 {
291 out, err := cs.refHash(
292 []byte("RefHash"),
293 hexb("40312db83f651883c05ab26fa12c6af61930015c81947cfd0f129e6d99210bb2"),
294 )
295 if err != nil {
296 return err
297 }
298 if !bytesEqual(out, hexb("e8027fffc5f9bb469f29172538dc0f3a78f14f323495bbd2217eba7a77fb242a")) {
299 return errors.New("0x0001 refHash mismatch")
300 }
301 }
302
303 // expandWithLabel (length=16 exercises AEAD-key-sized expand).
304 {
305 out, err := cs.expandWithLabel(
306 hexb("1499360a561335f4ef51d0a1b0d586900dc8007ae405b1ab79bf4207bb3d67e4"),
307 []byte("ExpandWithLabel"),
308 hexb("2ff8c1f9d9c1248f82e372ddb5791c771695e01882abca6a64097bd2f04c971f"),
309 16,
310 )
311 if err != nil {
312 return err
313 }
314 if !bytesEqual(out, hexb("c1e8eb360391526c0c64039f13e0c5b1")) {
315 return errors.New("0x0001 expandWithLabel mismatch")
316 }
317 }
318
319 // deriveSecret.
320 {
321 out, err := cs.deriveSecret(
322 hexb("1a9ce178a53f8752d2513c27efe9c85133f6c0a97f7b35ac200695024a77228e"),
323 []byte("DeriveSecret"),
324 )
325 if err != nil {
326 return err
327 }
328 if !bytesEqual(out, hexb("3b08c195a246c4ad469c1d11c10e62890d8fa6b684494ff925409efdb1ff0464")) {
329 return errors.New("0x0001 deriveSecret mismatch")
330 }
331 }
332
333 // deriveTreeSecret.
334 {
335 out, err := deriveTreeSecret(cs,
336 hexb("5133c6f8bad297f5d3beacdf477f0c45ec51b02de659d305220c5f9385c6eb43"),
337 []byte("DeriveTreeSecret"),
338 2694881440,
339 32,
340 )
341 if err != nil {
342 return err
343 }
344 if !bytesEqual(out, hexb("8461f3ccc603eae52149a23a4134d29c880a1ad1ba70441e5d586e3521ec7b25")) {
345 return errors.New("0x0001 deriveTreeSecret mismatch")
346 }
347 }
348
349 // signWithLabel: verify reference signature (Ed25519 - same primitive as
350 // 0x0003, routed via 0x0001 dispatch).
351 {
352 ok := cs.verifyWithLabel(
353 hexb("85600e54e5c2919ccbd0742126e5d837cf7a2ba50d75a69b3f35dcfe4a50ffe2"),
354 []byte("SignWithLabel"),
355 hexb("cd289cc7ba2869f64f3c32ffd133f500d17abace919a5ffe7faa974200d81932"),
356 hexb("996bd223ddb4d55a2b57d85cb2944f21facc95696053ddf66d590060fdc719f4a26c6212ce605414e0d5e66a55921dd99d11122218c35bc23408b0076e8bc40b"),
357 )
358 if !ok {
359 return errors.New("0x0001 signWithLabel: reference signature verify failed")
360 }
361 }
362
363 // signWithLabel: sign + verify round-trip.
364 {
365 sig, err := cs.signWithLabel(
366 hexb("a2f640dd5005fcad6adb8e9bd8b60d70946bb802e1e788307929fdac81e1ec74"),
367 []byte("SignWithLabel"),
368 hexb("cd289cc7ba2869f64f3c32ffd133f500d17abace919a5ffe7faa974200d81932"),
369 )
370 if err != nil {
371 return err
372 }
373 ok := cs.verifyWithLabel(
374 hexb("85600e54e5c2919ccbd0742126e5d837cf7a2ba50d75a69b3f35dcfe4a50ffe2"),
375 []byte("SignWithLabel"),
376 hexb("cd289cc7ba2869f64f3c32ffd133f500d17abace919a5ffe7faa974200d81932"),
377 sig,
378 )
379 if !ok {
380 return errors.New("0x0001 signWithLabel: round-trip verify failed")
381 }
382 }
383
384 // decryptWithLabel: decrypt reference HPKE ciphertext (exercises AES-128-GCM
385 // within HPKE-base with 0x0001 suite_id).
386 {
387 pt, err := cs.decryptWithLabel(
388 hexb("fb1ade7939987ff12a9d620772b1f9f7caeba26f8a3ecea9617d9402cd862444"),
389 []byte("EncryptWithLabel"),
390 hexb("26347dd7f218d1de8673d6a66646ce06ac5fd3aa8d5c33f65d86aeefdcf4a31e"),
391 hexb("0a144e8fbf2d6dcf6fe9d2e2b8aeca5461ff5b0ea9c0ede1040c3dc7ed1dfd1c"),
392 hexb("15c80ea2bc37db221baa530ef5aea88650f0ce0f262803d6f78f3a1392f7ccd960eff94ca081ee54efa4c3acfa0eb591"),
393 )
394 if err != nil {
395 return errors.New("0x0001 decryptWithLabel error: " | err.Error())
396 }
397 expected := hexb("8f55dd30f03d64335c22b53ea7670bb1becf49b04021f706368fe93eeb358f46")
398 if !bytesEqual(pt, expected) {
399 return errors.New("0x0001 decryptWithLabel plaintext mismatch")
400 }
401 }
402
403 return nil
404 }
405
406 func hexb(s string) (buf []byte) {
407 n := len(s) / 2
408 out := []byte{:n}
409 for i := 0; i < n; i++ {
410 out[i] = hexdig(s[2*i])<<4 | hexdig(s[2*i+1])
411 }
412 return out
413 }
414
415
416 func hexenc(b []byte) (s string) {
417 hexchars := []byte("0123456789abcdef")
418 out := []byte{:len(b)*2}
419 for i := 0; i < len(b); i++ {
420 out[2*i] = hexchars[b[i]>>4]
421 out[2*i+1] = hexchars[b[i]&0x0f]
422 }
423 return string(out)
424 }
425
426 func hexdig(c byte) (b byte) {
427 if c >= '0' && c <= '9' {
428 return c - '0'
429 }
430 if c >= 'a' && c <= 'f' {
431 return c - 'a' + 10
432 }
433 return 0
434 }
435
436 // treeMathVector holds one test case. -1 means null.
437 type treeMathVector struct {
438 nLeaves uint32
439 nNodes uint32
440 root uint32
441 left []int32
442 right []int32
443 parent []int32
444 sibling []int32
445 }
446
447 // RFC 9420 tree-math vectors for n_leaves = 1, 2, 4, 8.
448