test_key_schedule.mx raw

   1  //go:build !wasm
   2  
   3  package mls
   4  
   5  import "errors"
   6  
   7  // TestKeySchedule validates key schedule derivation against RFC 9420 test vectors
   8  // (cipher_suite 3, key-schedule.json index 2). 5 epochs, full derivation chain.
   9  func TestKeySchedule() error {
  10  	cs := CipherSuite0x0003
  11  
  12  	groupID := hexb("a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e")
  13  	initSecret := hexb("a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e")
  14  
  15  	for i, ep := range ksEpochs {
  16  		ctx := &groupContext{
  17  			version:                 protocolVersionMLS10,
  18  			cipherSuite:             cs,
  19  			groupID:                 GroupID(groupID),
  20  			epoch:                   uint64(i),
  21  			treeHash:                ep.treeHash,
  22  			confirmedTranscriptHash: ep.confirmedTranscriptHash,
  23  		}
  24  
  25  		// Verify serialized group context matches vector.
  26  		raw, err := marshalRaw(ctx)
  27  		if err != nil {
  28  			return errors.New("epoch " | itoa(i) | ": marshalRaw: " | err.Error())
  29  		}
  30  		if !bytesEqual(raw, ep.groupContext) {
  31  			return errors.New("epoch " | itoa(i) | ": group_context mismatch: got " | hexenc(raw) | " want " | hexenc(ep.groupContext))
  32  		}
  33  
  34  		// extractJoinerSecret
  35  		joinerSecret, err := ctx.extractJoinerSecret(initSecret, ep.commitSecret)
  36  		if err != nil {
  37  			return errors.New("epoch " | itoa(i) | ": extractJoinerSecret: " | err.Error())
  38  		}
  39  		if !bytesEqual(joinerSecret, ep.joinerSecret) {
  40  			return errors.New("epoch " | itoa(i) | ": joiner_secret mismatch: got " | hexenc(joinerSecret) | " want " | hexenc(ep.joinerSecret))
  41  		}
  42  
  43  		// extractWelcomeSecret
  44  		welcomeSecret, err := extractWelcomeSecret(cs, joinerSecret, ep.pskSecret)
  45  		if err != nil {
  46  			return errors.New("epoch " | itoa(i) | ": extractWelcomeSecret: " | err.Error())
  47  		}
  48  		if !bytesEqual(welcomeSecret, ep.welcomeSecret) {
  49  			return errors.New("epoch " | itoa(i) | ": welcome_secret mismatch")
  50  		}
  51  
  52  		// extractEpochSecret
  53  		epochSecret, err := ctx.extractEpochSecret(joinerSecret, ep.pskSecret)
  54  		if err != nil {
  55  			return errors.New("epoch " | itoa(i) | ": extractEpochSecret: " | err.Error())
  56  		}
  57  
  58  		// Derive all 8 labeled secrets from epochSecret.
  59  		type labelCheck struct {
  60  			label []byte
  61  			want  []byte
  62  		}
  63  		checks := []labelCheck{
  64  			{secretLabelInit, ep.initSecret},
  65  			{secretLabelSenderData, ep.senderDataSecret},
  66  			{secretLabelEncryption, ep.encryptionSecret},
  67  			{secretLabelExporter, ep.exporterSecret},
  68  			{secretLabelExternal, ep.externalSecret},
  69  			{secretLabelConfirm, ep.confirmationKey},
  70  			{secretLabelMembership, ep.membershipKey},
  71  			{secretLabelResumption, ep.resumptionPSK},
  72  		}
  73  		for _, chk := range checks {
  74  			got, err := cs.deriveSecret(epochSecret, chk.label)
  75  			if err != nil {
  76  				return errors.New("epoch " | itoa(i) | ": deriveSecret(" | string(chk.label) | "): " | err.Error())
  77  			}
  78  			if !bytesEqual(got, chk.want) {
  79  				return errors.New("epoch " | itoa(i) | ": deriveSecret(" | string(chk.label) | ") mismatch: got " | hexenc(got) | " want " | hexenc(chk.want))
  80  			}
  81  		}
  82  
  83  		// epoch_authenticator = deriveSecret(epochSecret, "authentication")
  84  		epochAuth, err := cs.deriveSecret(epochSecret, secretLabelAuthentication)
  85  		if err != nil {
  86  			return errors.New("epoch " | itoa(i) | ": deriveSecret(authentication): " | err.Error())
  87  		}
  88  		if !bytesEqual(epochAuth, ep.epochAuthenticator) {
  89  			return errors.New("epoch " | itoa(i) | ": epoch_authenticator mismatch")
  90  		}
  91  
  92  		// deriveEncryptionKeyPair from externalSecret → compare pub to externalPub
  93  		externalPub, _, err := cs.deriveEncryptionKeyPair(ep.externalSecret)
  94  		if err != nil {
  95  			return errors.New("epoch " | itoa(i) | ": deriveEncryptionKeyPair: " | err.Error())
  96  		}
  97  		if !bytesEqual([]byte(externalPub), ep.externalPub) {
  98  			return errors.New("epoch " | itoa(i) | ": external_pub mismatch: got " | hexenc([]byte(externalPub)) | " want " | hexenc(ep.externalPub))
  99  		}
 100  
 101  		// deriveExporter
 102  		exporterValue, err := deriveExporter(cs, ep.exporterSecret, ep.exporterLabel, ep.exporterContext, uint16(ep.exporterLength))
 103  		if err != nil {
 104  			return errors.New("epoch " | itoa(i) | ": deriveExporter: " | err.Error())
 105  		}
 106  		if !bytesEqual(exporterValue, ep.exporterValue) {
 107  			return errors.New("epoch " | itoa(i) | ": exporter mismatch: got " | hexenc(exporterValue) | " want " | hexenc(ep.exporterValue))
 108  		}
 109  
 110  		// Advance initSecret for next epoch.
 111  		initSecret = ep.initSecret
 112  	}
 113  
 114  	return nil
 115  }
 116  
 117  func itoa(n int) string {
 118  	if n == 0 {
 119  		return "0"
 120  	}
 121  	buf := []byte{:8}
 122  	i := len(buf)
 123  	for n > 0 {
 124  		i--
 125  		buf[i] = byte('0' + n%10)
 126  		n /= 10
 127  	}
 128  	return string(buf[i:])
 129  }
 130  
 131  type ksEpoch struct {
 132  	// Inputs
 133  	commitSecret            []byte
 134  	treeHash                []byte
 135  	confirmedTranscriptHash []byte
 136  	pskSecret               []byte
 137  
 138  	// Serialized group context (for verification)
 139  	groupContext []byte
 140  
 141  	// Key schedule outputs
 142  	joinerSecret     []byte
 143  	welcomeSecret    []byte
 144  	initSecret       []byte
 145  	senderDataSecret []byte
 146  	encryptionSecret []byte
 147  	exporterSecret   []byte
 148  	externalSecret   []byte
 149  	confirmationKey  []byte
 150  	membershipKey    []byte
 151  	resumptionPSK    []byte
 152  
 153  	// epoch_authenticator
 154  	epochAuthenticator []byte
 155  
 156  	// External keypair (pub derived from externalSecret)
 157  	externalPub []byte
 158  
 159  	// Exporter test
 160  	exporterLabel   []byte
 161  	exporterContext []byte
 162  	exporterLength  int
 163  	exporterValue   []byte
 164  }
 165  
 166  var ksEpochs = []ksEpoch{
 167  	// --- epoch 0 ---
 168  	{
 169  		commitSecret:            hexb("a22606222e350fd7f0937168fe7548fb06626ab143cba7611d641693b1447509"),
 170  		treeHash:                hexb("9769e302a99c457350a8e636009b12a2fee068664004606d6318eb3a1977d818"),
 171  		confirmedTranscriptHash: hexb("5e57c9364dc71f0f71b19ffe561ab77257c490708a47e29f8f73f2b318201d2f"),
 172  		pskSecret:               hexb("e871b247379522395689182736cb3d1e7b108d6ae934b802223975de8dc3f80b"),
 173  		groupContext:            hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e0000000000000000209769e302a99c457350a8e636009b12a2fee068664004606d6318eb3a1977d818205e57c9364dc71f0f71b19ffe561ab77257c490708a47e29f8f73f2b318201d2f00"),
 174  		joinerSecret:            hexb("3e28da76edc09fb9ad59fae258839c7dc46e3c092a125499959c7413a60250b2"),
 175  		welcomeSecret:           hexb("b0defbdd2232224b0c0427e8efa80f011f7813291dca783433f2da1431620bbc"),
 176  		initSecret:              hexb("418b197eafd925ebbf4bfd94d650aa83b1a11d6d02f33c2cc81631c6734f69d9"),
 177  		senderDataSecret:        hexb("de1df3a74bbcfc7fcc631213a20c1b1842860eab8e6f0c864dcfb541cd42cf24"),
 178  		encryptionSecret:        hexb("ffcc3d4a757224eaf62c124f8e7def12c0db74740cf494c9f56fa7dd07214947"),
 179  		exporterSecret:          hexb("27518b380b39834affecb08780ee9709627859d5f6f37994e8783791004485cb"),
 180  		externalSecret:          hexb("46f51f54ce4c3457ee5681925b9d1a282de166f04e28a4a316404bd14dda3138"),
 181  		confirmationKey:         hexb("e8bdff522e2675c7e0582321fbeb7e61763b1f88e7ded3c57ea78c691e1d0b93"),
 182  		membershipKey:           hexb("6839abba79aaeb82385397612fb90cbea3bf8d427806cb3f0bfe5793c1a42fc9"),
 183  		resumptionPSK:           hexb("244b05004ced1a7d1dc3da6a7541e9b180b6ffe41cc6e24d63c5c9c0742b4870"),
 184  		epochAuthenticator:      hexb("f68f6735aeeb97331d674ef4f580e11352beff543b3b6688a01a1bab97d42f26"),
 185  		externalPub:             hexb("8206ea1eb4d8d5730a2737f7470718b9d00c2276d24a98ac4e6d7ef52cba0631"),
 186  		exporterLabel:           []byte("9ba13d54ecdec7cbefcb47b4268d7b1990fabc6d6e67681e167959389d84e4e4"),
 187  		exporterContext:         hexb("884f1af892ab002f5be4c5d5081ade9e0e6418c6ea7a9a92e90534f19dcef785"),
 188  		exporterLength:          32,
 189  		exporterValue:           hexb("623c858acd2728c5b860a77ae0cde77fa8aef14e9ac124464cab06bbc3cf3635"),
 190  	},
 191  	// --- epoch 1 ---
 192  	{
 193  		commitSecret:            hexb("7b3027aa5d2224aab7e2a18660bbf57930e2e21d95e02b849c704d970e3e28c5"),
 194  		treeHash:                hexb("826a4d3b0956277ce5e272e4d18fdca023ffb63ea4cea636e34cc837ae7c5c5d"),
 195  		confirmedTranscriptHash: hexb("14a2985ea47db0685924a74d47ac8a08ec241f843b536dd1348e3ffb2d78184e"),
 196  		pskSecret:               hexb("ca7a68f2a8a52147d70f1eb7195de968d2e182b93596bc5a61393861e91180e4"),
 197  		groupContext:            hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e000000000000000120826a4d3b0956277ce5e272e4d18fdca023ffb63ea4cea636e34cc837ae7c5c5d2014a2985ea47db0685924a74d47ac8a08ec241f843b536dd1348e3ffb2d78184e00"),
 198  		joinerSecret:            hexb("f589c7bc9de6fac35e546bbf4ac89632980158e40336cdf89165b526a482f228"),
 199  		welcomeSecret:           hexb("f577c28c938b9f57224e5f208ee8a64f65a4fb734bf8f74f44f0020f55e22a20"),
 200  		initSecret:              hexb("6947180097f92f021d5d0739568d9e41647ebe26d754f679f0a883013165b6c9"),
 201  		senderDataSecret:        hexb("c6b20eeb67246447456fe4d4dd9ee8e6e88e710cf766f862440e9a7b4789be81"),
 202  		encryptionSecret:        hexb("bd364a14dbe3e9d14f6573d2df1ee014cb0bfcadd948f257d35091fba8735d3a"),
 203  		exporterSecret:          hexb("eac27705796cb8ff5867d5883f111f8a9990de3e69befc5d69bf2d02f539b863"),
 204  		externalSecret:          hexb("e7b78eda3dc2d13c3aee74426a6aa6e8a90fb95992be66b5eeb8cdc0b08c6f1a"),
 205  		confirmationKey:         hexb("839d6621c61a40a14d30a5042aa9a530e267f825734d485ac9e415f19e35d9dd"),
 206  		membershipKey:           hexb("d835002d8367b0c81e7d0159527b12a24870dbb8efc8fb299ad16b72b2199a6c"),
 207  		resumptionPSK:           hexb("e2cc0c5444b77af2d15a8d5adf2ce9fd051429758ab80bd247d80f6c44982a64"),
 208  		epochAuthenticator:      hexb("acd66baee8206cca6b60a71c9db4f1ae97718db857e267c352d495f29ff0776b"),
 209  		externalPub:             hexb("e2daac515f8378d164bf51746f4143b54d7a8d574f4ae1081b5d1d0ba88fe615"),
 210  		exporterLabel:           []byte("ed66d7f1da52171ac9448f0f902edcfefa4ebbda843a43bd3d173cb7c5b4331e"),
 211  		exporterContext:         hexb("02dc18fc5bc4d9093cf41fa0053521653775b123784d40ac7d46cc5a72ef4d46"),
 212  		exporterLength:          32,
 213  		exporterValue:           hexb("f6ae0abce67ed43e7cda3c04774278930c96bb1abfa77707cb7ac3351a9ded5f"),
 214  	},
 215  	// --- epoch 2 ---
 216  	{
 217  		commitSecret:            hexb("d2825785628f1ea7404d6761f27272af5f99416ea28cc9d335df47ed2b0097d4"),
 218  		treeHash:                hexb("661ce3bd9ebec8608fa97bf5413a4588f50a8f9face225ec67a6d29c862b2516"),
 219  		confirmedTranscriptHash: hexb("b5d7ec8c9d8b6a28c9467fe4918844be6acc08e98c1e10c71122e95f9a5e78c5"),
 220  		pskSecret:               hexb("599aa672406270914c60d30b7a31d2f2e217c3b5298b279b79e34c65a60e5f24"),
 221  		groupContext:            hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e000000000000000220661ce3bd9ebec8608fa97bf5413a4588f50a8f9face225ec67a6d29c862b251620b5d7ec8c9d8b6a28c9467fe4918844be6acc08e98c1e10c71122e95f9a5e78c500"),
 222  		joinerSecret:            hexb("2895d244e83e8f29025bc990bedff61ce114281e4ff22362c4ddd1646e0fd052"),
 223  		welcomeSecret:           hexb("58a7d5da2ab6a8cbebb89c34741c52ce3f4aab1b0ca3fe53f47c25f77de9576c"),
 224  		initSecret:              hexb("300b71ecfff9459934b4ede696bc0ee9858aa70a89c0e934fadad97383347e4c"),
 225  		senderDataSecret:        hexb("498d6f93ac48b8a8d3ba1321b4baf760fe59d486be53208ad0c2f57a34ce6263"),
 226  		encryptionSecret:        hexb("a61b4a90c66febc563f95e7af4417e1be5e33181605b2b9f8725ea30b184928e"),
 227  		exporterSecret:          hexb("fdac6dcb3929509f612106168550370afd92beeaf838bc09bad0a4db09c03b4e"),
 228  		externalSecret:          hexb("072b9f6d87abb70c908e0808fb4f60e0680426d12c64d4aa8389bf6c423b6cc3"),
 229  		confirmationKey:         hexb("3cdd1841fe689fad3c4afca9d78e20506cbc377c34e67245721448d944e63ace"),
 230  		membershipKey:           hexb("08caa1215ef79aab5fb229c286b82c2a5702932aeb68190496b18cf537c86e5c"),
 231  		resumptionPSK:           hexb("887ca27b1d9f00bb88d60a6ecb86df34bf500bf0f755e7d02b6fd14457fff9d9"),
 232  		epochAuthenticator:      hexb("1b9bbd13d92e2b4d600af772e340c8130b5704ad81cf17766ff5649603d058b3"),
 233  		externalPub:             hexb("c32e86b676cdcf9daf07823faad7bf0d650ea5b1593bf816573236a0bc744571"),
 234  		exporterLabel:           []byte("06f549e9bf966d7b7135b6ef6d4e3032a1c720adf0281c3ef1c0beac0da88621"),
 235  		exporterContext:         hexb("b0fa7e3f0f2199278a55267d551d43946bbfc6d847632867dd86abd1217982a5"),
 236  		exporterLength:          32,
 237  		exporterValue:           hexb("58560324e8b45296b31c44f250df77194b977cd4f9b1c0fd55b15cd260a3d582"),
 238  	},
 239  	// --- epoch 3 ---
 240  	{
 241  		commitSecret:            hexb("f652baa9151c9719ecb2716240a2a5ed9aeede1df19de0de862ded166a724783"),
 242  		treeHash:                hexb("08225ed7f3b0b8aa9f03b24395ed8ee7002d38209fddd7d941dd8ac629ac8a62"),
 243  		confirmedTranscriptHash: hexb("dab8c2f8ec97a0e5a137c55a5b9ac1ccdf5ae8329810e98e0bc3930aae0b4be1"),
 244  		pskSecret:               hexb("4106e07ffe8f0bfdbfd317d92e37a1fc6c4d1fba53ee054b7acf8587013d533b"),
 245  		groupContext:            hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e00000000000000032008225ed7f3b0b8aa9f03b24395ed8ee7002d38209fddd7d941dd8ac629ac8a6220dab8c2f8ec97a0e5a137c55a5b9ac1ccdf5ae8329810e98e0bc3930aae0b4be100"),
 246  		joinerSecret:            hexb("02a17a6d0995432062b8825fa071fabefb0aecadbe9864d229e82ea73abaf71d"),
 247  		welcomeSecret:           hexb("c935f0face4e94e6f7e886a9df715bfcdcdc3d3a546601fc4ffa7f46b10904f6"),
 248  		initSecret:              hexb("561d5fe3f86642d8b9782d6227bb4b0dae805e6930432ed1b45334e14ff5225a"),
 249  		senderDataSecret:        hexb("785fd9a3bbe87178b2b26aa494851d0528a1f39430c1b787c4d48ccacb1012ce"),
 250  		encryptionSecret:        hexb("72842779a3f83949693f20efb2a64c60a43a8e5b12981d92b43660508869d9d9"),
 251  		exporterSecret:          hexb("db2749738091f49e479a570001f2daa7dc4aeeca2e1cfbb60f7d983b3dfd427c"),
 252  		externalSecret:          hexb("75edc2d917753b5238d08a8549150672d8730731fddcd1aac9ac4dcdffab1fe6"),
 253  		confirmationKey:         hexb("b531f478b39a21ff975491f292bd971764b1c99ec5bec5ba5e12f2922fa13c9d"),
 254  		membershipKey:           hexb("d68c57291ad3a57f20ace9386e52187581a3de9820938bd0424696287df64622"),
 255  		resumptionPSK:           hexb("89b4a85bf900b6ba890a89eae531bb58c5b3b48f406918846a2d7195088edf04"),
 256  		epochAuthenticator:      hexb("5c811e1b167d6ccbbd1ddafe4df66f833e418cadd562e23e6ff7d8c8103f8ca3"),
 257  		externalPub:             hexb("1e8f03db3e2f5d6fa885455904f726f426354a441c5317a271417360ef7b9d2e"),
 258  		exporterLabel:           []byte("b76193af29eadc6e16c66493e2a4ef8219aad79986b6d4911493741ec1e666cc"),
 259  		exporterContext:         hexb("941db06073e76050679d33cf1f0ec33de3e5a2cd00cb738b54c0dd90de251cdd"),
 260  		exporterLength:          32,
 261  		exporterValue:           hexb("4e2dd79d84c728b7503cee9acbe1e057523d0ff973d81e011905b5b581d2adb3"),
 262  	},
 263  	// --- epoch 4 ---
 264  	{
 265  		commitSecret:            hexb("50fb68cdd31ff76b8d86b88b80124534d845cf2a835411b002b40b7b08d28278"),
 266  		treeHash:                hexb("918e07d9bfd965f880f860830b24427d9200fcac485e973b4943e67d322c1682"),
 267  		confirmedTranscriptHash: hexb("5e88437e7e8e91582bc440a375e93280417c94c5e38db8537963dd3750dd3e16"),
 268  		pskSecret:               hexb("c7e0f52b886962b1edde9e75b9ecafa0d8efeffb474732a3da298c470f1d1445"),
 269  		groupContext:            hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e000000000000000420918e07d9bfd965f880f860830b24427d9200fcac485e973b4943e67d322c1682205e88437e7e8e91582bc440a375e93280417c94c5e38db8537963dd3750dd3e1600"),
 270  		joinerSecret:            hexb("e3e8700db2c43917c6e36dae28732b21c285f9af371f66a5a9f5f9a70c65e1b2"),
 271  		welcomeSecret:           hexb("6f6494718441537ce4cea94ad29fb66d3048c711e61f7f8aefccf4f6d61fcd86"),
 272  		initSecret:              hexb("20315bc816babd2c11b78bbcac01baa787136a26baf80adc0753d163c9b09d89"),
 273  		senderDataSecret:        hexb("1535616e267425ab17e48a8607d9edc196601579303592885db62c71efbbdcbf"),
 274  		encryptionSecret:        hexb("7e893a0ef9b0b4b1708213996b2ae8c8382a50fbce6caa4496e07093c36de351"),
 275  		exporterSecret:          hexb("7cd91ce426cada365fc15f9fb060c360ad81bc350096b404bc3d66a7d64a00cc"),
 276  		externalSecret:          hexb("b3f67ce4547ae1e886e2275aa3760146d55cc9a1746bd36b0291c127368c7c38"),
 277  		confirmationKey:         hexb("9a34f258b8c3a3733d734a3c84ebf3683f34fa67760eeabf1e9a9b329a733d56"),
 278  		membershipKey:           hexb("3dadf8dbb09c5c1e25c565dc7cffc494055e517b0129e0bc83fcbb7280e9ae7f"),
 279  		resumptionPSK:           hexb("e914d45c8c1b06670b926ae56a7908c93d2721cc562f82778044e9888dc9e2d1"),
 280  		epochAuthenticator:      hexb("d59f86014e0c306be049bd9f2dbae06a3d225486e74042dba30238e4d72ab89e"),
 281  		externalPub:             hexb("8236c9f14f1d5bc27635be4da2795b5367660879f5afe3951b7f1eb7b69fd769"),
 282  		exporterLabel:           []byte("cc9c4b25b0bd69250b6e4f9908d1b170bf62fe8cc11ad36d33e602324ac0662a"),
 283  		exporterContext:         hexb("a0f762fc82d5e421d8fdf8a317b2fd463008c625bf19db7fbfa4ac778b5106a2"),
 284  		exporterLength:          32,
 285  		exporterValue:           hexb("b07890e33ce8c281d07c7228c186c48f882a70967f30958a1cc0d9d3f547d551"),
 286  	},
 287  }