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