//go:build !wasm package mls import "errors" // TestKeySchedule validates key schedule derivation against RFC 9420 test vectors // (cipher_suite 3, key-schedule.json index 2). 5 epochs, full derivation chain. func TestKeySchedule() error { cs := CipherSuite0x0003 groupID := hexb("a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e") initSecret := hexb("a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e") for i, ep := range ksEpochs { ctx := &groupContext{ version: protocolVersionMLS10, cipherSuite: cs, groupID: GroupID(groupID), epoch: uint64(i), treeHash: ep.treeHash, confirmedTranscriptHash: ep.confirmedTranscriptHash, } // Verify serialized group context matches vector. raw, err := marshalRaw(ctx) if err != nil { return errors.New("epoch " | itoa(i) | ": marshalRaw: " | err.Error()) } if !bytesEqual(raw, ep.groupContext) { return errors.New("epoch " | itoa(i) | ": group_context mismatch: got " | hexenc(raw) | " want " | hexenc(ep.groupContext)) } // extractJoinerSecret joinerSecret, err := ctx.extractJoinerSecret(initSecret, ep.commitSecret) if err != nil { return errors.New("epoch " | itoa(i) | ": extractJoinerSecret: " | err.Error()) } if !bytesEqual(joinerSecret, ep.joinerSecret) { return errors.New("epoch " | itoa(i) | ": joiner_secret mismatch: got " | hexenc(joinerSecret) | " want " | hexenc(ep.joinerSecret)) } // extractWelcomeSecret welcomeSecret, err := extractWelcomeSecret(cs, joinerSecret, ep.pskSecret) if err != nil { return errors.New("epoch " | itoa(i) | ": extractWelcomeSecret: " | err.Error()) } if !bytesEqual(welcomeSecret, ep.welcomeSecret) { return errors.New("epoch " | itoa(i) | ": welcome_secret mismatch") } // extractEpochSecret epochSecret, err := ctx.extractEpochSecret(joinerSecret, ep.pskSecret) if err != nil { return errors.New("epoch " | itoa(i) | ": extractEpochSecret: " | err.Error()) } // Derive all 8 labeled secrets from epochSecret. type labelCheck struct { label []byte want []byte } checks := []labelCheck{ {secretLabelInit, ep.initSecret}, {secretLabelSenderData, ep.senderDataSecret}, {secretLabelEncryption, ep.encryptionSecret}, {secretLabelExporter, ep.exporterSecret}, {secretLabelExternal, ep.externalSecret}, {secretLabelConfirm, ep.confirmationKey}, {secretLabelMembership, ep.membershipKey}, {secretLabelResumption, ep.resumptionPSK}, } for _, chk := range checks { got, err := cs.deriveSecret(epochSecret, chk.label) if err != nil { return errors.New("epoch " | itoa(i) | ": deriveSecret(" | string(chk.label) | "): " | err.Error()) } if !bytesEqual(got, chk.want) { return errors.New("epoch " | itoa(i) | ": deriveSecret(" | string(chk.label) | ") mismatch: got " | hexenc(got) | " want " | hexenc(chk.want)) } } // epoch_authenticator = deriveSecret(epochSecret, "authentication") epochAuth, err := cs.deriveSecret(epochSecret, secretLabelAuthentication) if err != nil { return errors.New("epoch " | itoa(i) | ": deriveSecret(authentication): " | err.Error()) } if !bytesEqual(epochAuth, ep.epochAuthenticator) { return errors.New("epoch " | itoa(i) | ": epoch_authenticator mismatch") } // deriveEncryptionKeyPair from externalSecret → compare pub to externalPub externalPub, _, err := cs.deriveEncryptionKeyPair(ep.externalSecret) if err != nil { return errors.New("epoch " | itoa(i) | ": deriveEncryptionKeyPair: " | err.Error()) } if !bytesEqual([]byte(externalPub), ep.externalPub) { return errors.New("epoch " | itoa(i) | ": external_pub mismatch: got " | hexenc([]byte(externalPub)) | " want " | hexenc(ep.externalPub)) } // deriveExporter exporterValue, err := deriveExporter(cs, ep.exporterSecret, ep.exporterLabel, ep.exporterContext, uint16(ep.exporterLength)) if err != nil { return errors.New("epoch " | itoa(i) | ": deriveExporter: " | err.Error()) } if !bytesEqual(exporterValue, ep.exporterValue) { return errors.New("epoch " | itoa(i) | ": exporter mismatch: got " | hexenc(exporterValue) | " want " | hexenc(ep.exporterValue)) } // Advance initSecret for next epoch. initSecret = ep.initSecret } return nil } func itoa(n int) string { if n == 0 { return "0" } buf := []byte{:8} i := len(buf) for n > 0 { i-- buf[i] = byte('0' + n%10) n /= 10 } return string(buf[i:]) } type ksEpoch struct { // Inputs commitSecret []byte treeHash []byte confirmedTranscriptHash []byte pskSecret []byte // Serialized group context (for verification) groupContext []byte // Key schedule outputs joinerSecret []byte welcomeSecret []byte initSecret []byte senderDataSecret []byte encryptionSecret []byte exporterSecret []byte externalSecret []byte confirmationKey []byte membershipKey []byte resumptionPSK []byte // epoch_authenticator epochAuthenticator []byte // External keypair (pub derived from externalSecret) externalPub []byte // Exporter test exporterLabel []byte exporterContext []byte exporterLength int exporterValue []byte } var ksEpochs = []ksEpoch{ // --- epoch 0 --- { commitSecret: hexb("a22606222e350fd7f0937168fe7548fb06626ab143cba7611d641693b1447509"), treeHash: hexb("9769e302a99c457350a8e636009b12a2fee068664004606d6318eb3a1977d818"), confirmedTranscriptHash: hexb("5e57c9364dc71f0f71b19ffe561ab77257c490708a47e29f8f73f2b318201d2f"), pskSecret: hexb("e871b247379522395689182736cb3d1e7b108d6ae934b802223975de8dc3f80b"), groupContext: hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e0000000000000000209769e302a99c457350a8e636009b12a2fee068664004606d6318eb3a1977d818205e57c9364dc71f0f71b19ffe561ab77257c490708a47e29f8f73f2b318201d2f00"), joinerSecret: hexb("3e28da76edc09fb9ad59fae258839c7dc46e3c092a125499959c7413a60250b2"), welcomeSecret: hexb("b0defbdd2232224b0c0427e8efa80f011f7813291dca783433f2da1431620bbc"), initSecret: hexb("418b197eafd925ebbf4bfd94d650aa83b1a11d6d02f33c2cc81631c6734f69d9"), senderDataSecret: hexb("de1df3a74bbcfc7fcc631213a20c1b1842860eab8e6f0c864dcfb541cd42cf24"), encryptionSecret: hexb("ffcc3d4a757224eaf62c124f8e7def12c0db74740cf494c9f56fa7dd07214947"), exporterSecret: hexb("27518b380b39834affecb08780ee9709627859d5f6f37994e8783791004485cb"), externalSecret: hexb("46f51f54ce4c3457ee5681925b9d1a282de166f04e28a4a316404bd14dda3138"), confirmationKey: hexb("e8bdff522e2675c7e0582321fbeb7e61763b1f88e7ded3c57ea78c691e1d0b93"), membershipKey: hexb("6839abba79aaeb82385397612fb90cbea3bf8d427806cb3f0bfe5793c1a42fc9"), resumptionPSK: hexb("244b05004ced1a7d1dc3da6a7541e9b180b6ffe41cc6e24d63c5c9c0742b4870"), epochAuthenticator: hexb("f68f6735aeeb97331d674ef4f580e11352beff543b3b6688a01a1bab97d42f26"), externalPub: hexb("8206ea1eb4d8d5730a2737f7470718b9d00c2276d24a98ac4e6d7ef52cba0631"), exporterLabel: []byte("9ba13d54ecdec7cbefcb47b4268d7b1990fabc6d6e67681e167959389d84e4e4"), exporterContext: hexb("884f1af892ab002f5be4c5d5081ade9e0e6418c6ea7a9a92e90534f19dcef785"), exporterLength: 32, exporterValue: hexb("623c858acd2728c5b860a77ae0cde77fa8aef14e9ac124464cab06bbc3cf3635"), }, // --- epoch 1 --- { commitSecret: hexb("7b3027aa5d2224aab7e2a18660bbf57930e2e21d95e02b849c704d970e3e28c5"), treeHash: hexb("826a4d3b0956277ce5e272e4d18fdca023ffb63ea4cea636e34cc837ae7c5c5d"), confirmedTranscriptHash: hexb("14a2985ea47db0685924a74d47ac8a08ec241f843b536dd1348e3ffb2d78184e"), pskSecret: hexb("ca7a68f2a8a52147d70f1eb7195de968d2e182b93596bc5a61393861e91180e4"), groupContext: hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e000000000000000120826a4d3b0956277ce5e272e4d18fdca023ffb63ea4cea636e34cc837ae7c5c5d2014a2985ea47db0685924a74d47ac8a08ec241f843b536dd1348e3ffb2d78184e00"), joinerSecret: hexb("f589c7bc9de6fac35e546bbf4ac89632980158e40336cdf89165b526a482f228"), welcomeSecret: hexb("f577c28c938b9f57224e5f208ee8a64f65a4fb734bf8f74f44f0020f55e22a20"), initSecret: hexb("6947180097f92f021d5d0739568d9e41647ebe26d754f679f0a883013165b6c9"), senderDataSecret: hexb("c6b20eeb67246447456fe4d4dd9ee8e6e88e710cf766f862440e9a7b4789be81"), encryptionSecret: hexb("bd364a14dbe3e9d14f6573d2df1ee014cb0bfcadd948f257d35091fba8735d3a"), exporterSecret: hexb("eac27705796cb8ff5867d5883f111f8a9990de3e69befc5d69bf2d02f539b863"), externalSecret: hexb("e7b78eda3dc2d13c3aee74426a6aa6e8a90fb95992be66b5eeb8cdc0b08c6f1a"), confirmationKey: hexb("839d6621c61a40a14d30a5042aa9a530e267f825734d485ac9e415f19e35d9dd"), membershipKey: hexb("d835002d8367b0c81e7d0159527b12a24870dbb8efc8fb299ad16b72b2199a6c"), resumptionPSK: hexb("e2cc0c5444b77af2d15a8d5adf2ce9fd051429758ab80bd247d80f6c44982a64"), epochAuthenticator: hexb("acd66baee8206cca6b60a71c9db4f1ae97718db857e267c352d495f29ff0776b"), externalPub: hexb("e2daac515f8378d164bf51746f4143b54d7a8d574f4ae1081b5d1d0ba88fe615"), exporterLabel: []byte("ed66d7f1da52171ac9448f0f902edcfefa4ebbda843a43bd3d173cb7c5b4331e"), exporterContext: hexb("02dc18fc5bc4d9093cf41fa0053521653775b123784d40ac7d46cc5a72ef4d46"), exporterLength: 32, exporterValue: hexb("f6ae0abce67ed43e7cda3c04774278930c96bb1abfa77707cb7ac3351a9ded5f"), }, // --- epoch 2 --- { commitSecret: hexb("d2825785628f1ea7404d6761f27272af5f99416ea28cc9d335df47ed2b0097d4"), treeHash: hexb("661ce3bd9ebec8608fa97bf5413a4588f50a8f9face225ec67a6d29c862b2516"), confirmedTranscriptHash: hexb("b5d7ec8c9d8b6a28c9467fe4918844be6acc08e98c1e10c71122e95f9a5e78c5"), pskSecret: hexb("599aa672406270914c60d30b7a31d2f2e217c3b5298b279b79e34c65a60e5f24"), groupContext: hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e000000000000000220661ce3bd9ebec8608fa97bf5413a4588f50a8f9face225ec67a6d29c862b251620b5d7ec8c9d8b6a28c9467fe4918844be6acc08e98c1e10c71122e95f9a5e78c500"), joinerSecret: hexb("2895d244e83e8f29025bc990bedff61ce114281e4ff22362c4ddd1646e0fd052"), welcomeSecret: hexb("58a7d5da2ab6a8cbebb89c34741c52ce3f4aab1b0ca3fe53f47c25f77de9576c"), initSecret: hexb("300b71ecfff9459934b4ede696bc0ee9858aa70a89c0e934fadad97383347e4c"), senderDataSecret: hexb("498d6f93ac48b8a8d3ba1321b4baf760fe59d486be53208ad0c2f57a34ce6263"), encryptionSecret: hexb("a61b4a90c66febc563f95e7af4417e1be5e33181605b2b9f8725ea30b184928e"), exporterSecret: hexb("fdac6dcb3929509f612106168550370afd92beeaf838bc09bad0a4db09c03b4e"), externalSecret: hexb("072b9f6d87abb70c908e0808fb4f60e0680426d12c64d4aa8389bf6c423b6cc3"), confirmationKey: hexb("3cdd1841fe689fad3c4afca9d78e20506cbc377c34e67245721448d944e63ace"), membershipKey: hexb("08caa1215ef79aab5fb229c286b82c2a5702932aeb68190496b18cf537c86e5c"), resumptionPSK: hexb("887ca27b1d9f00bb88d60a6ecb86df34bf500bf0f755e7d02b6fd14457fff9d9"), epochAuthenticator: hexb("1b9bbd13d92e2b4d600af772e340c8130b5704ad81cf17766ff5649603d058b3"), externalPub: hexb("c32e86b676cdcf9daf07823faad7bf0d650ea5b1593bf816573236a0bc744571"), exporterLabel: []byte("06f549e9bf966d7b7135b6ef6d4e3032a1c720adf0281c3ef1c0beac0da88621"), exporterContext: hexb("b0fa7e3f0f2199278a55267d551d43946bbfc6d847632867dd86abd1217982a5"), exporterLength: 32, exporterValue: hexb("58560324e8b45296b31c44f250df77194b977cd4f9b1c0fd55b15cd260a3d582"), }, // --- epoch 3 --- { commitSecret: hexb("f652baa9151c9719ecb2716240a2a5ed9aeede1df19de0de862ded166a724783"), treeHash: hexb("08225ed7f3b0b8aa9f03b24395ed8ee7002d38209fddd7d941dd8ac629ac8a62"), confirmedTranscriptHash: hexb("dab8c2f8ec97a0e5a137c55a5b9ac1ccdf5ae8329810e98e0bc3930aae0b4be1"), pskSecret: hexb("4106e07ffe8f0bfdbfd317d92e37a1fc6c4d1fba53ee054b7acf8587013d533b"), groupContext: hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e00000000000000032008225ed7f3b0b8aa9f03b24395ed8ee7002d38209fddd7d941dd8ac629ac8a6220dab8c2f8ec97a0e5a137c55a5b9ac1ccdf5ae8329810e98e0bc3930aae0b4be100"), joinerSecret: hexb("02a17a6d0995432062b8825fa071fabefb0aecadbe9864d229e82ea73abaf71d"), welcomeSecret: hexb("c935f0face4e94e6f7e886a9df715bfcdcdc3d3a546601fc4ffa7f46b10904f6"), initSecret: hexb("561d5fe3f86642d8b9782d6227bb4b0dae805e6930432ed1b45334e14ff5225a"), senderDataSecret: hexb("785fd9a3bbe87178b2b26aa494851d0528a1f39430c1b787c4d48ccacb1012ce"), encryptionSecret: hexb("72842779a3f83949693f20efb2a64c60a43a8e5b12981d92b43660508869d9d9"), exporterSecret: hexb("db2749738091f49e479a570001f2daa7dc4aeeca2e1cfbb60f7d983b3dfd427c"), externalSecret: hexb("75edc2d917753b5238d08a8549150672d8730731fddcd1aac9ac4dcdffab1fe6"), confirmationKey: hexb("b531f478b39a21ff975491f292bd971764b1c99ec5bec5ba5e12f2922fa13c9d"), membershipKey: hexb("d68c57291ad3a57f20ace9386e52187581a3de9820938bd0424696287df64622"), resumptionPSK: hexb("89b4a85bf900b6ba890a89eae531bb58c5b3b48f406918846a2d7195088edf04"), epochAuthenticator: hexb("5c811e1b167d6ccbbd1ddafe4df66f833e418cadd562e23e6ff7d8c8103f8ca3"), externalPub: hexb("1e8f03db3e2f5d6fa885455904f726f426354a441c5317a271417360ef7b9d2e"), exporterLabel: []byte("b76193af29eadc6e16c66493e2a4ef8219aad79986b6d4911493741ec1e666cc"), exporterContext: hexb("941db06073e76050679d33cf1f0ec33de3e5a2cd00cb738b54c0dd90de251cdd"), exporterLength: 32, exporterValue: hexb("4e2dd79d84c728b7503cee9acbe1e057523d0ff973d81e011905b5b581d2adb3"), }, // --- epoch 4 --- { commitSecret: hexb("50fb68cdd31ff76b8d86b88b80124534d845cf2a835411b002b40b7b08d28278"), treeHash: hexb("918e07d9bfd965f880f860830b24427d9200fcac485e973b4943e67d322c1682"), confirmedTranscriptHash: hexb("5e88437e7e8e91582bc440a375e93280417c94c5e38db8537963dd3750dd3e16"), pskSecret: hexb("c7e0f52b886962b1edde9e75b9ecafa0d8efeffb474732a3da298c470f1d1445"), groupContext: hexb("0001000320a897b53575b4dd35fed4466e4e714bfa949eaa72e616a9c68a47b39cb7a60d2e000000000000000420918e07d9bfd965f880f860830b24427d9200fcac485e973b4943e67d322c1682205e88437e7e8e91582bc440a375e93280417c94c5e38db8537963dd3750dd3e1600"), joinerSecret: hexb("e3e8700db2c43917c6e36dae28732b21c285f9af371f66a5a9f5f9a70c65e1b2"), welcomeSecret: hexb("6f6494718441537ce4cea94ad29fb66d3048c711e61f7f8aefccf4f6d61fcd86"), initSecret: hexb("20315bc816babd2c11b78bbcac01baa787136a26baf80adc0753d163c9b09d89"), senderDataSecret: hexb("1535616e267425ab17e48a8607d9edc196601579303592885db62c71efbbdcbf"), encryptionSecret: hexb("7e893a0ef9b0b4b1708213996b2ae8c8382a50fbce6caa4496e07093c36de351"), exporterSecret: hexb("7cd91ce426cada365fc15f9fb060c360ad81bc350096b404bc3d66a7d64a00cc"), externalSecret: hexb("b3f67ce4547ae1e886e2275aa3760146d55cc9a1746bd36b0291c127368c7c38"), confirmationKey: hexb("9a34f258b8c3a3733d734a3c84ebf3683f34fa67760eeabf1e9a9b329a733d56"), membershipKey: hexb("3dadf8dbb09c5c1e25c565dc7cffc494055e517b0129e0bc83fcbb7280e9ae7f"), resumptionPSK: hexb("e914d45c8c1b06670b926ae56a7908c93d2721cc562f82778044e9888dc9e2d1"), epochAuthenticator: hexb("d59f86014e0c306be049bd9f2dbae06a3d225486e74042dba30238e4d72ab89e"), externalPub: hexb("8236c9f14f1d5bc27635be4da2795b5367660879f5afe3951b7f1eb7b69fd769"), exporterLabel: []byte("cc9c4b25b0bd69250b6e4f9908d1b170bf62fe8cc11ad36d33e602324ac0662a"), exporterContext: hexb("a0f762fc82d5e421d8fdf8a317b2fd463008c625bf19db7fbfa4ac778b5106a2"), exporterLength: 32, exporterValue: hexb("b07890e33ce8c281d07c7228c186c48f882a70967f30958a1cc0d9d3f547d551"), }, }