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  }