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 }