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