//go:build !wasm package mls import "errors" // TestMessageProtection validates framing crypto (sign/verify + encrypt/decrypt) // against RFC 9420 test vectors (cipher_suite 3, message-protection.json index 2). // // Public messages (proposal, commit): // - Unmarshal mlsMessage → publicMessage // - Verify signature with signature_pub // - Verify membership tag with membership_key // - Compare raw framed content body to vector's proposal/commit bytes // // Private messages (proposal, commit, application): // - Unmarshal mlsMessage → privateMessage // - Decrypt sender data with sender_data_secret // - Derive secret tree (numLeaves=2, encryption_secret) // - Advance handshake/application ratchet to senderData.generation // - Decrypt content // - Verify signature, compare raw content body to vector bytes func TestMessageProtection() error { cs := CipherSuite0x0003 groupID := GroupID(hexb("fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf")) epoch := uint64(1184274) treeHash := hexb("5ae6ee5f282786b753d55f15938248f6212c69b2f677319e5fcdb2ce0bfbf48a") confirmedTH := hexb("0ba35f0d36007bc44df41db074b9d98a1314e3c690cb345ceeae152fbe5b6afe") signaturePub := signaturePublicKey(hexb("b1d7f2e45d6c94fa8bff3b63308bbba38024c5b70c22234834ee24294b6e0a28")) encryptionSecret := hexb("ce02c4dd80a8ed57503e3a50ec11b926a549d65e840417c0635427a838b3cf29") senderDataSecret := hexb("85fc31fac878dbe2850e30a8fa88bdc63dacd890756aade03433791b50a510f5") membershipKey := hexb("82f73c829938c5d1a19e19de2b2c109214455ee6671cf918af426f364162678e") proposalBytes := hexb("000300000002") proposalPriv := hexb("0001000220fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf000000000012121202001cc15e1b90a18448376191078a6b66567941ce2472f110def1215eb1114058f9bf6e2c2dd19ff11b450ce94d9990ca3dbaf94edf8873c0f7cbf8feeefd362ac674b32d4c1c6ca646b8282fec7aaa51dcdd402345792aa77d98eaa97da5bf13969e44845f33114c10e1edd765d1c738059757570366687b") proposalPub := hexb("0001000120fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf00000000001212120100000001000200030000000240402aef68e55e2092800105a0a963c84afb2039093f5431f9c59fce19d3f2bca20689a617deb8fa59085f21b609649b0373e62c0c099e9bfc66f939e623a851d3022048451581d2796c68ca95ddbf06fe4cc69f5b1360f12dce5947e095ab26c259a3") commitBytes := hexb("4046010004012017e89d9ff20be622654e2b82b1e55d5b51c9e01d3b02cf0fa179c14e8697c11620d3ad3e549ffe3fe157d4d2e6627b8d35be6b5b8f37ffb10ed358bfb47c6c829500") commitPriv := hexb("0001000220fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf000000000012121203001c71db9299c28f6e4feec4ddd63660f8ee186769ba51f2721953c48b9440bc32b3c5b162267da077973a9966c1c1bf6406d4326d9b0649bffeceec9ebe4dcf8603aa291254ce1954471cb01670b8e8e2f111ed54f8c818b348bd0300eb174602b484899f2fa95bbebdae6807de23355e2a988cf30cdeb2327759b2274205f485ff15b668b00a4f36ead53e21091be1133c2dd9a635bd6a942509921e33647e864a26af80aa5e54e15e695d2156209c2da2a7b8fe1938632192fce5e75736249c2fb6a7c2af46b29bfb5a83699d568f6b5614bf4edca36dc290f71d") commitPub := hexb("0001000120fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf0000000000121212010000000100034046010004012017e89d9ff20be622654e2b82b1e55d5b51c9e01d3b02cf0fa179c14e8697c11620d3ad3e549ffe3fe157d4d2e6627b8d35be6b5b8f37ffb10ed358bfb47c6c82950040402ad9df6c6e068cfb07cd8a6e49455f256ca630341744128cdc2bd180c8dd76b8e2b3affc5e8c0b3a70d55e349e46adf95e2641299b6c1ac78336e69ec243860520b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad20aa0af8115702fcd155306e3f0091dbc40c772a97547ee9398157106ad10e9bf1") applicationBytes := hexb("725b6517ff1e5ed74224de23777a9b5f4a2dd74fe4a816d19ea71baa069d74c0975b7f10e61b7c3ef339") applicationPriv := hexb("0001000220fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf000000000012121201001c22d286c12043af1e8093bd213bf707b9b675ee98a171d0eeb206de91407d9d099e17b2aa1de37065098feb8efa598721128ec5bada3a6a07e06123eb61ced1dd265a0e7468f05f39b549ede80606f4795425de91acaee0b0bd840d0eabd284ae30adf04a5bf97027180836adee1203636c2cfd36f477747e9f0549dc99f5b3ef3a3db4f4def426625757365cf871061383a6dbd516127a11f83cc4") ctx := &groupContext{ version: protocolVersionMLS10, cipherSuite: cs, groupID: groupID, epoch: epoch, treeHash: treeHash, confirmedTranscriptHash: confirmedTH, } // --- Public messages --- if err := checkPublicMessage(ctx, proposalPub, proposalBytes, contentTypeProposal, signaturePub, membershipKey, "proposal"); err != nil { return err } if err := checkPublicMessage(ctx, commitPub, commitBytes, contentTypeCommit, signaturePub, membershipKey, "commit"); err != nil { return err } // --- Private messages --- secTree, err := deriveSecretTree(cs, numLeaves(2), encryptionSecret) if err != nil { return errors.New("deriveSecretTree: " | err.Error()) } if err := checkPrivateMessage(ctx, secTree, senderDataSecret, proposalPriv, proposalBytes, contentTypeProposal, signaturePub, "proposal_priv"); err != nil { return err } if err := checkPrivateMessage(ctx, secTree, senderDataSecret, commitPriv, commitBytes, contentTypeCommit, signaturePub, "commit_priv"); err != nil { return err } if err := checkPrivateMessage(ctx, secTree, senderDataSecret, applicationPriv, applicationBytes, contentTypeApplication, signaturePub, "application_priv"); err != nil { return err } return nil } func checkPublicMessage(ctx *groupContext, wire, wantBody []byte, wantCT contentType, sigPub signaturePublicKey, membershipKey []byte, tag string) error { var msg mlsMessage if err := unmarshalRaw(wire, &msg); err != nil { return errors.New(tag | ": unmarshal mlsMessage: " | err.Error()) } if msg.wireFormat != wireFormatMLSPublicMessage || msg.publicMessage == nil { return errors.New(tag | ": wireFormat is not public") } pub := msg.publicMessage if pub.content.contentType != wantCT { return errors.New(tag | ": unexpected contentType") } authContent := pub.authenticatedContent() if !authContent.verifySignature(sigPub, ctx) { return errors.New(tag | ": verifySignature failed") } if !pub.verifyMembershipTag(membershipKey, ctx) { return errors.New(tag | ": verifyMembershipTag failed") } // Compare the content body (proposal/commit wire bytes) against the vector. var w Writer switch wantCT { case contentTypeProposal: pub.content.proposal.marshal(&w) case contentTypeCommit: pub.content.commit.marshal(&w) default: return errors.New(tag | ": unsupported contentType for public message") } got, err := w.bytes() if err != nil { return errors.New(tag | ": marshal content body: " | err.Error()) } if !bytesEqual(got, wantBody) { return errors.New(tag | ": content body mismatch: got " | hexenc(got) | " want " | hexenc(wantBody)) } return nil } func checkPrivateMessage(ctx *groupContext, tree secretTree, senderDataSecret, wire, wantBody []byte, wantCT contentType, sigPub signaturePublicKey, tag string) error { cs := ctx.cipherSuite var msg mlsMessage if err := unmarshalRaw(wire, &msg); err != nil { return errors.New(tag | ": unmarshal mlsMessage: " | err.Error()) } if msg.wireFormat != wireFormatMLSPrivateMessage || msg.privateMessage == nil { return errors.New(tag | ": wireFormat is not private") } priv := msg.privateMessage if priv.contentType != wantCT { return errors.New(tag | ": unexpected contentType") } sd, err := priv.decryptSenderData(cs, senderDataSecret) if err != nil { return errors.New(tag | ": decryptSenderData: " | err.Error()) } // Advance the appropriate ratchet (handshake for proposal/commit, application for application) // to the sender's generation. label := ratchetLabelFromContentType(wantCT) secret, err := tree.deriveRatchetRoot(cs, sd.leafIndex.nodeIndex(), label) if err != nil { return errors.New(tag | ": deriveRatchetRoot: " | err.Error()) } for secret.generation < sd.generation { secret, err = secret.deriveNext(cs) if err != nil { return errors.New(tag | ": deriveNext: " | err.Error()) } } content, err := priv.decryptContent(cs, secret, sd.reuseGuard) if err != nil { return errors.New(tag | ": decryptContent: " | err.Error()) } // Reconstruct an authenticatedContent from the decrypted privateMessageContent // so we can verify the signature. fc := &framedContent{ groupID: priv.groupID, epoch: priv.epoch, sender: sender{senderType: senderTypeMember, leafIndex: sd.leafIndex}, authenticatedData: priv.authenticatedData, contentType: priv.contentType, applicationData: content.applicationData, proposal: content.proposal, commit: content.commit, } authContent := &authenticatedContent{ wireFormat: wireFormatMLSPrivateMessage, content: *fc, auth: content.auth, } if !authContent.verifySignature(sigPub, ctx) { return errors.New(tag | ": verifySignature failed") } // Compare the content body to the vector. var w Writer switch wantCT { case contentTypeApplication: w.addBytes(content.applicationData) case contentTypeProposal: content.proposal.marshal(&w) case contentTypeCommit: content.commit.marshal(&w) default: return errors.New(tag | ": unsupported contentType") } got, err := w.bytes() if err != nil { return errors.New(tag | ": marshal content body: " | err.Error()) } if !bytesEqual(got, wantBody) { return errors.New(tag | ": content body mismatch: got " | hexenc(got) | " want " | hexenc(wantBody)) } return nil }