test_message_protection.mx raw
1 //go:build !wasm
2
3 package mls
4
5 import "errors"
6
7 // TestMessageProtection validates framing crypto (sign/verify + encrypt/decrypt)
8 // against RFC 9420 test vectors (cipher_suite 3, message-protection.json index 2).
9 //
10 // Public messages (proposal, commit):
11 // - Unmarshal mlsMessage → publicMessage
12 // - Verify signature with signature_pub
13 // - Verify membership tag with membership_key
14 // - Compare raw framed content body to vector's proposal/commit bytes
15 //
16 // Private messages (proposal, commit, application):
17 // - Unmarshal mlsMessage → privateMessage
18 // - Decrypt sender data with sender_data_secret
19 // - Derive secret tree (numLeaves=2, encryption_secret)
20 // - Advance handshake/application ratchet to senderData.generation
21 // - Decrypt content
22 // - Verify signature, compare raw content body to vector bytes
23 func TestMessageProtection() error {
24 cs := CipherSuite0x0003
25
26 groupID := GroupID(hexb("fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf"))
27 epoch := uint64(1184274)
28 treeHash := hexb("5ae6ee5f282786b753d55f15938248f6212c69b2f677319e5fcdb2ce0bfbf48a")
29 confirmedTH := hexb("0ba35f0d36007bc44df41db074b9d98a1314e3c690cb345ceeae152fbe5b6afe")
30 signaturePub := signaturePublicKey(hexb("b1d7f2e45d6c94fa8bff3b63308bbba38024c5b70c22234834ee24294b6e0a28"))
31 encryptionSecret := hexb("ce02c4dd80a8ed57503e3a50ec11b926a549d65e840417c0635427a838b3cf29")
32 senderDataSecret := hexb("85fc31fac878dbe2850e30a8fa88bdc63dacd890756aade03433791b50a510f5")
33 membershipKey := hexb("82f73c829938c5d1a19e19de2b2c109214455ee6671cf918af426f364162678e")
34
35 proposalBytes := hexb("000300000002")
36 proposalPriv := hexb("0001000220fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf000000000012121202001cc15e1b90a18448376191078a6b66567941ce2472f110def1215eb1114058f9bf6e2c2dd19ff11b450ce94d9990ca3dbaf94edf8873c0f7cbf8feeefd362ac674b32d4c1c6ca646b8282fec7aaa51dcdd402345792aa77d98eaa97da5bf13969e44845f33114c10e1edd765d1c738059757570366687b")
37 proposalPub := hexb("0001000120fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf00000000001212120100000001000200030000000240402aef68e55e2092800105a0a963c84afb2039093f5431f9c59fce19d3f2bca20689a617deb8fa59085f21b609649b0373e62c0c099e9bfc66f939e623a851d3022048451581d2796c68ca95ddbf06fe4cc69f5b1360f12dce5947e095ab26c259a3")
38
39 commitBytes := hexb("4046010004012017e89d9ff20be622654e2b82b1e55d5b51c9e01d3b02cf0fa179c14e8697c11620d3ad3e549ffe3fe157d4d2e6627b8d35be6b5b8f37ffb10ed358bfb47c6c829500")
40 commitPriv := hexb("0001000220fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf000000000012121203001c71db9299c28f6e4feec4ddd63660f8ee186769ba51f2721953c48b9440bc32b3c5b162267da077973a9966c1c1bf6406d4326d9b0649bffeceec9ebe4dcf8603aa291254ce1954471cb01670b8e8e2f111ed54f8c818b348bd0300eb174602b484899f2fa95bbebdae6807de23355e2a988cf30cdeb2327759b2274205f485ff15b668b00a4f36ead53e21091be1133c2dd9a635bd6a942509921e33647e864a26af80aa5e54e15e695d2156209c2da2a7b8fe1938632192fce5e75736249c2fb6a7c2af46b29bfb5a83699d568f6b5614bf4edca36dc290f71d")
41 commitPub := hexb("0001000120fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf0000000000121212010000000100034046010004012017e89d9ff20be622654e2b82b1e55d5b51c9e01d3b02cf0fa179c14e8697c11620d3ad3e549ffe3fe157d4d2e6627b8d35be6b5b8f37ffb10ed358bfb47c6c82950040402ad9df6c6e068cfb07cd8a6e49455f256ca630341744128cdc2bd180c8dd76b8e2b3affc5e8c0b3a70d55e349e46adf95e2641299b6c1ac78336e69ec243860520b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad20aa0af8115702fcd155306e3f0091dbc40c772a97547ee9398157106ad10e9bf1")
42
43 applicationBytes := hexb("725b6517ff1e5ed74224de23777a9b5f4a2dd74fe4a816d19ea71baa069d74c0975b7f10e61b7c3ef339")
44 applicationPriv := hexb("0001000220fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf000000000012121201001c22d286c12043af1e8093bd213bf707b9b675ee98a171d0eeb206de91407d9d099e17b2aa1de37065098feb8efa598721128ec5bada3a6a07e06123eb61ced1dd265a0e7468f05f39b549ede80606f4795425de91acaee0b0bd840d0eabd284ae30adf04a5bf97027180836adee1203636c2cfd36f477747e9f0549dc99f5b3ef3a3db4f4def426625757365cf871061383a6dbd516127a11f83cc4")
45
46 ctx := &groupContext{
47 version: protocolVersionMLS10,
48 cipherSuite: cs,
49 groupID: groupID,
50 epoch: epoch,
51 treeHash: treeHash,
52 confirmedTranscriptHash: confirmedTH,
53 }
54
55 // --- Public messages ---
56 if err := checkPublicMessage(ctx, proposalPub, proposalBytes, contentTypeProposal, signaturePub, membershipKey, "proposal"); err != nil {
57 return err
58 }
59 if err := checkPublicMessage(ctx, commitPub, commitBytes, contentTypeCommit, signaturePub, membershipKey, "commit"); err != nil {
60 return err
61 }
62
63 // --- Private messages ---
64 secTree, err := deriveSecretTree(cs, numLeaves(2), encryptionSecret)
65 if err != nil {
66 return errors.New("deriveSecretTree: " | err.Error())
67 }
68
69 if err := checkPrivateMessage(ctx, secTree, senderDataSecret, proposalPriv, proposalBytes, contentTypeProposal, signaturePub, "proposal_priv"); err != nil {
70 return err
71 }
72 if err := checkPrivateMessage(ctx, secTree, senderDataSecret, commitPriv, commitBytes, contentTypeCommit, signaturePub, "commit_priv"); err != nil {
73 return err
74 }
75 if err := checkPrivateMessage(ctx, secTree, senderDataSecret, applicationPriv, applicationBytes, contentTypeApplication, signaturePub, "application_priv"); err != nil {
76 return err
77 }
78
79 return nil
80 }
81
82 func checkPublicMessage(ctx *groupContext, wire, wantBody []byte, wantCT contentType, sigPub signaturePublicKey, membershipKey []byte, tag string) error {
83 var msg mlsMessage
84 if err := unmarshalRaw(wire, &msg); err != nil {
85 return errors.New(tag | ": unmarshal mlsMessage: " | err.Error())
86 }
87 if msg.wireFormat != wireFormatMLSPublicMessage || msg.publicMessage == nil {
88 return errors.New(tag | ": wireFormat is not public")
89 }
90 pub := msg.publicMessage
91 if pub.content.contentType != wantCT {
92 return errors.New(tag | ": unexpected contentType")
93 }
94
95 authContent := pub.authenticatedContent()
96 if !authContent.verifySignature(sigPub, ctx) {
97 return errors.New(tag | ": verifySignature failed")
98 }
99
100 if !pub.verifyMembershipTag(membershipKey, ctx) {
101 return errors.New(tag | ": verifyMembershipTag failed")
102 }
103
104 // Compare the content body (proposal/commit wire bytes) against the vector.
105 var w Writer
106 switch wantCT {
107 case contentTypeProposal:
108 pub.content.proposal.marshal(&w)
109 case contentTypeCommit:
110 pub.content.commit.marshal(&w)
111 default:
112 return errors.New(tag | ": unsupported contentType for public message")
113 }
114 got, err := w.bytes()
115 if err != nil {
116 return errors.New(tag | ": marshal content body: " | err.Error())
117 }
118 if !bytesEqual(got, wantBody) {
119 return errors.New(tag | ": content body mismatch: got " | hexenc(got) | " want " | hexenc(wantBody))
120 }
121 return nil
122 }
123
124 func checkPrivateMessage(ctx *groupContext, tree secretTree, senderDataSecret, wire, wantBody []byte, wantCT contentType, sigPub signaturePublicKey, tag string) error {
125 cs := ctx.cipherSuite
126 var msg mlsMessage
127 if err := unmarshalRaw(wire, &msg); err != nil {
128 return errors.New(tag | ": unmarshal mlsMessage: " | err.Error())
129 }
130 if msg.wireFormat != wireFormatMLSPrivateMessage || msg.privateMessage == nil {
131 return errors.New(tag | ": wireFormat is not private")
132 }
133 priv := msg.privateMessage
134 if priv.contentType != wantCT {
135 return errors.New(tag | ": unexpected contentType")
136 }
137
138 sd, err := priv.decryptSenderData(cs, senderDataSecret)
139 if err != nil {
140 return errors.New(tag | ": decryptSenderData: " | err.Error())
141 }
142
143 // Advance the appropriate ratchet (handshake for proposal/commit, application for application)
144 // to the sender's generation.
145 label := ratchetLabelFromContentType(wantCT)
146 secret, err := tree.deriveRatchetRoot(cs, sd.leafIndex.nodeIndex(), label)
147 if err != nil {
148 return errors.New(tag | ": deriveRatchetRoot: " | err.Error())
149 }
150 for secret.generation < sd.generation {
151 secret, err = secret.deriveNext(cs)
152 if err != nil {
153 return errors.New(tag | ": deriveNext: " | err.Error())
154 }
155 }
156
157 content, err := priv.decryptContent(cs, secret, sd.reuseGuard)
158 if err != nil {
159 return errors.New(tag | ": decryptContent: " | err.Error())
160 }
161
162 // Reconstruct an authenticatedContent from the decrypted privateMessageContent
163 // so we can verify the signature.
164 fc := &framedContent{
165 groupID: priv.groupID,
166 epoch: priv.epoch,
167 sender: sender{senderType: senderTypeMember, leafIndex: sd.leafIndex},
168 authenticatedData: priv.authenticatedData,
169 contentType: priv.contentType,
170 applicationData: content.applicationData,
171 proposal: content.proposal,
172 commit: content.commit,
173 }
174 authContent := &authenticatedContent{
175 wireFormat: wireFormatMLSPrivateMessage,
176 content: *fc,
177 auth: content.auth,
178 }
179 if !authContent.verifySignature(sigPub, ctx) {
180 return errors.New(tag | ": verifySignature failed")
181 }
182
183 // Compare the content body to the vector.
184 var w Writer
185 switch wantCT {
186 case contentTypeApplication:
187 w.addBytes(content.applicationData)
188 case contentTypeProposal:
189 content.proposal.marshal(&w)
190 case contentTypeCommit:
191 content.commit.marshal(&w)
192 default:
193 return errors.New(tag | ": unsupported contentType")
194 }
195 got, err := w.bytes()
196 if err != nil {
197 return errors.New(tag | ": marshal content body: " | err.Error())
198 }
199 if !bytesEqual(got, wantBody) {
200 return errors.New(tag | ": content body mismatch: got " | hexenc(got) | " want " | hexenc(wantBody))
201 }
202 return nil
203 }