├── .build.yml ├── LICENSE ├── README.md ├── ciphersuite.go ├── ciphersuite_test.go ├── crypto.go ├── framing.go ├── framing_test.go ├── go.mod ├── go.sum ├── group.go ├── group_test.go ├── key_package.go ├── key_schedule.go ├── key_schedule_test.go ├── mls.go ├── mls_test.go ├── passive_client_test.go ├── secret_tree.go ├── secret_tree_test.go ├── testdata ├── crypto-basics.json ├── deserialization.json ├── key-schedule.json ├── message-protection.json ├── messages.json ├── passive-client-handling-commit.json ├── passive-client-random.json ├── passive-client-welcome.json ├── psk_secret.json ├── secret-tree.json ├── transcript-hashes.json ├── tree-math.json ├── tree-operations.json ├── tree-validation.json ├── treekem.json └── welcome.json ├── tree.go ├── tree_math.go ├── tree_math_test.go └── tree_test.go /.build.yml: -------------------------------------------------------------------------------- 1 | image: alpine/latest 2 | packages: 3 | - go 4 | sources: 5 | - https://github.com/emersion/go-mls.git 6 | tasks: 7 | - build: | 8 | cd go-mls 9 | go build -race -v ./... 10 | - test: | 11 | cd go-mls 12 | go test -race ./... 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Simon Ser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-mls 2 | 3 | A Go library for [MLS]. 4 | 5 | ## Contributing 6 | 7 | Send patches on [GitHub]. Come and discuss in [#emersion on Libera Chat]. 8 | 9 | ## License 10 | 11 | MIT 12 | 13 | [MLS]: https://datatracker.ietf.org/doc/html/rfc9420 14 | [GitHub]: https://github.com/emersion/go-mls 15 | [#emersion on Libera Chat]: ircs://irc.libera.chat/#emersion 16 | -------------------------------------------------------------------------------- /ciphersuite.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/ed25519" 7 | "crypto/elliptic" 8 | "crypto/hmac" 9 | "crypto/rand" 10 | _ "crypto/sha256" 11 | _ "crypto/sha512" 12 | "fmt" 13 | "math/big" 14 | 15 | "github.com/cloudflare/circl/hpke" 16 | "github.com/cloudflare/circl/sign/ed448" 17 | "golang.org/x/crypto/cryptobyte" 18 | ) 19 | 20 | type cipherSuite uint16 21 | 22 | const ( 23 | cipherSuiteMLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 cipherSuite = 0x0001 24 | cipherSuiteMLS_128_DHKEMP256_AES128GCM_SHA256_P256 cipherSuite = 0x0002 25 | cipherSuiteMLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519 cipherSuite = 0x0003 26 | cipherSuiteMLS_256_DHKEMX448_AES256GCM_SHA512_Ed448 cipherSuite = 0x0004 27 | cipherSuiteMLS_256_DHKEMP521_AES256GCM_SHA512_P521 cipherSuite = 0x0005 28 | cipherSuiteMLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448 cipherSuite = 0x0006 29 | cipherSuiteMLS_256_DHKEMP384_AES256GCM_SHA384_P384 cipherSuite = 0x0007 30 | ) 31 | 32 | func (cs cipherSuite) String() string { 33 | switch cs { 34 | case cipherSuiteMLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519: 35 | return "MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519" 36 | case cipherSuiteMLS_128_DHKEMP256_AES128GCM_SHA256_P256: 37 | return "MLS_128_DHKEMP256_AES128GCM_SHA256_P256" 38 | case cipherSuiteMLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519: 39 | return "MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519" 40 | case cipherSuiteMLS_256_DHKEMX448_AES256GCM_SHA512_Ed448: 41 | return "MLS_256_DHKEMX448_AES256GCM_SHA512_Ed448" 42 | case cipherSuiteMLS_256_DHKEMP521_AES256GCM_SHA512_P521: 43 | return "MLS_256_DHKEMP521_AES256GCM_SHA512_P521" 44 | case cipherSuiteMLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448: 45 | return "MLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448" 46 | case cipherSuiteMLS_256_DHKEMP384_AES256GCM_SHA384_P384: 47 | return "MLS_256_DHKEMP384_AES256GCM_SHA384_P384" 48 | } 49 | return fmt.Sprintf("<%d>", cs) 50 | } 51 | 52 | func (cs cipherSuite) hash() crypto.Hash { 53 | desc, ok := cipherSuiteDescriptions[cs] 54 | if !ok { 55 | panic(fmt.Errorf("mls: invalid cipher suite %d", cs)) 56 | } 57 | return desc.hash 58 | } 59 | 60 | func (cs cipherSuite) hpke() hpke.Suite { 61 | desc, ok := cipherSuiteDescriptions[cs] 62 | if !ok { 63 | panic(fmt.Errorf("mls: invalid cipher suite %d", cs)) 64 | } 65 | return desc.hpke 66 | } 67 | 68 | func (cs cipherSuite) signatureScheme() signatureScheme { 69 | desc, ok := cipherSuiteDescriptions[cs] 70 | if !ok { 71 | panic(fmt.Errorf("mls: invalid cipher suite %d", cs)) 72 | } 73 | return desc.sig 74 | } 75 | 76 | type cipherSuiteDescription struct { 77 | hash crypto.Hash 78 | hpke hpke.Suite 79 | sig signatureScheme 80 | } 81 | 82 | var cipherSuiteDescriptions = map[cipherSuite]cipherSuiteDescription{ 83 | cipherSuiteMLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519: { 84 | hash: crypto.SHA256, 85 | hpke: hpke.NewSuite(hpke.KEM_X25519_HKDF_SHA256, hpke.KDF_HKDF_SHA256, hpke.AEAD_AES128GCM), 86 | sig: ed25519SignatureScheme{}, 87 | }, 88 | cipherSuiteMLS_128_DHKEMP256_AES128GCM_SHA256_P256: { 89 | hash: crypto.SHA256, 90 | hpke: hpke.NewSuite(hpke.KEM_P256_HKDF_SHA256, hpke.KDF_HKDF_SHA256, hpke.AEAD_AES128GCM), 91 | sig: ecdsaSignatureScheme{elliptic.P256(), crypto.SHA256}, 92 | }, 93 | cipherSuiteMLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519: { 94 | hash: crypto.SHA256, 95 | hpke: hpke.NewSuite(hpke.KEM_X25519_HKDF_SHA256, hpke.KDF_HKDF_SHA256, hpke.AEAD_ChaCha20Poly1305), 96 | sig: ed25519SignatureScheme{}, 97 | }, 98 | cipherSuiteMLS_256_DHKEMX448_AES256GCM_SHA512_Ed448: { 99 | hash: crypto.SHA512, 100 | hpke: hpke.NewSuite(hpke.KEM_X448_HKDF_SHA512, hpke.KDF_HKDF_SHA512, hpke.AEAD_AES256GCM), 101 | sig: ed448SignatureScheme{}, 102 | }, 103 | cipherSuiteMLS_256_DHKEMP521_AES256GCM_SHA512_P521: { 104 | hash: crypto.SHA512, 105 | hpke: hpke.NewSuite(hpke.KEM_P521_HKDF_SHA512, hpke.KDF_HKDF_SHA512, hpke.AEAD_AES256GCM), 106 | sig: ecdsaSignatureScheme{elliptic.P521(), crypto.SHA512}, 107 | }, 108 | cipherSuiteMLS_256_DHKEMX448_CHACHA20POLY1305_SHA512_Ed448: { 109 | hash: crypto.SHA512, 110 | hpke: hpke.NewSuite(hpke.KEM_X448_HKDF_SHA512, hpke.KDF_HKDF_SHA512, hpke.AEAD_ChaCha20Poly1305), 111 | sig: ed448SignatureScheme{}, 112 | }, 113 | cipherSuiteMLS_256_DHKEMP384_AES256GCM_SHA384_P384: { 114 | hash: crypto.SHA384, 115 | hpke: hpke.NewSuite(hpke.KEM_P384_HKDF_SHA384, hpke.KDF_HKDF_SHA384, hpke.AEAD_AES256GCM), 116 | sig: ecdsaSignatureScheme{elliptic.P384(), crypto.SHA384}, 117 | }, 118 | } 119 | 120 | func (cs cipherSuite) signMAC(key, message []byte) []byte { 121 | // All cipher suites use HMAC 122 | mac := hmac.New(cs.hash().New, key) 123 | mac.Write(message) 124 | return mac.Sum(nil) 125 | } 126 | 127 | func (cs cipherSuite) verifyMAC(key, message, tag []byte) bool { 128 | return hmac.Equal(tag, cs.signMAC(key, message)) 129 | } 130 | 131 | func (cs cipherSuite) refHash(label, value []byte) ([]byte, error) { 132 | var b cryptobyte.Builder 133 | writeOpaqueVec(&b, label) 134 | writeOpaqueVec(&b, value) 135 | in, err := b.Bytes() 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | h := cs.hash().New() 141 | h.Write(in) 142 | return h.Sum(nil), nil 143 | } 144 | 145 | func (cs cipherSuite) expandWithLabel(secret, label, context []byte, length uint16) ([]byte, error) { 146 | label = append([]byte("MLS 1.0 "), label...) 147 | 148 | var b cryptobyte.Builder 149 | b.AddUint16(length) 150 | writeOpaqueVec(&b, label) 151 | writeOpaqueVec(&b, context) 152 | kdfLabel, err := b.Bytes() 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | _, kdf, _ := cs.hpke().Params() 158 | return kdf.Expand(secret, kdfLabel, uint(length)), nil 159 | } 160 | 161 | func (cs cipherSuite) deriveSecret(secret, label []byte) ([]byte, error) { 162 | _, kdf, _ := cs.hpke().Params() 163 | return cs.expandWithLabel(secret, label, nil, uint16(kdf.ExtractSize())) 164 | } 165 | 166 | func (cs cipherSuite) signWithLabel(signKey, label, content []byte) ([]byte, error) { 167 | signContent, err := marshalSignContent(label, content) 168 | if err != nil { 169 | return nil, err 170 | } 171 | 172 | return cs.signatureScheme().Sign(signKey, signContent) 173 | } 174 | 175 | func (cs cipherSuite) verifyWithLabel(verifKey, label, content, signValue []byte) bool { 176 | signContent, err := marshalSignContent(label, content) 177 | if err != nil { 178 | return false 179 | } 180 | 181 | return cs.signatureScheme().Verify(verifKey, signContent, signValue) 182 | } 183 | 184 | func (cs cipherSuite) encryptWithLabel(publicKey, label, context, plaintext []byte) (kemOutput, ciphertext []byte, err error) { 185 | encryptContext, err := marshalEncryptContext(label, context) 186 | if err != nil { 187 | return nil, nil, err 188 | } 189 | 190 | hpke := cs.hpke() 191 | kem, _, _ := hpke.Params() 192 | pub, err := kem.Scheme().UnmarshalBinaryPublicKey(publicKey) 193 | if err != nil { 194 | return nil, nil, err 195 | } 196 | 197 | sender, err := hpke.NewSender(pub, encryptContext) 198 | if err != nil { 199 | return nil, nil, err 200 | } 201 | 202 | kemOutput, sealer, err := sender.Setup(rand.Reader) 203 | if err != nil { 204 | return nil, nil, err 205 | } 206 | 207 | ciphertext, err = sealer.Seal(plaintext, nil) 208 | return kemOutput, ciphertext, err 209 | } 210 | 211 | func (cs cipherSuite) decryptWithLabel(privateKey, label, context, kemOutput, ciphertext []byte) ([]byte, error) { 212 | encryptContext, err := marshalEncryptContext(label, context) 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | hpke := cs.hpke() 218 | kem, _, _ := hpke.Params() 219 | priv, err := kem.Scheme().UnmarshalBinaryPrivateKey(privateKey) 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | receiver, err := hpke.NewReceiver(priv, encryptContext) 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | opener, err := receiver.Setup(kemOutput) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | return opener.Open(ciphertext, nil) 235 | } 236 | 237 | func marshalSignContent(label, content []byte) ([]byte, error) { 238 | label = append([]byte("MLS 1.0 "), label...) 239 | 240 | var b cryptobyte.Builder 241 | writeOpaqueVec(&b, label) 242 | writeOpaqueVec(&b, content) 243 | return b.Bytes() 244 | } 245 | 246 | func marshalEncryptContext(label, context []byte) ([]byte, error) { 247 | label = append([]byte("MLS 1.0 "), label...) 248 | 249 | var b cryptobyte.Builder 250 | writeOpaqueVec(&b, label) 251 | writeOpaqueVec(&b, context) 252 | return b.Bytes() 253 | } 254 | 255 | type signatureScheme interface { 256 | Sign(signKey, message []byte) ([]byte, error) 257 | Verify(publicKey, message, sig []byte) bool 258 | } 259 | 260 | type ed25519SignatureScheme struct{} 261 | 262 | func (ed25519SignatureScheme) Sign(signKey, message []byte) ([]byte, error) { 263 | if len(signKey) != ed25519.SeedSize { 264 | return nil, fmt.Errorf("mls: invalid Ed25519 private key size") 265 | } 266 | priv := ed25519.NewKeyFromSeed(signKey) 267 | return ed25519.Sign(priv, message), nil 268 | } 269 | 270 | func (ed25519SignatureScheme) Verify(publicKey, message, sig []byte) bool { 271 | if len(publicKey) != ed25519.PublicKeySize { 272 | return false 273 | } 274 | return ed25519.Verify(ed25519.PublicKey(publicKey), message, sig) 275 | } 276 | 277 | type ecdsaSignatureScheme struct { 278 | curve elliptic.Curve 279 | hash crypto.Hash 280 | } 281 | 282 | func (scheme ecdsaSignatureScheme) hashSum(message []byte) []byte { 283 | h := scheme.hash.New() 284 | h.Write(message) 285 | return h.Sum(nil) 286 | } 287 | 288 | func (scheme ecdsaSignatureScheme) Sign(signKey, message []byte) ([]byte, error) { 289 | d := new(big.Int).SetBytes(signKey) 290 | x, y := scheme.curve.ScalarBaseMult(signKey) 291 | priv := &ecdsa.PrivateKey{ 292 | PublicKey: ecdsa.PublicKey{Curve: scheme.curve, X: x, Y: y}, 293 | D: d, 294 | } 295 | return ecdsa.SignASN1(rand.Reader, priv, scheme.hashSum(message)) 296 | } 297 | 298 | func (scheme ecdsaSignatureScheme) Verify(publicKey, message, sig []byte) bool { 299 | x, y := elliptic.Unmarshal(scheme.curve, publicKey) 300 | pub := &ecdsa.PublicKey{Curve: scheme.curve, X: x, Y: y} 301 | return ecdsa.VerifyASN1(pub, scheme.hashSum(message), sig) 302 | } 303 | 304 | type ed448SignatureScheme struct{} 305 | 306 | func (ed448SignatureScheme) Sign(signKey, message []byte) ([]byte, error) { 307 | if len(signKey) != ed448.SeedSize { 308 | return nil, fmt.Errorf("mls: invalid Ed448 private key size") 309 | } 310 | priv := ed448.NewKeyFromSeed(signKey) 311 | return ed448.Sign(priv, message, ""), nil 312 | } 313 | 314 | func (ed448SignatureScheme) Verify(publicKey, message, sig []byte) bool { 315 | if len(publicKey) != ed448.PublicKeySize { 316 | return false 317 | } 318 | return ed448.Verify(ed448.PublicKey(publicKey), message, sig, "") 319 | } 320 | -------------------------------------------------------------------------------- /ciphersuite_test.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | type cryptoBasicsTest struct { 10 | CipherSuite cipherSuite `json:"cipher_suite"` 11 | RefHash refHashTest `json:"ref_hash"` 12 | ExpandWithLabel expandWithLabelTest `json:"expand_with_label"` 13 | DeriveSecret deriveSecretTest `json:"derive_secret"` 14 | DeriveTreeSecret deriveTreeSecretTest `json:"derive_tree_secret"` 15 | SignWithLabel signWithLabelTest `json:"sign_with_label"` 16 | EncryptWithLabel encryptWithLabelTest `json:"encrypt_with_label"` 17 | } 18 | 19 | type refHashTest struct { 20 | Label string `json:"label"` 21 | Out testBytes `json:"out"` 22 | Value testBytes `json:"value"` 23 | } 24 | 25 | func testRefHash(t *testing.T, cs cipherSuite, tc *refHashTest) { 26 | out, err := cs.refHash([]byte(tc.Label), []byte(tc.Value)) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | if !bytes.Equal([]byte(tc.Out), out) { 31 | t.Errorf("got %v, want %v", out, tc.Out) 32 | } 33 | } 34 | 35 | type expandWithLabelTest struct { 36 | Secret testBytes `json:"secret"` 37 | Label string `json:"label"` 38 | Context testBytes `json:"context"` 39 | Length uint16 `json:"length"` 40 | Out testBytes `json:"out"` 41 | } 42 | 43 | func testExpandWithLabel(t *testing.T, cs cipherSuite, tc *expandWithLabelTest) { 44 | out, err := cs.expandWithLabel([]byte(tc.Secret), []byte(tc.Label), []byte(tc.Context), tc.Length) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | if !bytes.Equal([]byte(tc.Out), out) { 49 | t.Errorf("got %v, want %v", out, tc.Out) 50 | } 51 | } 52 | 53 | type deriveSecretTest struct { 54 | Label string `json:"label"` 55 | Out testBytes `json:"out"` 56 | Secret testBytes `json:"secret"` 57 | } 58 | 59 | func testDeriveSecret(t *testing.T, cs cipherSuite, tc *deriveSecretTest) { 60 | out, err := cs.deriveSecret([]byte(tc.Secret), []byte(tc.Label)) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if !bytes.Equal([]byte(tc.Out), out) { 65 | t.Errorf("got %v, want %v", out, tc.Out) 66 | } 67 | } 68 | 69 | type deriveTreeSecretTest struct { 70 | Secret testBytes `json:"secret"` 71 | Label string `json:"label"` 72 | Generation uint32 `json:"generation"` 73 | Length uint16 `json:"length"` 74 | Out testBytes `json:"out"` 75 | } 76 | 77 | func testDeriveTreeSecret(t *testing.T, cs cipherSuite, tc *deriveTreeSecretTest) { 78 | out, err := deriveTreeSecret(cs, []byte(tc.Secret), []byte(tc.Label), tc.Generation, tc.Length) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | if !bytes.Equal([]byte(tc.Out), out) { 83 | t.Errorf("got %v, want %v", out, tc.Out) 84 | } 85 | } 86 | 87 | type signWithLabelTest struct { 88 | Priv testBytes `json:"priv"` 89 | Pub testBytes `json:"pub"` 90 | Content testBytes `json:"content"` 91 | Label string `json:"label"` 92 | Signature testBytes `json:"signature"` 93 | } 94 | 95 | func testSignWithLabel(t *testing.T, cs cipherSuite, tc *signWithLabelTest) { 96 | if !cs.verifyWithLabel([]byte(tc.Pub), []byte(tc.Label), []byte(tc.Content), []byte(tc.Signature)) { 97 | t.Error("reference signature did not verify") 98 | } 99 | 100 | signValue, err := cs.signWithLabel([]byte(tc.Priv), []byte(tc.Label), []byte(tc.Content)) 101 | if err != nil { 102 | t.Fatalf("signWithLabel() = %v", err) 103 | } 104 | if !cs.verifyWithLabel([]byte(tc.Pub), []byte(tc.Label), []byte(tc.Content), signValue) { 105 | t.Error("generated signature did not verify") 106 | } 107 | } 108 | 109 | type encryptWithLabelTest struct { 110 | Priv testBytes `json:"priv"` 111 | Pub testBytes `json:"pub"` 112 | Label string `json:"label"` 113 | Context testBytes `json:"context"` 114 | Plaintext testBytes `json:"plaintext"` 115 | KEMOutput testBytes `json:"kem_output"` 116 | Ciphertext testBytes `json:"ciphertext"` 117 | } 118 | 119 | func testEncryptWithLabel(t *testing.T, cs cipherSuite, tc *encryptWithLabelTest) { 120 | plaintext, err := cs.decryptWithLabel([]byte(tc.Priv), []byte(tc.Label), []byte(tc.Context), []byte(tc.KEMOutput), []byte(tc.Ciphertext)) 121 | if err != nil { 122 | t.Fatalf("decryptWithLabel() = %v", err) 123 | } 124 | if !bytes.Equal([]byte(tc.Plaintext), plaintext) { 125 | t.Fatalf("decrypting reference ciphertext: got %v, want %v", plaintext, tc.Plaintext) 126 | } 127 | 128 | kemOutput, ciphertext, err := cs.encryptWithLabel([]byte(tc.Pub), []byte(tc.Label), []byte(tc.Context), []byte(tc.Plaintext)) 129 | if err != nil { 130 | t.Fatalf("encryptWithLabel() = %v", err) 131 | } 132 | plaintext, err = cs.decryptWithLabel([]byte(tc.Priv), []byte(tc.Label), []byte(tc.Context), kemOutput, ciphertext) 133 | if err != nil { 134 | t.Fatalf("decryptWithLabel() = %v", err) 135 | } 136 | if !bytes.Equal([]byte(tc.Plaintext), plaintext) { 137 | t.Fatalf("decrypting reference ciphertext: got %v, want %v", plaintext, tc.Plaintext) 138 | } 139 | } 140 | 141 | func TestCryptoBasics(t *testing.T) { 142 | var tests []cryptoBasicsTest 143 | loadTestVector(t, "testdata/crypto-basics.json", &tests) 144 | 145 | for i, tc := range tests { 146 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 147 | t.Run("ref_hash", func(t *testing.T) { 148 | testRefHash(t, tc.CipherSuite, &tc.RefHash) 149 | }) 150 | t.Run("expand_with_label", func(t *testing.T) { 151 | testExpandWithLabel(t, tc.CipherSuite, &tc.ExpandWithLabel) 152 | }) 153 | t.Run("derive_secret", func(t *testing.T) { 154 | testDeriveSecret(t, tc.CipherSuite, &tc.DeriveSecret) 155 | }) 156 | t.Run("derive_tree_secret", func(t *testing.T) { 157 | testDeriveTreeSecret(t, tc.CipherSuite, &tc.DeriveTreeSecret) 158 | }) 159 | t.Run("sign_with_label", func(t *testing.T) { 160 | testSignWithLabel(t, tc.CipherSuite, &tc.SignWithLabel) 161 | }) 162 | t.Run("encrypt_with_label", func(t *testing.T) { 163 | testEncryptWithLabel(t, tc.CipherSuite, &tc.EncryptWithLabel) 164 | }) 165 | }) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "golang.org/x/crypto/cryptobyte" 8 | ) 9 | 10 | type ( 11 | hpkePublicKey []byte 12 | signaturePublicKey []byte 13 | ) 14 | 15 | type credentialType uint16 16 | 17 | // https://www.iana.org/assignments/mls/mls.xhtml#mls-credential-types 18 | const ( 19 | credentialTypeBasic credentialType = 0x0001 20 | credentialTypeX509 credentialType = 0x0002 21 | ) 22 | 23 | type credential struct { 24 | credentialType credentialType 25 | identity []byte // for credentialTypeBasic 26 | certificates [][]byte // for credentialTypeX509 27 | } 28 | 29 | func (cred *credential) unmarshal(s *cryptobyte.String) error { 30 | *cred = credential{} 31 | 32 | if !s.ReadUint16((*uint16)(&cred.credentialType)) { 33 | return io.ErrUnexpectedEOF 34 | } 35 | 36 | switch cred.credentialType { 37 | case credentialTypeBasic: 38 | if !readOpaqueVec(s, &cred.identity) { 39 | return io.ErrUnexpectedEOF 40 | } 41 | return nil 42 | case credentialTypeX509: 43 | return readVector(s, func(s *cryptobyte.String) error { 44 | var cert []byte 45 | if !readOpaqueVec(s, &cert) { 46 | return io.ErrUnexpectedEOF 47 | } 48 | cred.certificates = append(cred.certificates, cert) 49 | return nil 50 | }) 51 | default: 52 | return fmt.Errorf("mls: invalid credential type %d", cred.credentialType) 53 | } 54 | } 55 | 56 | func (cred *credential) marshal(b *cryptobyte.Builder) { 57 | b.AddUint16(uint16(cred.credentialType)) 58 | switch cred.credentialType { 59 | case credentialTypeBasic: 60 | writeOpaqueVec(b, cred.identity) 61 | case credentialTypeX509: 62 | writeVector(b, len(cred.certificates), func(b *cryptobyte.Builder, i int) { 63 | writeOpaqueVec(b, cred.certificates[i]) 64 | }) 65 | default: 66 | panic("unreachable") 67 | } 68 | } 69 | 70 | type hpkeCiphertext struct { 71 | kemOutput []byte 72 | ciphertext []byte 73 | } 74 | 75 | func (hpke *hpkeCiphertext) unmarshal(s *cryptobyte.String) error { 76 | *hpke = hpkeCiphertext{} 77 | if !readOpaqueVec(s, &hpke.kemOutput) || !readOpaqueVec(s, &hpke.ciphertext) { 78 | return io.ErrUnexpectedEOF 79 | } 80 | return nil 81 | } 82 | 83 | func (hpke *hpkeCiphertext) marshal(b *cryptobyte.Builder) { 84 | writeOpaqueVec(b, hpke.kemOutput) 85 | writeOpaqueVec(b, hpke.ciphertext) 86 | } 87 | -------------------------------------------------------------------------------- /framing.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "io" 7 | 8 | "golang.org/x/crypto/cryptobyte" 9 | ) 10 | 11 | type protocolVersion uint16 12 | 13 | const ( 14 | protocolVersionMLS10 protocolVersion = 1 15 | ) 16 | 17 | type contentType uint8 18 | 19 | const ( 20 | contentTypeApplication contentType = 1 21 | contentTypeProposal contentType = 2 22 | contentTypeCommit contentType = 3 23 | ) 24 | 25 | func (ct *contentType) unmarshal(s *cryptobyte.String) error { 26 | if !s.ReadUint8((*uint8)(ct)) { 27 | return io.ErrUnexpectedEOF 28 | } 29 | switch *ct { 30 | case contentTypeApplication, contentTypeProposal, contentTypeCommit: 31 | return nil 32 | default: 33 | return fmt.Errorf("mls: invalid content type %d", *ct) 34 | } 35 | } 36 | 37 | func (ct contentType) marshal(b *cryptobyte.Builder) { 38 | b.AddUint8(uint8(ct)) 39 | } 40 | 41 | type senderType uint8 42 | 43 | const ( 44 | senderTypeMember senderType = 1 45 | senderTypeExternal senderType = 2 46 | senderTypeNewMemberProposal senderType = 3 47 | senderTypeNewMemberCommit senderType = 4 48 | ) 49 | 50 | func (st *senderType) unmarshal(s *cryptobyte.String) error { 51 | if !s.ReadUint8((*uint8)(st)) { 52 | return io.ErrUnexpectedEOF 53 | } 54 | switch *st { 55 | case senderTypeMember, senderTypeExternal, senderTypeNewMemberProposal, senderTypeNewMemberCommit: 56 | return nil 57 | default: 58 | return fmt.Errorf("mls: invalid sender type %d", *st) 59 | } 60 | } 61 | 62 | func (st senderType) marshal(b *cryptobyte.Builder) { 63 | b.AddUint8(uint8(st)) 64 | } 65 | 66 | type sender struct { 67 | senderType senderType 68 | leafIndex leafIndex // for senderTypeMember 69 | senderIndex uint32 // for senderTypeExternal 70 | } 71 | 72 | func (snd *sender) unmarshal(s *cryptobyte.String) error { 73 | *snd = sender{} 74 | if err := snd.senderType.unmarshal(s); err != nil { 75 | return err 76 | } 77 | switch snd.senderType { 78 | case senderTypeMember: 79 | if !s.ReadUint32((*uint32)(&snd.leafIndex)) { 80 | return io.ErrUnexpectedEOF 81 | } 82 | case senderTypeExternal: 83 | if !s.ReadUint32(&snd.senderIndex) { 84 | return io.ErrUnexpectedEOF 85 | } 86 | } 87 | return nil 88 | } 89 | 90 | func (snd *sender) marshal(b *cryptobyte.Builder) { 91 | snd.senderType.marshal(b) 92 | switch snd.senderType { 93 | case senderTypeMember: 94 | b.AddUint32(uint32(snd.leafIndex)) 95 | case senderTypeExternal: 96 | b.AddUint32(snd.senderIndex) 97 | } 98 | } 99 | 100 | type wireFormat uint16 101 | 102 | // http://www.iana.org/assignments/mls/mls.xhtml#mls-wire-formats 103 | const ( 104 | wireFormatMLSPublicMessage wireFormat = 0x0001 105 | wireFormatMLSPrivateMessage wireFormat = 0x0002 106 | wireFormatMLSWelcome wireFormat = 0x0003 107 | wireFormatMLSGroupInfo wireFormat = 0x0004 108 | wireFormatMLSKeyPackage wireFormat = 0x0005 109 | ) 110 | 111 | func (wf *wireFormat) unmarshal(s *cryptobyte.String) error { 112 | if !s.ReadUint16((*uint16)(wf)) { 113 | return io.ErrUnexpectedEOF 114 | } 115 | switch *wf { 116 | case wireFormatMLSPublicMessage, wireFormatMLSPrivateMessage, wireFormatMLSWelcome, wireFormatMLSGroupInfo, wireFormatMLSKeyPackage: 117 | return nil 118 | default: 119 | return fmt.Errorf("mls: invalid wire format %d", *wf) 120 | } 121 | } 122 | 123 | func (wf wireFormat) marshal(b *cryptobyte.Builder) { 124 | b.AddUint16(uint16(wf)) 125 | } 126 | 127 | // GroupID is an application-specific group identifier. 128 | type GroupID []byte 129 | 130 | type framedContent struct { 131 | groupID GroupID 132 | epoch uint64 133 | sender sender 134 | authenticatedData []byte 135 | 136 | contentType contentType 137 | applicationData []byte // for contentTypeApplication 138 | proposal *proposal // for contentTypeProposal 139 | commit *commit // for contentTypeCommit 140 | } 141 | 142 | func (content *framedContent) unmarshal(s *cryptobyte.String) error { 143 | *content = framedContent{} 144 | 145 | if !readOpaqueVec(s, (*[]byte)(&content.groupID)) || !s.ReadUint64(&content.epoch) { 146 | return io.ErrUnexpectedEOF 147 | } 148 | if err := content.sender.unmarshal(s); err != nil { 149 | return err 150 | } 151 | if !readOpaqueVec(s, &content.authenticatedData) { 152 | return io.ErrUnexpectedEOF 153 | } 154 | if err := content.contentType.unmarshal(s); err != nil { 155 | return err 156 | } 157 | 158 | switch content.contentType { 159 | case contentTypeApplication: 160 | if !readOpaqueVec(s, &content.applicationData) { 161 | return io.ErrUnexpectedEOF 162 | } 163 | return nil 164 | case contentTypeProposal: 165 | content.proposal = new(proposal) 166 | return content.proposal.unmarshal(s) 167 | case contentTypeCommit: 168 | content.commit = new(commit) 169 | return content.commit.unmarshal(s) 170 | default: 171 | panic("unreachable") 172 | } 173 | } 174 | 175 | func (content *framedContent) marshal(b *cryptobyte.Builder) { 176 | writeOpaqueVec(b, []byte(content.groupID)) 177 | b.AddUint64(content.epoch) 178 | content.sender.marshal(b) 179 | writeOpaqueVec(b, content.authenticatedData) 180 | content.contentType.marshal(b) 181 | switch content.contentType { 182 | case contentTypeApplication: 183 | writeOpaqueVec(b, content.applicationData) 184 | case contentTypeProposal: 185 | content.proposal.marshal(b) 186 | case contentTypeCommit: 187 | content.commit.marshal(b) 188 | default: 189 | panic("unreachable") 190 | } 191 | } 192 | 193 | type mlsMessage struct { 194 | version protocolVersion 195 | wireFormat wireFormat 196 | publicMessage *publicMessage // for wireFormatMLSPublicMessage 197 | privateMessage *privateMessage // for wireFormatMLSPrivateMessage 198 | welcome *welcome // for wireFormatMLSWelcome 199 | groupInfo *groupInfo // for wireFormatMLSGroupInfo 200 | keyPackage *keyPackage // for wireFormatMLSKeyPackage 201 | } 202 | 203 | func (msg *mlsMessage) unmarshal(s *cryptobyte.String) error { 204 | *msg = mlsMessage{} 205 | 206 | if !s.ReadUint16((*uint16)(&msg.version)) { 207 | return io.ErrUnexpectedEOF 208 | } 209 | if msg.version != protocolVersionMLS10 { 210 | return fmt.Errorf("mls: invalid protocol version %d", msg.version) 211 | } 212 | 213 | if err := msg.wireFormat.unmarshal(s); err != nil { 214 | return err 215 | } 216 | 217 | switch msg.wireFormat { 218 | case wireFormatMLSPublicMessage: 219 | msg.publicMessage = new(publicMessage) 220 | return msg.publicMessage.unmarshal(s) 221 | case wireFormatMLSPrivateMessage: 222 | msg.privateMessage = new(privateMessage) 223 | return msg.privateMessage.unmarshal(s) 224 | case wireFormatMLSWelcome: 225 | msg.welcome = new(welcome) 226 | return msg.welcome.unmarshal(s) 227 | case wireFormatMLSGroupInfo: 228 | msg.groupInfo = new(groupInfo) 229 | return msg.groupInfo.unmarshal(s) 230 | case wireFormatMLSKeyPackage: 231 | msg.keyPackage = new(keyPackage) 232 | return msg.keyPackage.unmarshal(s) 233 | default: 234 | panic("unreachable") 235 | } 236 | } 237 | 238 | func (msg *mlsMessage) marshal(b *cryptobyte.Builder) { 239 | b.AddUint16(uint16(msg.version)) 240 | msg.wireFormat.marshal(b) 241 | switch msg.wireFormat { 242 | case wireFormatMLSPublicMessage: 243 | msg.publicMessage.marshal(b) 244 | case wireFormatMLSPrivateMessage: 245 | msg.privateMessage.marshal(b) 246 | case wireFormatMLSWelcome: 247 | msg.welcome.marshal(b) 248 | case wireFormatMLSGroupInfo: 249 | msg.groupInfo.marshal(b) 250 | case wireFormatMLSKeyPackage: 251 | msg.keyPackage.marshal(b) 252 | default: 253 | panic("unreachable") 254 | } 255 | } 256 | 257 | type authenticatedContent struct { 258 | wireFormat wireFormat 259 | content framedContent 260 | auth framedContentAuthData 261 | } 262 | 263 | func signAuthenticatedContent(cs cipherSuite, signKey []byte, wf wireFormat, content *framedContent, ctx *groupContext) (*authenticatedContent, error) { 264 | authContent := authenticatedContent{ 265 | wireFormat: wf, 266 | content: *content, 267 | } 268 | tbs := authContent.framedContentTBS(ctx) 269 | signature, err := signFramedContent(cs, signKey, tbs) 270 | if err != nil { 271 | return nil, err 272 | } 273 | authContent.auth.signature = signature 274 | return &authContent, nil 275 | } 276 | 277 | func (authContent *authenticatedContent) unmarshal(s *cryptobyte.String) error { 278 | if err := authContent.wireFormat.unmarshal(s); err != nil { 279 | return err 280 | } 281 | if err := authContent.content.unmarshal(s); err != nil { 282 | return err 283 | } 284 | if err := authContent.auth.unmarshal(s, authContent.content.contentType); err != nil { 285 | return err 286 | } 287 | return nil 288 | } 289 | 290 | func (authContent *authenticatedContent) marshal(b *cryptobyte.Builder) { 291 | authContent.wireFormat.marshal(b) 292 | authContent.content.marshal(b) 293 | authContent.auth.marshal(b, authContent.content.contentType) 294 | } 295 | 296 | func (authContent *authenticatedContent) confirmedTranscriptHashInput() *confirmedTranscriptHashInput { 297 | return &confirmedTranscriptHashInput{ 298 | wireFormat: authContent.wireFormat, 299 | content: authContent.content, 300 | signature: authContent.auth.signature, 301 | } 302 | } 303 | 304 | func (authContent *authenticatedContent) framedContentTBS(ctx *groupContext) *framedContentTBS { 305 | return &framedContentTBS{ 306 | version: protocolVersionMLS10, 307 | wireFormat: authContent.wireFormat, 308 | content: authContent.content, 309 | context: ctx, 310 | } 311 | } 312 | 313 | func (authContent *authenticatedContent) verifySignature(cs cipherSuite, verifKey []byte, ctx *groupContext) bool { 314 | return authContent.auth.verifySignature(cs, verifKey, authContent.framedContentTBS(ctx)) 315 | } 316 | 317 | func (authContent *authenticatedContent) generateProposalRef(cs cipherSuite) (proposalRef, error) { 318 | if authContent.content.contentType != contentTypeProposal { 319 | panic("mls: AuthenticatedContent is not a proposal") 320 | } 321 | 322 | var b cryptobyte.Builder 323 | authContent.marshal(&b) 324 | raw, err := b.Bytes() 325 | if err != nil { 326 | return nil, err 327 | } 328 | 329 | hash, err := cs.refHash([]byte("MLS 1.0 Proposal Reference"), raw) 330 | if err != nil { 331 | return nil, err 332 | } 333 | 334 | return proposalRef(hash), nil 335 | } 336 | 337 | type framedContentAuthData struct { 338 | signature []byte 339 | confirmationTag []byte // for contentTypeCommit 340 | } 341 | 342 | func (authData *framedContentAuthData) unmarshal(s *cryptobyte.String, ct contentType) error { 343 | *authData = framedContentAuthData{} 344 | 345 | if !readOpaqueVec(s, &authData.signature) { 346 | return io.ErrUnexpectedEOF 347 | } 348 | 349 | if ct == contentTypeCommit { 350 | if !readOpaqueVec(s, &authData.confirmationTag) { 351 | return io.ErrUnexpectedEOF 352 | } 353 | } 354 | 355 | return nil 356 | } 357 | 358 | func (authData *framedContentAuthData) marshal(b *cryptobyte.Builder, ct contentType) { 359 | writeOpaqueVec(b, authData.signature) 360 | 361 | if ct == contentTypeCommit { 362 | writeOpaqueVec(b, authData.confirmationTag) 363 | } 364 | } 365 | 366 | func (authData *framedContentAuthData) verifyConfirmationTag(cs cipherSuite, confirmationKey, confirmedTranscriptHash []byte) bool { 367 | if len(authData.confirmationTag) == 0 { 368 | return false 369 | } 370 | return cs.verifyMAC(confirmationKey, confirmedTranscriptHash, authData.confirmationTag) 371 | } 372 | 373 | func (authData *framedContentAuthData) verifySignature(cs cipherSuite, verifKey []byte, content *framedContentTBS) bool { 374 | rawContent, err := marshal(content) 375 | if err != nil { 376 | return false 377 | } 378 | return cs.verifyWithLabel(verifKey, []byte("FramedContentTBS"), rawContent, authData.signature) 379 | } 380 | 381 | func signFramedContent(cs cipherSuite, signKey []byte, content *framedContentTBS) ([]byte, error) { 382 | rawContent, err := marshal(content) 383 | if err != nil { 384 | return nil, err 385 | } 386 | return cs.signWithLabel(signKey, []byte("FramedContentTBS"), rawContent) 387 | } 388 | 389 | type framedContentTBS struct { 390 | version protocolVersion 391 | wireFormat wireFormat 392 | content framedContent 393 | context *groupContext // for senderTypeMember and senderTypeNewMemberCommit 394 | } 395 | 396 | func (content *framedContentTBS) marshal(b *cryptobyte.Builder) { 397 | b.AddUint16(uint16(content.version)) 398 | content.wireFormat.marshal(b) 399 | content.content.marshal(b) 400 | switch content.content.sender.senderType { 401 | case senderTypeMember, senderTypeNewMemberCommit: 402 | content.context.marshal(b) 403 | } 404 | } 405 | 406 | type publicMessage struct { 407 | content framedContent 408 | auth framedContentAuthData 409 | membershipTag []byte // for senderTypeMember 410 | } 411 | 412 | func signPublicMessage(cs cipherSuite, signKey []byte, content *framedContent, ctx *groupContext) (*publicMessage, error) { 413 | authContent, err := signAuthenticatedContent(cs, signKey, wireFormatMLSPublicMessage, content, ctx) 414 | if err != nil { 415 | return nil, err 416 | } 417 | return &publicMessage{ 418 | content: authContent.content, 419 | auth: authContent.auth, 420 | }, nil 421 | } 422 | 423 | func (msg *publicMessage) unmarshal(s *cryptobyte.String) error { 424 | *msg = publicMessage{} 425 | 426 | if err := msg.content.unmarshal(s); err != nil { 427 | return err 428 | } 429 | if err := msg.auth.unmarshal(s, msg.content.contentType); err != nil { 430 | return err 431 | } 432 | 433 | if msg.content.sender.senderType == senderTypeMember { 434 | if !readOpaqueVec(s, &msg.membershipTag) { 435 | return io.ErrUnexpectedEOF 436 | } 437 | } 438 | 439 | return nil 440 | } 441 | 442 | func (msg *publicMessage) marshal(b *cryptobyte.Builder) { 443 | msg.content.marshal(b) 444 | msg.auth.marshal(b, msg.content.contentType) 445 | 446 | if msg.content.sender.senderType == senderTypeMember { 447 | writeOpaqueVec(b, msg.membershipTag) 448 | } 449 | } 450 | 451 | func (msg *publicMessage) authenticatedContent() *authenticatedContent { 452 | return &authenticatedContent{ 453 | wireFormat: wireFormatMLSPublicMessage, 454 | content: msg.content, 455 | auth: msg.auth, 456 | } 457 | } 458 | 459 | func (msg *publicMessage) authenticatedContentTBM(ctx *groupContext) *authenticatedContentTBM { 460 | return &authenticatedContentTBM{ 461 | contentTBS: *msg.authenticatedContent().framedContentTBS(ctx), 462 | auth: msg.auth, 463 | } 464 | } 465 | 466 | func (msg *publicMessage) signMembershipTag(cs cipherSuite, membershipKey []byte, ctx *groupContext) error { 467 | if msg.content.sender.senderType != senderTypeMember { 468 | return nil 469 | } 470 | rawAuthContentTBM, err := marshal(msg.authenticatedContentTBM(ctx)) 471 | if err != nil { 472 | return err 473 | } 474 | msg.membershipTag = cs.signMAC(membershipKey, rawAuthContentTBM) 475 | return nil 476 | } 477 | 478 | func (msg *publicMessage) verifyMembershipTag(cs cipherSuite, membershipKey []byte, ctx *groupContext) bool { 479 | if msg.content.sender.senderType != senderTypeMember { 480 | return true // there is no membership tag 481 | } 482 | rawAuthContentTBM, err := marshal(msg.authenticatedContentTBM(ctx)) 483 | if err != nil { 484 | return false 485 | } 486 | return cs.verifyMAC(membershipKey, rawAuthContentTBM, msg.membershipTag) 487 | } 488 | 489 | type authenticatedContentTBM struct { 490 | contentTBS framedContentTBS 491 | auth framedContentAuthData 492 | } 493 | 494 | func (tbm *authenticatedContentTBM) marshal(b *cryptobyte.Builder) { 495 | tbm.contentTBS.marshal(b) 496 | tbm.auth.marshal(b, tbm.contentTBS.content.contentType) 497 | } 498 | 499 | type privateMessage struct { 500 | groupID GroupID 501 | epoch uint64 502 | contentType contentType 503 | authenticatedData []byte 504 | encryptedSenderData []byte 505 | ciphertext []byte 506 | } 507 | 508 | func encryptPrivateMessage(cs cipherSuite, signPriv []byte, secret ratchetSecret, senderDataSecret []byte, content *framedContent, senderData *senderData, ctx *groupContext) (*privateMessage, error) { 509 | ciphertext, err := encryptPrivateMessageContent(cs, signPriv, secret, content, ctx, senderData.reuseGuard) 510 | if err != nil { 511 | return nil, err 512 | } 513 | encryptedSenderData, err := encryptSenderData(cs, senderDataSecret, senderData, content, ciphertext) 514 | if err != nil { 515 | return nil, err 516 | } 517 | return &privateMessage{ 518 | groupID: content.groupID, 519 | epoch: content.epoch, 520 | contentType: content.contentType, 521 | authenticatedData: content.authenticatedData, 522 | encryptedSenderData: encryptedSenderData, 523 | ciphertext: ciphertext, 524 | }, nil 525 | } 526 | 527 | func (msg *privateMessage) unmarshal(s *cryptobyte.String) error { 528 | *msg = privateMessage{} 529 | ok := readOpaqueVec(s, (*[]byte)(&msg.groupID)) && 530 | s.ReadUint64(&msg.epoch) 531 | if !ok { 532 | return io.ErrUnexpectedEOF 533 | } 534 | if err := msg.contentType.unmarshal(s); err != nil { 535 | return err 536 | } 537 | ok = readOpaqueVec(s, &msg.authenticatedData) && 538 | readOpaqueVec(s, &msg.encryptedSenderData) && 539 | readOpaqueVec(s, &msg.ciphertext) 540 | if !ok { 541 | return io.ErrUnexpectedEOF 542 | } 543 | return nil 544 | } 545 | 546 | func (msg *privateMessage) marshal(b *cryptobyte.Builder) { 547 | writeOpaqueVec(b, []byte(msg.groupID)) 548 | b.AddUint64(msg.epoch) 549 | msg.contentType.marshal(b) 550 | writeOpaqueVec(b, msg.authenticatedData) 551 | writeOpaqueVec(b, msg.encryptedSenderData) 552 | writeOpaqueVec(b, msg.ciphertext) 553 | } 554 | 555 | func (msg *privateMessage) decryptSenderData(cs cipherSuite, senderDataSecret []byte) (*senderData, error) { 556 | key, err := expandSenderDataKey(cs, senderDataSecret, msg.ciphertext) 557 | if err != nil { 558 | return nil, err 559 | } 560 | nonce, err := expandSenderDataNonce(cs, senderDataSecret, msg.ciphertext) 561 | if err != nil { 562 | return nil, err 563 | } 564 | 565 | aad := senderDataAAD{ 566 | groupID: msg.groupID, 567 | epoch: msg.epoch, 568 | contentType: msg.contentType, 569 | } 570 | rawAAD, err := marshal(&aad) 571 | if err != nil { 572 | return nil, err 573 | } 574 | 575 | _, _, aead := cs.hpke().Params() 576 | cipher, err := aead.New(key) 577 | if err != nil { 578 | return nil, err 579 | } 580 | 581 | rawSenderData, err := cipher.Open(nil, nonce, msg.encryptedSenderData, rawAAD) 582 | if err != nil { 583 | return nil, err 584 | } 585 | 586 | var senderData senderData 587 | if err := unmarshal(rawSenderData, &senderData); err != nil { 588 | return nil, err 589 | } 590 | 591 | return &senderData, nil 592 | } 593 | 594 | func (msg *privateMessage) decryptContent(cs cipherSuite, secret ratchetSecret, reuseGuard [4]byte) (*privateMessageContent, error) { 595 | key, nonce, err := derivePrivateMessageKeyAndNonce(cs, secret, reuseGuard) 596 | if err != nil { 597 | return nil, err 598 | } 599 | 600 | aad := privateContentAAD{ 601 | groupID: msg.groupID, 602 | epoch: msg.epoch, 603 | contentType: msg.contentType, 604 | authenticatedData: msg.authenticatedData, 605 | } 606 | rawAAD, err := marshal(&aad) 607 | if err != nil { 608 | return nil, err 609 | } 610 | 611 | _, _, aead := cs.hpke().Params() 612 | cipher, err := aead.New(key) 613 | if err != nil { 614 | return nil, err 615 | } 616 | 617 | rawContent, err := cipher.Open(nil, nonce, msg.ciphertext, rawAAD) 618 | if err != nil { 619 | return nil, err 620 | } 621 | 622 | s := cryptobyte.String(rawContent) 623 | var content privateMessageContent 624 | if err := content.unmarshal(&s, msg.contentType); err != nil { 625 | return nil, err 626 | } 627 | 628 | for _, v := range s { 629 | if v != 0 { 630 | return nil, fmt.Errorf("mls: padding contains non-zero bytes") 631 | } 632 | } 633 | 634 | return &content, nil 635 | } 636 | 637 | func derivePrivateMessageKeyAndNonce(cs cipherSuite, secret ratchetSecret, reuseGuard [4]byte) (key, nonce []byte, err error) { 638 | key, err = secret.deriveKey(cs) 639 | if err != nil { 640 | return nil, nil, err 641 | } 642 | nonce, err = secret.deriveNonce(cs) 643 | if err != nil { 644 | return nil, nil, err 645 | } 646 | 647 | for i := range reuseGuard { 648 | nonce[i] = nonce[i] ^ reuseGuard[i] 649 | } 650 | 651 | return key, nonce, nil 652 | } 653 | 654 | func (msg *privateMessage) authenticatedContent(senderData *senderData, content *privateMessageContent) *authenticatedContent { 655 | return &authenticatedContent{ 656 | wireFormat: wireFormatMLSPrivateMessage, 657 | content: framedContent{ 658 | groupID: msg.groupID, 659 | epoch: msg.epoch, 660 | sender: sender{ 661 | senderType: senderTypeMember, 662 | leafIndex: senderData.leafIndex, 663 | }, 664 | authenticatedData: msg.authenticatedData, 665 | contentType: msg.contentType, 666 | applicationData: content.applicationData, 667 | proposal: content.proposal, 668 | commit: content.commit, 669 | }, 670 | auth: content.auth, 671 | } 672 | } 673 | 674 | type senderDataAAD struct { 675 | groupID GroupID 676 | epoch uint64 677 | contentType contentType 678 | } 679 | 680 | func (aad *senderDataAAD) marshal(b *cryptobyte.Builder) { 681 | writeOpaqueVec(b, []byte(aad.groupID)) 682 | b.AddUint64(aad.epoch) 683 | aad.contentType.marshal(b) 684 | } 685 | 686 | type privateContentAAD struct { 687 | groupID GroupID 688 | epoch uint64 689 | contentType contentType 690 | authenticatedData []byte 691 | } 692 | 693 | func (aad *privateContentAAD) marshal(b *cryptobyte.Builder) { 694 | writeOpaqueVec(b, []byte(aad.groupID)) 695 | b.AddUint64(aad.epoch) 696 | aad.contentType.marshal(b) 697 | writeOpaqueVec(b, aad.authenticatedData) 698 | } 699 | 700 | type privateMessageContent struct { 701 | applicationData []byte // for contentTypeApplication 702 | proposal *proposal // for contentTypeProposal 703 | commit *commit // for contentTypeCommit 704 | 705 | auth framedContentAuthData 706 | } 707 | 708 | func (content *privateMessageContent) unmarshal(s *cryptobyte.String, ct contentType) error { 709 | *content = privateMessageContent{} 710 | 711 | var err error 712 | switch ct { 713 | case contentTypeApplication: 714 | if !readOpaqueVec(s, &content.applicationData) { 715 | err = io.ErrUnexpectedEOF 716 | } 717 | case contentTypeProposal: 718 | content.proposal = new(proposal) 719 | err = content.proposal.unmarshal(s) 720 | case contentTypeCommit: 721 | content.commit = new(commit) 722 | err = content.commit.unmarshal(s) 723 | default: 724 | panic("unreachable") 725 | } 726 | if err != nil { 727 | return err 728 | } 729 | 730 | return content.auth.unmarshal(s, ct) 731 | } 732 | 733 | func (content *privateMessageContent) marshal(b *cryptobyte.Builder, ct contentType) { 734 | switch ct { 735 | case contentTypeApplication: 736 | writeOpaqueVec(b, content.applicationData) 737 | case contentTypeProposal: 738 | content.proposal.marshal(b) 739 | case contentTypeCommit: 740 | content.commit.marshal(b) 741 | default: 742 | panic("unreachable") 743 | } 744 | content.auth.marshal(b, ct) 745 | } 746 | 747 | func encryptPrivateMessageContent(cs cipherSuite, signKey []byte, secret ratchetSecret, content *framedContent, ctx *groupContext, reuseGuard [4]byte) ([]byte, error) { 748 | authContent, err := signAuthenticatedContent(cs, signKey, wireFormatMLSPrivateMessage, content, ctx) 749 | if err != nil { 750 | return nil, err 751 | } 752 | 753 | privContent := privateMessageContent{ 754 | applicationData: content.applicationData, 755 | proposal: content.proposal, 756 | commit: content.commit, 757 | auth: authContent.auth, 758 | } 759 | var b cryptobyte.Builder 760 | privContent.marshal(&b, content.contentType) 761 | plaintext, err := b.Bytes() 762 | if err != nil { 763 | return nil, err 764 | } 765 | 766 | key, nonce, err := derivePrivateMessageKeyAndNonce(cs, secret, reuseGuard) 767 | if err != nil { 768 | return nil, err 769 | } 770 | 771 | aad := privateContentAAD{ 772 | groupID: content.groupID, 773 | epoch: content.epoch, 774 | contentType: content.contentType, 775 | authenticatedData: content.authenticatedData, 776 | } 777 | rawAAD, err := marshal(&aad) 778 | if err != nil { 779 | return nil, err 780 | } 781 | 782 | _, _, aead := cs.hpke().Params() 783 | cipher, err := aead.New(key) 784 | if err != nil { 785 | return nil, err 786 | } 787 | 788 | return cipher.Seal(nil, nonce, plaintext, rawAAD), nil 789 | } 790 | 791 | func encryptSenderData(cs cipherSuite, senderDataSecret []byte, senderData *senderData, content *framedContent, ciphertext []byte) ([]byte, error) { 792 | key, err := expandSenderDataKey(cs, senderDataSecret, ciphertext) 793 | if err != nil { 794 | return nil, err 795 | } 796 | nonce, err := expandSenderDataNonce(cs, senderDataSecret, ciphertext) 797 | if err != nil { 798 | return nil, err 799 | } 800 | 801 | aad := senderDataAAD{ 802 | groupID: content.groupID, 803 | epoch: content.epoch, 804 | contentType: content.contentType, 805 | } 806 | rawAAD, err := marshal(&aad) 807 | if err != nil { 808 | return nil, err 809 | } 810 | 811 | _, _, aead := cs.hpke().Params() 812 | cipher, err := aead.New(key) 813 | if err != nil { 814 | return nil, err 815 | } 816 | 817 | rawSenderData, err := marshal(senderData) 818 | if err != nil { 819 | return nil, err 820 | } 821 | 822 | return cipher.Seal(nil, nonce, rawSenderData, rawAAD), nil 823 | } 824 | 825 | type senderData struct { 826 | leafIndex leafIndex 827 | generation uint32 828 | reuseGuard [4]byte 829 | } 830 | 831 | func newSenderData(leafIndex leafIndex, generation uint32) (*senderData, error) { 832 | data := senderData{ 833 | leafIndex: leafIndex, 834 | generation: generation, 835 | } 836 | if _, err := rand.Read(data.reuseGuard[:]); err != nil { 837 | return nil, err 838 | } 839 | return &data, nil 840 | } 841 | 842 | func (data *senderData) unmarshal(s *cryptobyte.String) error { 843 | if !s.ReadUint32((*uint32)(&data.leafIndex)) || !s.ReadUint32(&data.generation) || !s.CopyBytes(data.reuseGuard[:]) { 844 | return io.ErrUnexpectedEOF 845 | } 846 | return nil 847 | } 848 | 849 | func (data *senderData) marshal(b *cryptobyte.Builder) { 850 | b.AddUint32(uint32(data.leafIndex)) 851 | b.AddUint32(data.generation) 852 | b.AddBytes(data.reuseGuard[:]) 853 | } 854 | 855 | func expandSenderDataKey(cs cipherSuite, senderDataSecret, ciphertext []byte) ([]byte, error) { 856 | _, _, aead := cs.hpke().Params() 857 | ciphertextSample := sampleCiphertext(cs, ciphertext) 858 | return cs.expandWithLabel(senderDataSecret, []byte("key"), ciphertextSample, uint16(aead.KeySize())) 859 | } 860 | 861 | func expandSenderDataNonce(cs cipherSuite, senderDataSecret, ciphertext []byte) ([]byte, error) { 862 | _, _, aead := cs.hpke().Params() 863 | ciphertextSample := sampleCiphertext(cs, ciphertext) 864 | return cs.expandWithLabel(senderDataSecret, []byte("nonce"), ciphertextSample, uint16(aead.NonceSize())) 865 | } 866 | 867 | func sampleCiphertext(cs cipherSuite, ciphertext []byte) []byte { 868 | _, kdf, _ := cs.hpke().Params() 869 | n := kdf.ExtractSize() 870 | if len(ciphertext) < n { 871 | return ciphertext 872 | } 873 | return ciphertext[:n] 874 | } 875 | -------------------------------------------------------------------------------- /framing_test.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func testMessages(t *testing.T, tc map[string]testBytes) { 10 | msgs := []struct { 11 | name string 12 | v interface { 13 | unmarshaler 14 | marshaler 15 | } 16 | }{ 17 | {"mls_welcome", new(mlsMessage)}, 18 | {"mls_group_info", new(mlsMessage)}, 19 | {"mls_key_package", new(mlsMessage)}, 20 | 21 | {"ratchet_tree", new(ratchetTree)}, 22 | {"group_secrets", new(groupSecrets)}, 23 | 24 | {"add_proposal", new(add)}, 25 | {"update_proposal", new(update)}, 26 | {"remove_proposal", new(remove)}, 27 | {"pre_shared_key_proposal", new(preSharedKey)}, 28 | {"re_init_proposal", new(reInit)}, 29 | {"external_init_proposal", new(externalInit)}, 30 | {"group_context_extensions_proposal", new(groupContextExtensions)}, 31 | 32 | {"commit", new(commit)}, 33 | 34 | {"public_message_application", new(mlsMessage)}, 35 | {"public_message_proposal", new(mlsMessage)}, 36 | {"public_message_commit", new(mlsMessage)}, 37 | {"private_message", new(mlsMessage)}, 38 | } 39 | for _, msg := range msgs { 40 | t.Run(msg.name, func(t *testing.T) { 41 | raw, ok := tc[msg.name] 42 | if !ok { 43 | t.Fatal("reference blob not found") 44 | } 45 | if err := unmarshal(raw, msg.v); err != nil { 46 | t.Fatalf("unmarshal() = %v", err) 47 | } 48 | 49 | out, err := marshal(msg.v) 50 | if err != nil { 51 | t.Errorf("marshal() = %v", err) 52 | } else if !bytes.Equal(out, raw) { 53 | t.Errorf("marshal() = \n%v\nbut want \n%v", out, raw) 54 | } 55 | }) 56 | } 57 | } 58 | 59 | func TestMessages(t *testing.T) { 60 | var tests []map[string]testBytes 61 | loadTestVector(t, "testdata/messages.json", &tests) 62 | 63 | for i, tc := range tests { 64 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 65 | testMessages(t, tc) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emersion/go-mls 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/cloudflare/circl v1.3.7 7 | golang.org/x/crypto v0.21.0 8 | ) 9 | 10 | require golang.org/x/sys v0.18.0 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 2 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 3 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 4 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 5 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 6 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 7 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "golang.org/x/crypto/cryptobyte" 9 | ) 10 | 11 | // http://www.iana.org/assignments/mls/mls.xhtml#mls-proposal-types 12 | type proposalType uint16 13 | 14 | const ( 15 | proposalTypeAdd proposalType = 0x0001 16 | proposalTypeUpdate proposalType = 0x0002 17 | proposalTypeRemove proposalType = 0x0003 18 | proposalTypePSK proposalType = 0x0004 19 | proposalTypeReinit proposalType = 0x0005 20 | proposalTypeExternalInit proposalType = 0x0006 21 | proposalTypeGroupContextExtensions proposalType = 0x0007 22 | ) 23 | 24 | func (t *proposalType) unmarshal(s *cryptobyte.String) error { 25 | if !s.ReadUint16((*uint16)(t)) { 26 | return io.ErrUnexpectedEOF 27 | } 28 | switch *t { 29 | case proposalTypeAdd, proposalTypeUpdate, proposalTypeRemove, proposalTypePSK, proposalTypeReinit, proposalTypeExternalInit, proposalTypeGroupContextExtensions: 30 | return nil 31 | default: 32 | return fmt.Errorf("mls: invalid proposal type %d", *t) 33 | } 34 | } 35 | 36 | func (t proposalType) marshal(b *cryptobyte.Builder) { 37 | b.AddUint16(uint16(t)) 38 | } 39 | 40 | type proposal struct { 41 | proposalType proposalType 42 | add *add // for proposalTypeAdd 43 | update *update // for proposalTypeUpdate 44 | remove *remove // for proposalTypeRemove 45 | preSharedKey *preSharedKey // for proposalTypePSK 46 | reInit *reInit // for proposalTypeReinit 47 | externalInit *externalInit // for proposalTypeExternalInit 48 | groupContextExtensions *groupContextExtensions // for proposalTypeGroupContextExtensions 49 | } 50 | 51 | func (prop *proposal) unmarshal(s *cryptobyte.String) error { 52 | *prop = proposal{} 53 | if err := prop.proposalType.unmarshal(s); err != nil { 54 | return err 55 | } 56 | switch prop.proposalType { 57 | case proposalTypeAdd: 58 | prop.add = new(add) 59 | return prop.add.unmarshal(s) 60 | case proposalTypeUpdate: 61 | prop.update = new(update) 62 | return prop.update.unmarshal(s) 63 | case proposalTypeRemove: 64 | prop.remove = new(remove) 65 | return prop.remove.unmarshal(s) 66 | case proposalTypePSK: 67 | prop.preSharedKey = new(preSharedKey) 68 | return prop.preSharedKey.unmarshal(s) 69 | case proposalTypeReinit: 70 | prop.reInit = new(reInit) 71 | return prop.reInit.unmarshal(s) 72 | case proposalTypeExternalInit: 73 | prop.externalInit = new(externalInit) 74 | return prop.externalInit.unmarshal(s) 75 | case proposalTypeGroupContextExtensions: 76 | prop.groupContextExtensions = new(groupContextExtensions) 77 | return prop.groupContextExtensions.unmarshal(s) 78 | default: 79 | panic("unreachable") 80 | } 81 | } 82 | 83 | func (prop *proposal) marshal(b *cryptobyte.Builder) { 84 | prop.proposalType.marshal(b) 85 | switch prop.proposalType { 86 | case proposalTypeAdd: 87 | prop.add.marshal(b) 88 | case proposalTypeUpdate: 89 | prop.update.marshal(b) 90 | case proposalTypeRemove: 91 | prop.remove.marshal(b) 92 | case proposalTypePSK: 93 | prop.preSharedKey.marshal(b) 94 | case proposalTypeReinit: 95 | prop.reInit.marshal(b) 96 | case proposalTypeExternalInit: 97 | prop.externalInit.marshal(b) 98 | case proposalTypeGroupContextExtensions: 99 | prop.groupContextExtensions.marshal(b) 100 | default: 101 | panic("unreachable") 102 | } 103 | } 104 | 105 | type add struct { 106 | keyPackage keyPackage 107 | } 108 | 109 | func (a *add) unmarshal(s *cryptobyte.String) error { 110 | *a = add{} 111 | return a.keyPackage.unmarshal(s) 112 | } 113 | 114 | func (a *add) marshal(b *cryptobyte.Builder) { 115 | a.keyPackage.marshal(b) 116 | } 117 | 118 | type update struct { 119 | leafNode leafNode 120 | } 121 | 122 | func (upd *update) unmarshal(s *cryptobyte.String) error { 123 | *upd = update{} 124 | return upd.leafNode.unmarshal(s) 125 | } 126 | 127 | func (upd *update) marshal(b *cryptobyte.Builder) { 128 | upd.leafNode.marshal(b) 129 | } 130 | 131 | type remove struct { 132 | removed leafIndex 133 | } 134 | 135 | func (rm *remove) unmarshal(s *cryptobyte.String) error { 136 | *rm = remove{} 137 | if !s.ReadUint32((*uint32)(&rm.removed)) { 138 | return io.ErrUnexpectedEOF 139 | } 140 | return nil 141 | } 142 | 143 | func (rm *remove) marshal(b *cryptobyte.Builder) { 144 | b.AddUint32(uint32(rm.removed)) 145 | } 146 | 147 | type preSharedKey struct { 148 | psk preSharedKeyID 149 | } 150 | 151 | func (psk *preSharedKey) unmarshal(s *cryptobyte.String) error { 152 | *psk = preSharedKey{} 153 | return psk.psk.unmarshal(s) 154 | } 155 | 156 | func (psk *preSharedKey) marshal(b *cryptobyte.Builder) { 157 | psk.psk.marshal(b) 158 | } 159 | 160 | type reInit struct { 161 | groupID GroupID 162 | version protocolVersion 163 | cipherSuite cipherSuite 164 | extensions []extension 165 | } 166 | 167 | func (ri *reInit) unmarshal(s *cryptobyte.String) error { 168 | *ri = reInit{} 169 | 170 | if !readOpaqueVec(s, (*[]byte)(&ri.groupID)) || !s.ReadUint16((*uint16)(&ri.version)) || !s.ReadUint16((*uint16)(&ri.cipherSuite)) { 171 | return io.ErrUnexpectedEOF 172 | } 173 | 174 | exts, err := unmarshalExtensionVec(s) 175 | if err != nil { 176 | return err 177 | } 178 | ri.extensions = exts 179 | 180 | return nil 181 | } 182 | 183 | func (ri *reInit) marshal(b *cryptobyte.Builder) { 184 | writeOpaqueVec(b, []byte(ri.groupID)) 185 | b.AddUint16(uint16(ri.version)) 186 | b.AddUint16(uint16(ri.cipherSuite)) 187 | marshalExtensionVec(b, ri.extensions) 188 | } 189 | 190 | type externalInit struct { 191 | kemOutput []byte 192 | } 193 | 194 | func (ei *externalInit) unmarshal(s *cryptobyte.String) error { 195 | *ei = externalInit{} 196 | if !readOpaqueVec(s, &ei.kemOutput) { 197 | return io.ErrUnexpectedEOF 198 | } 199 | return nil 200 | } 201 | 202 | func (ei *externalInit) marshal(b *cryptobyte.Builder) { 203 | writeOpaqueVec(b, ei.kemOutput) 204 | } 205 | 206 | type groupContextExtensions struct { 207 | extensions []extension 208 | } 209 | 210 | func (exts *groupContextExtensions) unmarshal(s *cryptobyte.String) error { 211 | *exts = groupContextExtensions{} 212 | 213 | l, err := unmarshalExtensionVec(s) 214 | if err != nil { 215 | return err 216 | } 217 | exts.extensions = l 218 | 219 | return nil 220 | } 221 | 222 | func (exts *groupContextExtensions) marshal(b *cryptobyte.Builder) { 223 | marshalExtensionVec(b, exts.extensions) 224 | } 225 | 226 | type proposalOrRefType uint8 227 | 228 | const ( 229 | proposalOrRefTypeProposal proposalOrRefType = 1 230 | proposalOrRefTypeReference proposalOrRefType = 2 231 | ) 232 | 233 | func (t *proposalOrRefType) unmarshal(s *cryptobyte.String) error { 234 | if !s.ReadUint8((*uint8)(t)) { 235 | return io.ErrUnexpectedEOF 236 | } 237 | switch *t { 238 | case proposalOrRefTypeProposal, proposalOrRefTypeReference: 239 | return nil 240 | default: 241 | return fmt.Errorf("mls: invalid proposal or ref type %d", *t) 242 | } 243 | } 244 | 245 | func (t proposalOrRefType) marshal(b *cryptobyte.Builder) { 246 | b.AddUint8(uint8(t)) 247 | } 248 | 249 | type proposalRef []byte 250 | 251 | func (ref proposalRef) Equal(other proposalRef) bool { 252 | return bytes.Equal([]byte(ref), []byte(other)) 253 | } 254 | 255 | type proposalOrRef struct { 256 | typ proposalOrRefType 257 | proposal *proposal // for proposalOrRefTypeProposal 258 | reference proposalRef // for proposalOrRefTypeReference 259 | } 260 | 261 | func (propOrRef *proposalOrRef) unmarshal(s *cryptobyte.String) error { 262 | *propOrRef = proposalOrRef{} 263 | 264 | if err := propOrRef.typ.unmarshal(s); err != nil { 265 | return err 266 | } 267 | 268 | switch propOrRef.typ { 269 | case proposalOrRefTypeProposal: 270 | propOrRef.proposal = new(proposal) 271 | return propOrRef.proposal.unmarshal(s) 272 | case proposalOrRefTypeReference: 273 | if !readOpaqueVec(s, (*[]byte)(&propOrRef.reference)) { 274 | return io.ErrUnexpectedEOF 275 | } 276 | return nil 277 | default: 278 | panic("unreachable") 279 | } 280 | } 281 | 282 | func (propOrRef *proposalOrRef) marshal(b *cryptobyte.Builder) { 283 | propOrRef.typ.marshal(b) 284 | switch propOrRef.typ { 285 | case proposalOrRefTypeProposal: 286 | propOrRef.proposal.marshal(b) 287 | case proposalOrRefTypeReference: 288 | writeOpaqueVec(b, []byte(propOrRef.reference)) 289 | default: 290 | panic("unreachable") 291 | } 292 | } 293 | 294 | type commit struct { 295 | proposals []proposalOrRef 296 | path *updatePath // optional 297 | } 298 | 299 | func (c *commit) unmarshal(s *cryptobyte.String) error { 300 | *c = commit{} 301 | 302 | err := readVector(s, func(s *cryptobyte.String) error { 303 | var propOrRef proposalOrRef 304 | if err := propOrRef.unmarshal(s); err != nil { 305 | return err 306 | } 307 | c.proposals = append(c.proposals, propOrRef) 308 | return nil 309 | }) 310 | if err != nil { 311 | return err 312 | } 313 | 314 | var hasPath bool 315 | if !readOptional(s, &hasPath) { 316 | return io.ErrUnexpectedEOF 317 | } else if hasPath { 318 | c.path = new(updatePath) 319 | if err := c.path.unmarshal(s); err != nil { 320 | return err 321 | } 322 | } 323 | 324 | return nil 325 | } 326 | 327 | func (c *commit) marshal(b *cryptobyte.Builder) { 328 | writeVector(b, len(c.proposals), func(b *cryptobyte.Builder, i int) { 329 | c.proposals[i].marshal(b) 330 | }) 331 | writeOptional(b, c.path != nil) 332 | if c.path != nil { 333 | c.path.marshal(b) 334 | } 335 | } 336 | 337 | // verifyProposalList ensures that a list of proposals passes the checks for a 338 | // regular commit described in section 12.2. 339 | // 340 | // It does not perform all checks: 341 | // 342 | // - It does not check the validity of individual proposals (section 12.1). 343 | // - It does not check whether members in add proposals are already part of 344 | // the group. 345 | // - It does not check whether non-default proposal types are supported by 346 | // all members of the group who will process the commit. 347 | // - It does not check whether the ratchet tree is valid after processing the 348 | // commit. 349 | func verifyProposalList(proposals []proposal, senders []leafIndex, committer leafIndex) error { 350 | if len(proposals) != len(senders) { 351 | panic("unreachable") 352 | } 353 | 354 | add := make(map[string]struct{}) 355 | updateOrRemove := make(map[leafIndex]struct{}) 356 | psk := make(map[string]struct{}) 357 | groupContextExtensions := false 358 | for i, prop := range proposals { 359 | sender := senders[i] 360 | 361 | switch prop.proposalType { 362 | case proposalTypeAdd: 363 | k := string(prop.add.keyPackage.leafNode.signatureKey) 364 | if _, dup := add[k]; dup { 365 | return fmt.Errorf("mls: multiple add proposals have the same signature key") 366 | } 367 | add[k] = struct{}{} 368 | case proposalTypeUpdate: 369 | if sender == committer { 370 | return fmt.Errorf("mls: update proposal generated by the committer") 371 | } 372 | if _, dup := updateOrRemove[sender]; dup { 373 | return fmt.Errorf("mls: multiple update and/or remove proposals apply to the same leaf") 374 | } 375 | updateOrRemove[sender] = struct{}{} 376 | case proposalTypeRemove: 377 | if prop.remove.removed == committer { 378 | return fmt.Errorf("mls: remove proposal removes the committer") 379 | } 380 | if _, dup := updateOrRemove[prop.remove.removed]; dup { 381 | return fmt.Errorf("mls: multiple update and/or remove proposals apply to the same leaf") 382 | } 383 | updateOrRemove[prop.remove.removed] = struct{}{} 384 | case proposalTypePSK: 385 | b, err := marshal(&prop.preSharedKey.psk) 386 | if err != nil { 387 | return err 388 | } 389 | k := string(b) 390 | if _, dup := psk[k]; dup { 391 | return fmt.Errorf("mls: multiple PSK proposals reference the same PSK ID") 392 | } 393 | psk[k] = struct{}{} 394 | case proposalTypeGroupContextExtensions: 395 | if groupContextExtensions { 396 | return fmt.Errorf("mls: multiple group context extensions proposals") 397 | } 398 | groupContextExtensions = true 399 | case proposalTypeReinit: 400 | if len(proposals) > 1 { 401 | return fmt.Errorf("mls: reinit proposal together with any other proposal") 402 | } 403 | case proposalTypeExternalInit: 404 | return fmt.Errorf("mls: external init proposal is not allowed") 405 | } 406 | } 407 | return nil 408 | } 409 | 410 | func proposalListNeedsPath(proposals []proposal) bool { 411 | if len(proposals) == 0 { 412 | return true 413 | } 414 | 415 | for _, prop := range proposals { 416 | switch prop.proposalType { 417 | case proposalTypeUpdate, proposalTypeRemove, proposalTypeExternalInit, proposalTypeGroupContextExtensions: 418 | return true 419 | } 420 | } 421 | 422 | return false 423 | } 424 | 425 | type groupInfo struct { 426 | groupContext groupContext 427 | extensions []extension 428 | confirmationTag []byte 429 | signer leafIndex 430 | signature []byte 431 | } 432 | 433 | func (info *groupInfo) unmarshal(s *cryptobyte.String) error { 434 | *info = groupInfo{} 435 | 436 | if err := info.groupContext.unmarshal(s); err != nil { 437 | return err 438 | } 439 | 440 | exts, err := unmarshalExtensionVec(s) 441 | if err != nil { 442 | return err 443 | } 444 | info.extensions = exts 445 | 446 | if !readOpaqueVec(s, &info.confirmationTag) || !s.ReadUint32((*uint32)(&info.signer)) || !readOpaqueVec(s, &info.signature) { 447 | return err 448 | } 449 | 450 | return nil 451 | } 452 | 453 | func (info *groupInfo) marshal(b *cryptobyte.Builder) { 454 | (*groupInfoTBS)(info).marshal(b) 455 | writeOpaqueVec(b, info.signature) 456 | } 457 | 458 | func (info *groupInfo) verifySignature(signerPub signaturePublicKey) bool { 459 | cs := info.groupContext.cipherSuite 460 | tbs, err := marshal((*groupInfoTBS)(info)) 461 | if err != nil { 462 | return false 463 | } 464 | return cs.verifyWithLabel([]byte(signerPub), []byte("GroupInfoTBS"), tbs, info.signature) 465 | } 466 | 467 | func (info *groupInfo) verifyConfirmationTag(joinerSecret, pskSecret []byte) bool { 468 | cs := info.groupContext.cipherSuite 469 | epochSecret, err := info.groupContext.extractEpochSecret(joinerSecret, pskSecret) 470 | if err != nil { 471 | return false 472 | } 473 | confirmationKey, err := cs.deriveSecret(epochSecret, secretLabelConfirm) 474 | if err != nil { 475 | return false 476 | } 477 | return cs.verifyMAC(confirmationKey, info.groupContext.confirmedTranscriptHash, info.confirmationTag) 478 | } 479 | 480 | type groupInfoTBS groupInfo 481 | 482 | func (info *groupInfoTBS) marshal(b *cryptobyte.Builder) { 483 | info.groupContext.marshal(b) 484 | marshalExtensionVec(b, info.extensions) 485 | writeOpaqueVec(b, info.confirmationTag) 486 | b.AddUint32(uint32(info.signer)) 487 | } 488 | 489 | type groupSecrets struct { 490 | joinerSecret []byte 491 | pathSecret []byte // optional 492 | psks []preSharedKeyID 493 | } 494 | 495 | func (sec *groupSecrets) unmarshal(s *cryptobyte.String) error { 496 | *sec = groupSecrets{} 497 | 498 | if !readOpaqueVec(s, &sec.joinerSecret) { 499 | return io.ErrUnexpectedEOF 500 | } 501 | 502 | var hasPathSecret bool 503 | if !readOptional(s, &hasPathSecret) { 504 | return io.ErrUnexpectedEOF 505 | } else if hasPathSecret && !readOpaqueVec(s, &sec.pathSecret) { 506 | return io.ErrUnexpectedEOF 507 | } 508 | 509 | return readVector(s, func(s *cryptobyte.String) error { 510 | var psk preSharedKeyID 511 | if err := psk.unmarshal(s); err != nil { 512 | return err 513 | } 514 | sec.psks = append(sec.psks, psk) 515 | return nil 516 | }) 517 | } 518 | 519 | func (sec *groupSecrets) marshal(b *cryptobyte.Builder) { 520 | writeOpaqueVec(b, sec.joinerSecret) 521 | 522 | writeOptional(b, sec.pathSecret != nil) 523 | if sec.pathSecret != nil { 524 | writeOpaqueVec(b, sec.pathSecret) 525 | } 526 | 527 | writeVector(b, len(sec.psks), func(b *cryptobyte.Builder, i int) { 528 | sec.psks[i].marshal(b) 529 | }) 530 | } 531 | 532 | // verifySingleReInitOrBranchPSK verifies that at most one key has type 533 | // resumption with usage reinit or branch. 534 | func (sec *groupSecrets) verifySingleReinitOrBranchPSK() bool { 535 | n := 0 536 | for _, pskID := range sec.psks { 537 | if pskID.pskType != pskTypeResumption { 538 | continue 539 | } 540 | switch pskID.usage { 541 | case resumptionPSKUsageReinit, resumptionPSKUsageBranch: 542 | n++ 543 | } 544 | } 545 | return n <= 1 546 | } 547 | 548 | type welcome struct { 549 | cipherSuite cipherSuite 550 | secrets []encryptedGroupSecrets 551 | encryptedGroupInfo []byte 552 | } 553 | 554 | func (w *welcome) unmarshal(s *cryptobyte.String) error { 555 | *w = welcome{} 556 | 557 | if !s.ReadUint16((*uint16)(&w.cipherSuite)) { 558 | return io.ErrUnexpectedEOF 559 | } 560 | 561 | err := readVector(s, func(s *cryptobyte.String) error { 562 | var sec encryptedGroupSecrets 563 | if err := sec.unmarshal(s); err != nil { 564 | return err 565 | } 566 | w.secrets = append(w.secrets, sec) 567 | return nil 568 | }) 569 | if err != nil { 570 | return err 571 | } 572 | 573 | if !readOpaqueVec(s, &w.encryptedGroupInfo) { 574 | return io.ErrUnexpectedEOF 575 | } 576 | 577 | return nil 578 | } 579 | 580 | func (w *welcome) marshal(b *cryptobyte.Builder) { 581 | b.AddUint16(uint16(w.cipherSuite)) 582 | writeVector(b, len(w.secrets), func(b *cryptobyte.Builder, i int) { 583 | w.secrets[i].marshal(b) 584 | }) 585 | writeOpaqueVec(b, w.encryptedGroupInfo) 586 | } 587 | 588 | func (w *welcome) findSecret(ref keyPackageRef) *encryptedGroupSecrets { 589 | for i, sec := range w.secrets { 590 | if sec.newMember.Equal(ref) { 591 | return &w.secrets[i] 592 | } 593 | } 594 | return nil 595 | } 596 | 597 | func (w *welcome) decryptGroupSecrets(ref keyPackageRef, initKeyPriv []byte) (*groupSecrets, error) { 598 | cs := w.cipherSuite 599 | 600 | sec := w.findSecret(ref) 601 | if sec == nil { 602 | return nil, fmt.Errorf("mls: encrypted group secrets not found for provided key package ref") 603 | } 604 | 605 | rawGroupSecrets, err := cs.decryptWithLabel(initKeyPriv, []byte("Welcome"), w.encryptedGroupInfo, sec.encryptedGroupSecrets.kemOutput, sec.encryptedGroupSecrets.ciphertext) 606 | if err != nil { 607 | return nil, err 608 | } 609 | var groupSecrets groupSecrets 610 | if err := unmarshal(rawGroupSecrets, &groupSecrets); err != nil { 611 | return nil, err 612 | } 613 | 614 | return &groupSecrets, err 615 | } 616 | 617 | func (w *welcome) decryptGroupInfo(joinerSecret, pskSecret []byte) (*groupInfo, error) { 618 | cs := w.cipherSuite 619 | _, _, aead := cs.hpke().Params() 620 | 621 | welcomeSecret, err := extractWelcomeSecret(cs, joinerSecret, pskSecret) 622 | if err != nil { 623 | return nil, err 624 | } 625 | 626 | welcomeNonce, err := cs.expandWithLabel(welcomeSecret, []byte("nonce"), nil, uint16(aead.NonceSize())) 627 | if err != nil { 628 | return nil, err 629 | } 630 | welcomeKey, err := cs.expandWithLabel(welcomeSecret, []byte("key"), nil, uint16(aead.KeySize())) 631 | if err != nil { 632 | return nil, err 633 | } 634 | 635 | welcomeCipher, err := aead.New(welcomeKey) 636 | if err != nil { 637 | return nil, err 638 | } 639 | rawGroupInfo, err := welcomeCipher.Open(nil, welcomeNonce, w.encryptedGroupInfo, nil) 640 | if err != nil { 641 | return nil, err 642 | } 643 | 644 | var groupInfo groupInfo 645 | if err := unmarshal(rawGroupInfo, &groupInfo); err != nil { 646 | return nil, err 647 | } 648 | 649 | return &groupInfo, nil 650 | } 651 | 652 | type encryptedGroupSecrets struct { 653 | newMember keyPackageRef 654 | encryptedGroupSecrets hpkeCiphertext 655 | } 656 | 657 | func (sec *encryptedGroupSecrets) unmarshal(s *cryptobyte.String) error { 658 | *sec = encryptedGroupSecrets{} 659 | if !readOpaqueVec(s, (*[]byte)(&sec.newMember)) { 660 | return io.ErrUnexpectedEOF 661 | } 662 | if err := sec.encryptedGroupSecrets.unmarshal(s); err != nil { 663 | return err 664 | } 665 | return nil 666 | } 667 | 668 | func (sec *encryptedGroupSecrets) marshal(b *cryptobyte.Builder) { 669 | writeOpaqueVec(b, []byte(sec.newMember)) 670 | sec.encryptedGroupSecrets.marshal(b) 671 | } 672 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | type welcomeTest struct { 10 | CipherSuite cipherSuite `json:"cipher_suite"` 11 | 12 | InitPriv testBytes `json:"init_priv"` 13 | SignerPub testBytes `json:"signer_pub"` 14 | 15 | KeyPackage testBytes `json:"key_package"` 16 | Welcome testBytes `json:"welcome"` 17 | } 18 | 19 | func testWelcome(t *testing.T, tc *welcomeTest) { 20 | var welcomeMsg mlsMessage 21 | if err := welcomeMsg.unmarshal(tc.Welcome.ByteString()); err != nil { 22 | t.Fatalf("unmarshal(welcome) = %v", err) 23 | } else if welcomeMsg.wireFormat != wireFormatMLSWelcome { 24 | t.Fatalf("wireFormat = %v, want %v", welcomeMsg.wireFormat, wireFormatMLSWelcome) 25 | } 26 | welcome := welcomeMsg.welcome 27 | 28 | var keyPackageMsg mlsMessage 29 | if err := keyPackageMsg.unmarshal(tc.KeyPackage.ByteString()); err != nil { 30 | t.Fatalf("unmarshal(keyPackage) = %v", err) 31 | } else if keyPackageMsg.wireFormat != wireFormatMLSKeyPackage { 32 | t.Fatalf("wireFormat = %v, want %v", keyPackageMsg.wireFormat, wireFormatMLSKeyPackage) 33 | } 34 | keyPackage := keyPackageMsg.keyPackage 35 | 36 | keyPackageRef, err := keyPackage.generateRef() 37 | if err != nil { 38 | t.Fatalf("keyPackage.generateRef() = %v", err) 39 | } 40 | 41 | groupSecrets, err := welcome.decryptGroupSecrets(keyPackageRef, []byte(tc.InitPriv)) 42 | if err != nil { 43 | t.Fatalf("welcome.decryptGroupSecrets() = %v", err) 44 | } 45 | 46 | groupInfo, err := welcome.decryptGroupInfo(groupSecrets.joinerSecret, nil) 47 | if err != nil { 48 | t.Fatalf("welcome.decryptGroupInfo() = %v", err) 49 | } 50 | if !groupInfo.verifySignature(signaturePublicKey(tc.SignerPub)) { 51 | t.Errorf("groupInfo.verifySignature() failed") 52 | } 53 | if !groupInfo.verifyConfirmationTag(groupSecrets.joinerSecret, nil) { 54 | t.Errorf("groupInfo.verifyConfirmationTag() failed") 55 | } 56 | } 57 | 58 | func TestWelcome(t *testing.T) { 59 | var tests []welcomeTest 60 | loadTestVector(t, "testdata/welcome.json", &tests) 61 | 62 | for i, tc := range tests { 63 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 64 | testWelcome(t, &tc) 65 | }) 66 | } 67 | } 68 | 69 | type messageProtectionTest struct { 70 | CipherSuite cipherSuite `json:"cipher_suite"` 71 | 72 | GroupID testBytes `json:"group_id"` 73 | Epoch uint64 `json:"epoch"` 74 | TreeHash testBytes `json:"tree_hash"` 75 | ConfirmedTranscriptHash testBytes `json:"confirmed_transcript_hash"` 76 | 77 | SignaturePriv testBytes `json:"signature_priv"` 78 | SignaturePub testBytes `json:"signature_pub"` 79 | 80 | EncryptionSecret testBytes `json:"encryption_secret"` 81 | SenderDataSecret testBytes `json:"sender_data_secret"` 82 | MembershipKey testBytes `json:"membership_key"` 83 | 84 | Proposal testBytes `json:"proposal"` 85 | ProposalPub testBytes `json:"proposal_pub"` 86 | ProposalPriv testBytes `json:"proposal_priv"` 87 | 88 | Commit testBytes `json:"commit"` 89 | CommitPub testBytes `json:"commit_pub"` 90 | CommitPriv testBytes `json:"commit_priv"` 91 | 92 | Application testBytes `json:"application"` 93 | ApplicationPriv testBytes `json:"application_priv"` 94 | } 95 | 96 | func testMessageProtectionPub(t *testing.T, tc *messageProtectionTest, ctx *groupContext, wantRaw, rawPub []byte) { 97 | var msg mlsMessage 98 | if err := unmarshal(rawPub, &msg); err != nil { 99 | t.Fatalf("unmarshal() = %v", err) 100 | } else if msg.wireFormat != wireFormatMLSPublicMessage { 101 | t.Fatalf("unmarshal(): wireFormat = %v, want %v", msg.wireFormat, wireFormatMLSPublicMessage) 102 | } 103 | pubMsg := msg.publicMessage 104 | 105 | verifyPublicMessage(t, tc, ctx, pubMsg, wantRaw) 106 | 107 | pubMsg, err := signPublicMessage(tc.CipherSuite, []byte(tc.SignaturePriv), &pubMsg.content, ctx) 108 | if err != nil { 109 | t.Errorf("signPublicMessage() = %v", err) 110 | } 111 | if err := pubMsg.signMembershipTag(tc.CipherSuite, []byte(tc.MembershipKey), ctx); err != nil { 112 | t.Errorf("signMembershipTag() = %v", err) 113 | } 114 | verifyPublicMessage(t, tc, ctx, pubMsg, wantRaw) 115 | } 116 | 117 | func verifyPublicMessage(t *testing.T, tc *messageProtectionTest, ctx *groupContext, pubMsg *publicMessage, wantRaw []byte) { 118 | authContent := pubMsg.authenticatedContent() 119 | if !authContent.verifySignature(tc.CipherSuite, []byte(tc.SignaturePub), ctx) { 120 | t.Errorf("verifySignature() failed") 121 | } 122 | if !pubMsg.verifyMembershipTag(tc.CipherSuite, []byte(tc.MembershipKey), ctx) { 123 | t.Errorf("verifyMembershipTag() failed") 124 | } 125 | 126 | var ( 127 | raw []byte 128 | err error 129 | ) 130 | switch pubMsg.content.contentType { 131 | case contentTypeApplication: 132 | raw = pubMsg.content.applicationData 133 | case contentTypeProposal: 134 | raw, err = marshal(pubMsg.content.proposal) 135 | case contentTypeCommit: 136 | raw, err = marshal(pubMsg.content.commit) 137 | default: 138 | t.Errorf("unexpected content type %v", pubMsg.content.contentType) 139 | } 140 | if err != nil { 141 | t.Errorf("marshal() = %v", err) 142 | } else if !bytes.Equal(raw, wantRaw) { 143 | t.Errorf("marshal() = %v, want %v", raw, wantRaw) 144 | } 145 | } 146 | 147 | func testMessageProtectionPriv(t *testing.T, tc *messageProtectionTest, ctx *groupContext, wantRaw, rawPriv []byte) { 148 | var msg mlsMessage 149 | if err := unmarshal(rawPriv, &msg); err != nil { 150 | t.Fatalf("unmarshal() = %v", err) 151 | } else if msg.wireFormat != wireFormatMLSPrivateMessage { 152 | t.Fatalf("unmarshal(): wireFormat = %v, want %v", msg.wireFormat, wireFormatMLSPrivateMessage) 153 | } 154 | privMsg := msg.privateMessage 155 | 156 | tree, err := deriveSecretTree(tc.CipherSuite, numLeaves(2), []byte(tc.EncryptionSecret)) 157 | if err != nil { 158 | t.Fatalf("deriveSecretTree() = %v", err) 159 | } 160 | 161 | label := ratchetLabelFromContentType(privMsg.contentType) 162 | li := leafIndex(1) 163 | secret, err := tree.deriveRatchetRoot(tc.CipherSuite, li.nodeIndex(), label) 164 | if err != nil { 165 | t.Fatalf("deriveRatchetRoot() = %v", err) 166 | } 167 | 168 | content := decryptPrivateMessage(t, tc, ctx, secret, privMsg, wantRaw) 169 | 170 | senderData, err := newSenderData(li, 0) // TODO: set generation > 0 171 | if err != nil { 172 | t.Fatalf("newSenderData() = %v", err) 173 | } 174 | framedContent := framedContent{ 175 | groupID: GroupID(tc.GroupID), 176 | epoch: tc.Epoch, 177 | sender: sender{ 178 | senderType: senderTypeMember, 179 | leafIndex: li, 180 | }, 181 | contentType: privMsg.contentType, 182 | applicationData: content.applicationData, 183 | proposal: content.proposal, 184 | commit: content.commit, 185 | } 186 | privMsg, err = encryptPrivateMessage(tc.CipherSuite, []byte(tc.SignaturePriv), secret, []byte(tc.SenderDataSecret), &framedContent, senderData, ctx) 187 | if err != nil { 188 | t.Fatalf("encryptPrivateMessage() = %v", err) 189 | } 190 | decryptPrivateMessage(t, tc, ctx, secret, privMsg, wantRaw) 191 | } 192 | 193 | func decryptPrivateMessage(t *testing.T, tc *messageProtectionTest, ctx *groupContext, secret ratchetSecret, privMsg *privateMessage, wantRaw []byte) *privateMessageContent { 194 | senderData, err := privMsg.decryptSenderData(tc.CipherSuite, []byte(tc.SenderDataSecret)) 195 | if err != nil { 196 | t.Fatalf("decryptSenderData() = %v", err) 197 | } 198 | 199 | for secret.generation != senderData.generation { 200 | secret, err = secret.deriveNext(tc.CipherSuite) 201 | if err != nil { 202 | t.Fatalf("deriveNext() = %v", err) 203 | } 204 | } 205 | 206 | content, err := privMsg.decryptContent(tc.CipherSuite, secret, senderData.reuseGuard) 207 | if err != nil { 208 | t.Fatalf("decryptContent() = %v", err) 209 | } 210 | 211 | authContent := privMsg.authenticatedContent(senderData, content) 212 | if !authContent.verifySignature(tc.CipherSuite, []byte(tc.SignaturePub), ctx) { 213 | t.Errorf("verifySignature() failed") 214 | } 215 | 216 | var raw []byte 217 | switch privMsg.contentType { 218 | case contentTypeApplication: 219 | raw = content.applicationData 220 | case contentTypeProposal: 221 | raw, err = marshal(content.proposal) 222 | case contentTypeCommit: 223 | raw, err = marshal(content.commit) 224 | default: 225 | t.Errorf("unexpected content type %v", privMsg.contentType) 226 | } 227 | if err != nil { 228 | t.Errorf("marshal() = %v", err) 229 | } else if !bytes.Equal(raw, wantRaw) { 230 | t.Errorf("marshal() = %v, want %v", raw, wantRaw) 231 | } 232 | 233 | return content 234 | } 235 | 236 | func testMessageProtection(t *testing.T, tc *messageProtectionTest) { 237 | ctx := groupContext{ 238 | version: protocolVersionMLS10, 239 | cipherSuite: tc.CipherSuite, 240 | groupID: GroupID(tc.GroupID), 241 | epoch: tc.Epoch, 242 | treeHash: []byte(tc.TreeHash), 243 | confirmedTranscriptHash: []byte(tc.ConfirmedTranscriptHash), 244 | } 245 | 246 | wireFormats := []struct { 247 | name string 248 | raw, pub, priv testBytes 249 | }{ 250 | {"proposal", tc.Proposal, tc.ProposalPub, tc.ProposalPriv}, 251 | {"commit", tc.Commit, tc.CommitPub, tc.CommitPriv}, 252 | {"application", tc.Application, nil, tc.ApplicationPriv}, 253 | } 254 | for _, wireFormat := range wireFormats { 255 | t.Run(wireFormat.name, func(t *testing.T) { 256 | raw := []byte(wireFormat.raw) 257 | pub := []byte(wireFormat.pub) 258 | priv := []byte(wireFormat.priv) 259 | if wireFormat.pub != nil { 260 | t.Run("pub", func(t *testing.T) { 261 | testMessageProtectionPub(t, tc, &ctx, raw, pub) 262 | }) 263 | } 264 | t.Run("priv", func(t *testing.T) { 265 | testMessageProtectionPriv(t, tc, &ctx, raw, priv) 266 | }) 267 | }) 268 | } 269 | } 270 | 271 | func TestMessageProtection(t *testing.T) { 272 | var tests []messageProtectionTest 273 | loadTestVector(t, "testdata/message-protection.json", &tests) 274 | 275 | for i, tc := range tests { 276 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 277 | testMessageProtection(t, &tc) 278 | }) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /key_package.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "golang.org/x/crypto/cryptobyte" 9 | ) 10 | 11 | type keyPackage struct { 12 | version protocolVersion 13 | cipherSuite cipherSuite 14 | initKey hpkePublicKey 15 | leafNode leafNode 16 | extensions []extension 17 | signature []byte 18 | } 19 | 20 | func (pkg *keyPackage) unmarshal(s *cryptobyte.String) error { 21 | *pkg = keyPackage{} 22 | 23 | ok := s.ReadUint16((*uint16)(&pkg.version)) && 24 | s.ReadUint16((*uint16)(&pkg.cipherSuite)) && 25 | readOpaqueVec(s, (*[]byte)(&pkg.initKey)) 26 | if !ok { 27 | return io.ErrUnexpectedEOF 28 | } 29 | 30 | if err := pkg.leafNode.unmarshal(s); err != nil { 31 | return err 32 | } 33 | 34 | exts, err := unmarshalExtensionVec(s) 35 | if err != nil { 36 | return err 37 | } 38 | pkg.extensions = exts 39 | 40 | if !readOpaqueVec(s, &pkg.signature) { 41 | return err 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (pkg *keyPackage) marshalTBS(b *cryptobyte.Builder) { 48 | b.AddUint16(uint16(pkg.version)) 49 | b.AddUint16(uint16(pkg.cipherSuite)) 50 | writeOpaqueVec(b, []byte(pkg.initKey)) 51 | pkg.leafNode.marshal(b) 52 | marshalExtensionVec(b, pkg.extensions) 53 | } 54 | 55 | func (pkg *keyPackage) marshal(b *cryptobyte.Builder) { 56 | pkg.marshalTBS(b) 57 | writeOpaqueVec(b, pkg.signature) 58 | } 59 | 60 | func (pkg *keyPackage) verifySignature() bool { 61 | var b cryptobyte.Builder 62 | pkg.marshalTBS(&b) 63 | rawTBS, err := b.Bytes() 64 | if err != nil { 65 | return false 66 | } 67 | 68 | return pkg.cipherSuite.verifyWithLabel(pkg.leafNode.signatureKey, []byte("KeyPackageTBS"), rawTBS, pkg.signature) 69 | } 70 | 71 | // verify performs KeyPackage verification as described in RFC 9420 section 10.1. 72 | func (pkg *keyPackage) verify(ctx *groupContext) error { 73 | if pkg.version != ctx.version { 74 | return fmt.Errorf("mls: key package version doesn't match group context") 75 | } 76 | if pkg.cipherSuite != ctx.cipherSuite { 77 | return fmt.Errorf("mls: cipher suite doesn't match group context") 78 | } 79 | if pkg.leafNode.leafNodeSource != leafNodeSourceKeyPackage { 80 | return fmt.Errorf("mls: key package contains a leaf node with an invalid source") 81 | } 82 | if !pkg.verifySignature() { 83 | return fmt.Errorf("mls: invalid key package signature") 84 | } 85 | if bytes.Equal(pkg.leafNode.encryptionKey, pkg.initKey) { 86 | return fmt.Errorf("mls: key package encryption key and init key are identical") 87 | } 88 | return nil 89 | } 90 | 91 | func (pkg *keyPackage) generateRef() (keyPackageRef, error) { 92 | var b cryptobyte.Builder 93 | pkg.marshal(&b) 94 | raw, err := b.Bytes() 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | hash, err := pkg.cipherSuite.refHash([]byte("MLS 1.0 KeyPackage Reference"), raw) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | return keyPackageRef(hash), nil 105 | } 106 | 107 | type keyPackageRef []byte 108 | 109 | func (ref keyPackageRef) Equal(other keyPackageRef) bool { 110 | return bytes.Equal([]byte(ref), []byte(other)) 111 | } 112 | -------------------------------------------------------------------------------- /key_schedule.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "golang.org/x/crypto/cryptobyte" 8 | ) 9 | 10 | type groupContext struct { 11 | version protocolVersion 12 | cipherSuite cipherSuite 13 | groupID GroupID 14 | epoch uint64 15 | treeHash []byte 16 | confirmedTranscriptHash []byte 17 | extensions []extension 18 | } 19 | 20 | func (ctx *groupContext) unmarshal(s *cryptobyte.String) error { 21 | *ctx = groupContext{} 22 | 23 | ok := s.ReadUint16((*uint16)(&ctx.version)) && 24 | s.ReadUint16((*uint16)(&ctx.cipherSuite)) && 25 | readOpaqueVec(s, (*[]byte)(&ctx.groupID)) && 26 | s.ReadUint64(&ctx.epoch) && 27 | readOpaqueVec(s, &ctx.treeHash) && 28 | readOpaqueVec(s, &ctx.confirmedTranscriptHash) 29 | if !ok { 30 | return io.ErrUnexpectedEOF 31 | } 32 | 33 | if ctx.version != protocolVersionMLS10 { 34 | return fmt.Errorf("mls: invalid protocol version %d", ctx.version) 35 | } 36 | 37 | exts, err := unmarshalExtensionVec(s) 38 | if err != nil { 39 | return err 40 | } 41 | ctx.extensions = exts 42 | 43 | return nil 44 | } 45 | 46 | func (ctx *groupContext) marshal(b *cryptobyte.Builder) { 47 | b.AddUint16(uint16(ctx.version)) 48 | b.AddUint16(uint16(ctx.cipherSuite)) 49 | writeOpaqueVec(b, []byte(ctx.groupID)) 50 | b.AddUint64(ctx.epoch) 51 | writeOpaqueVec(b, ctx.treeHash) 52 | writeOpaqueVec(b, ctx.confirmedTranscriptHash) 53 | marshalExtensionVec(b, ctx.extensions) 54 | } 55 | 56 | func (ctx *groupContext) extractJoinerSecret(prevInitSecret, commitSecret []byte) ([]byte, error) { 57 | cs := ctx.cipherSuite 58 | _, kdf, _ := cs.hpke().Params() 59 | 60 | extracted := kdf.Extract(commitSecret, prevInitSecret) 61 | 62 | rawGroupContext, err := marshal(ctx) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return cs.expandWithLabel(extracted, []byte("joiner"), rawGroupContext, uint16(kdf.ExtractSize())) 67 | } 68 | 69 | func (ctx *groupContext) extractEpochSecret(joinerSecret, pskSecret []byte) ([]byte, error) { 70 | cs := ctx.cipherSuite 71 | _, kdf, _ := cs.hpke().Params() 72 | 73 | // TODO de-duplicate with extractWelcomeSecret 74 | if pskSecret == nil { 75 | pskSecret = make([]byte, kdf.ExtractSize()) 76 | } 77 | extracted := kdf.Extract(pskSecret, joinerSecret) 78 | 79 | rawGroupContext, err := marshal(ctx) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return cs.expandWithLabel(extracted, []byte("epoch"), rawGroupContext, uint16(kdf.ExtractSize())) 84 | } 85 | 86 | func extractWelcomeSecret(cs cipherSuite, joinerSecret, pskSecret []byte) ([]byte, error) { 87 | _, kdf, _ := cs.hpke().Params() 88 | 89 | if pskSecret == nil { 90 | pskSecret = make([]byte, kdf.ExtractSize()) 91 | } 92 | extracted := kdf.Extract(pskSecret, joinerSecret) 93 | 94 | return cs.deriveSecret(extracted, []byte("welcome")) 95 | } 96 | 97 | func deriveExporter(cs cipherSuite, exporterSecret, label, context []byte, length uint16) ([]byte, error) { 98 | derived, err := cs.deriveSecret(exporterSecret, label) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | h := cs.hash().New() 104 | h.Write(context) 105 | 106 | return cs.expandWithLabel(derived, []byte("exported"), h.Sum(nil), length) 107 | } 108 | 109 | var ( 110 | secretLabelInit = []byte("init") 111 | secretLabelSenderData = []byte("sender data") 112 | secretLabelEncryption = []byte("encryption") 113 | secretLabelExporter = []byte("exporter") 114 | secretLabelExternal = []byte("external") 115 | secretLabelConfirm = []byte("confirm") 116 | secretLabelMembership = []byte("membership") 117 | secretLabelResumption = []byte("resumption") 118 | secretLabelAuthentication = []byte("authentication") 119 | ) 120 | 121 | type confirmedTranscriptHashInput struct { 122 | wireFormat wireFormat 123 | content framedContent 124 | signature []byte 125 | } 126 | 127 | func (input *confirmedTranscriptHashInput) marshal(b *cryptobyte.Builder) { 128 | if input.content.contentType != contentTypeCommit { 129 | b.SetError(fmt.Errorf("mls: confirmedTranscriptHashInput can only contain contentTypeCommit")) 130 | return 131 | } 132 | input.wireFormat.marshal(b) 133 | input.content.marshal(b) 134 | writeOpaqueVec(b, input.signature) 135 | } 136 | 137 | func (input *confirmedTranscriptHashInput) hash(cs cipherSuite, interimTranscriptHashBefore []byte) ([]byte, error) { 138 | rawInput, err := marshal(input) 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | h := cs.hash().New() 144 | h.Write(interimTranscriptHashBefore) 145 | h.Write(rawInput) 146 | return h.Sum(nil), nil 147 | } 148 | 149 | func nextInterimTranscriptHash(cs cipherSuite, confirmedTranscriptHash, confirmationTag []byte) ([]byte, error) { 150 | var b cryptobyte.Builder 151 | writeOpaqueVec(&b, confirmationTag) 152 | rawInput, err := b.Bytes() 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | h := cs.hash().New() 158 | h.Write(confirmedTranscriptHash) 159 | h.Write(rawInput) 160 | return h.Sum(nil), nil 161 | } 162 | 163 | type pskType uint8 164 | 165 | const ( 166 | pskTypeExternal pskType = 1 167 | pskTypeResumption pskType = 2 168 | ) 169 | 170 | func (t *pskType) unmarshal(s *cryptobyte.String) error { 171 | if !s.ReadUint8((*uint8)(t)) { 172 | return io.ErrUnexpectedEOF 173 | } 174 | switch *t { 175 | case pskTypeExternal, pskTypeResumption: 176 | return nil 177 | default: 178 | return fmt.Errorf("mls: invalid PSK type %d", *t) 179 | } 180 | } 181 | 182 | func (t pskType) marshal(b *cryptobyte.Builder) { 183 | b.AddUint8(uint8(t)) 184 | } 185 | 186 | type resumptionPSKUsage uint8 187 | 188 | const ( 189 | resumptionPSKUsageApplication resumptionPSKUsage = 1 190 | resumptionPSKUsageReinit resumptionPSKUsage = 2 191 | resumptionPSKUsageBranch resumptionPSKUsage = 3 192 | ) 193 | 194 | func (usage *resumptionPSKUsage) unmarshal(s *cryptobyte.String) error { 195 | if !s.ReadUint8((*uint8)(usage)) { 196 | return io.ErrUnexpectedEOF 197 | } 198 | switch *usage { 199 | case resumptionPSKUsageApplication, resumptionPSKUsageReinit, resumptionPSKUsageBranch: 200 | return nil 201 | default: 202 | return fmt.Errorf("mls: invalid resumption PSK usage %d", *usage) 203 | } 204 | } 205 | 206 | func (usage resumptionPSKUsage) marshal(b *cryptobyte.Builder) { 207 | b.AddUint8(uint8(usage)) 208 | } 209 | 210 | type preSharedKeyID struct { 211 | pskType pskType 212 | 213 | // for pskTypeExternal 214 | pskID []byte 215 | 216 | // for pskTypeResumption 217 | usage resumptionPSKUsage 218 | pskGroupID GroupID 219 | pskEpoch uint64 220 | 221 | pskNonce []byte 222 | } 223 | 224 | func (id *preSharedKeyID) unmarshal(s *cryptobyte.String) error { 225 | *id = preSharedKeyID{} 226 | 227 | if err := id.pskType.unmarshal(s); err != nil { 228 | return err 229 | } 230 | 231 | switch id.pskType { 232 | case pskTypeExternal: 233 | if !readOpaqueVec(s, &id.pskID) { 234 | return io.ErrUnexpectedEOF 235 | } 236 | case pskTypeResumption: 237 | if err := id.usage.unmarshal(s); err != nil { 238 | return err 239 | } 240 | if !readOpaqueVec(s, (*[]byte)(&id.pskGroupID)) || !s.ReadUint64(&id.pskEpoch) { 241 | return io.ErrUnexpectedEOF 242 | } 243 | default: 244 | panic("unreachable") 245 | } 246 | 247 | if !readOpaqueVec(s, &id.pskNonce) { 248 | return io.ErrUnexpectedEOF 249 | } 250 | 251 | return nil 252 | } 253 | 254 | func (id *preSharedKeyID) marshal(b *cryptobyte.Builder) { 255 | id.pskType.marshal(b) 256 | switch id.pskType { 257 | case pskTypeExternal: 258 | writeOpaqueVec(b, id.pskID) 259 | case pskTypeResumption: 260 | id.usage.marshal(b) 261 | writeOpaqueVec(b, []byte(id.pskGroupID)) 262 | b.AddUint64(id.pskEpoch) 263 | default: 264 | panic("unreachable") 265 | } 266 | writeOpaqueVec(b, id.pskNonce) 267 | } 268 | 269 | func extractPSKSecret(cs cipherSuite, pskIDs []preSharedKeyID, psks [][]byte) ([]byte, error) { 270 | if len(pskIDs) != len(psks) { 271 | return nil, fmt.Errorf("mls: got %v PSK IDs and %v PSKs, want same number", len(pskIDs), len(psks)) 272 | } 273 | 274 | _, kdf, _ := cs.hpke().Params() 275 | zero := make([]byte, kdf.ExtractSize()) 276 | 277 | pskSecret := zero 278 | for i := range pskIDs { 279 | pskExtracted := kdf.Extract(psks[i], zero) 280 | 281 | pskLabel := pskLabel{ 282 | id: pskIDs[i], 283 | index: uint16(i), 284 | count: uint16(len(pskIDs)), 285 | } 286 | rawPSKLabel, err := marshal(&pskLabel) 287 | if err != nil { 288 | return nil, err 289 | } 290 | 291 | pskInput, err := cs.expandWithLabel(pskExtracted, []byte("derived psk"), rawPSKLabel, uint16(kdf.ExtractSize())) 292 | if err != nil { 293 | return nil, err 294 | } 295 | 296 | pskSecret = kdf.Extract(pskSecret, pskInput) 297 | } 298 | 299 | return pskSecret, nil 300 | } 301 | 302 | type pskLabel struct { 303 | id preSharedKeyID 304 | index uint16 305 | count uint16 306 | } 307 | 308 | func (label *pskLabel) marshal(b *cryptobyte.Builder) { 309 | label.id.marshal(b) 310 | b.AddUint16(label.index) 311 | b.AddUint16(label.count) 312 | } 313 | -------------------------------------------------------------------------------- /key_schedule_test.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | type pskSecretTest struct { 10 | CipherSuite cipherSuite `json:"cipher_suite"` 11 | 12 | PSKs []struct { 13 | PSKID testBytes `json:"psk_id"` 14 | PSK testBytes `json:"psk"` 15 | PSKNonce testBytes `json:"psk_nonce"` 16 | } `json:"psks"` 17 | 18 | PSKSecret testBytes `json:"psk_secret"` 19 | } 20 | 21 | func testPSKSecret(t *testing.T, tc *pskSecretTest) { 22 | var ( 23 | pskIDs []preSharedKeyID 24 | psks [][]byte 25 | ) 26 | for _, psk := range tc.PSKs { 27 | pskIDs = append(pskIDs, preSharedKeyID{ 28 | pskType: pskTypeExternal, 29 | pskID: []byte(psk.PSKID), 30 | pskNonce: []byte(psk.PSKNonce), 31 | }) 32 | psks = append(psks, []byte(psk.PSK)) 33 | } 34 | 35 | pskSecret, err := extractPSKSecret(tc.CipherSuite, pskIDs, psks) 36 | if err != nil { 37 | t.Fatalf("extractPSKSecret() = %v", err) 38 | } 39 | if !bytes.Equal(pskSecret, []byte(tc.PSKSecret)) { 40 | t.Errorf("extractPSKSecret() = %v, want %v", pskSecret, tc.PSKSecret) 41 | } 42 | } 43 | 44 | func TestPSKSecret(t *testing.T) { 45 | var tests []pskSecretTest 46 | loadTestVector(t, "testdata/psk_secret.json", &tests) 47 | 48 | for i, tc := range tests { 49 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 50 | testPSKSecret(t, &tc) 51 | }) 52 | } 53 | } 54 | 55 | type keyScheduleTest struct { 56 | CipherSuite cipherSuite `json:"cipher_suite"` 57 | 58 | GroupID testBytes `json:"group_id"` 59 | InitialInitSecret testBytes `json:"initial_init_secret"` 60 | 61 | Epochs []struct { 62 | TreeHash testBytes `json:"tree_hash"` 63 | CommitSecret testBytes `json:"commit_secret"` 64 | PSKSecret testBytes `json:"psk_secret"` 65 | ConfirmedTranscriptHash testBytes `json:"confirmed_transcript_hash"` 66 | 67 | GroupContext testBytes `json:"group_context"` 68 | 69 | JoinerSecret testBytes `json:"joiner_secret"` 70 | WelcomeSecret testBytes `json:"welcome_secret"` 71 | InitSecret testBytes `json:"init_secret"` 72 | 73 | SenderDataSecret testBytes `json:"sender_data_secret"` 74 | EncryptionSecret testBytes `json:"encryption_secret"` 75 | ExporterSecret testBytes `json:"exporter_secret"` 76 | EpochAuthenticator testBytes `json:"epoch_authenticator"` 77 | ExternalSecret testBytes `json:"external_secret"` 78 | ConfirmationKey testBytes `json:"confirmation_key"` 79 | MembershipKey testBytes `json:"membership_key"` 80 | ResumptionPSK testBytes `json:"resumption_psk"` 81 | 82 | ExternalPub testBytes `json:"external_pub"` 83 | Exporter struct { 84 | Label string `json:"label"` 85 | Context testBytes `json:"context"` 86 | Length uint16 `json:"length"` 87 | Secret testBytes `json:"secret"` 88 | } `json:"exporter"` 89 | } `json:"epochs"` 90 | } 91 | 92 | func testKeySchedule(t *testing.T, tc *keyScheduleTest) { 93 | initSecret := []byte(tc.InitialInitSecret) 94 | for i, epoch := range tc.Epochs { 95 | t.Logf("epoch %d", i) 96 | 97 | ctx := groupContext{ 98 | version: protocolVersionMLS10, 99 | cipherSuite: tc.CipherSuite, 100 | groupID: GroupID(tc.GroupID), 101 | epoch: uint64(i), 102 | treeHash: []byte(epoch.TreeHash), 103 | confirmedTranscriptHash: []byte(epoch.ConfirmedTranscriptHash), 104 | } 105 | rawCtx, err := marshal(&ctx) 106 | if err != nil { 107 | t.Fatalf("marshal(groupContext) = %v", err) 108 | } else if !bytes.Equal(rawCtx, []byte(epoch.GroupContext)) { 109 | t.Errorf("marshal(groupContext) = %v, want %v", rawCtx, epoch.GroupContext) 110 | } 111 | 112 | joinerSecret, err := ctx.extractJoinerSecret(initSecret, []byte(epoch.CommitSecret)) 113 | if err != nil { 114 | t.Errorf("extractJoinerSecret() = %v", err) 115 | } else if !bytes.Equal(joinerSecret, []byte(epoch.JoinerSecret)) { 116 | t.Errorf("extractJoinerSecret() = %v, want %v", joinerSecret, epoch.JoinerSecret) 117 | } 118 | 119 | welcomeSecret, err := extractWelcomeSecret(ctx.cipherSuite, joinerSecret, []byte(epoch.PSKSecret)) 120 | if err != nil { 121 | t.Errorf("extractWelcomeSecret() = %v", err) 122 | } else if !bytes.Equal(welcomeSecret, []byte(epoch.WelcomeSecret)) { 123 | t.Errorf("extractWelcomeSecret() = %v, want %v", welcomeSecret, epoch.WelcomeSecret) 124 | } 125 | 126 | epochSecret, err := ctx.extractEpochSecret(joinerSecret, []byte(epoch.PSKSecret)) 127 | if err != nil { 128 | t.Fatalf("extractEpochSecret() = %v", err) 129 | } 130 | 131 | initSecret, err = ctx.cipherSuite.deriveSecret(epochSecret, secretLabelInit) 132 | if err != nil { 133 | t.Errorf("deriveSecret(init) = %v", err) 134 | } else if !bytes.Equal(initSecret, []byte(epoch.InitSecret)) { 135 | t.Errorf("deriveSecret(init) = %v, want %v", initSecret, epoch.InitSecret) 136 | } 137 | 138 | secrets := []struct { 139 | label []byte 140 | want testBytes 141 | }{ 142 | {secretLabelSenderData, epoch.SenderDataSecret}, 143 | {secretLabelEncryption, epoch.EncryptionSecret}, 144 | {secretLabelExporter, epoch.ExporterSecret}, 145 | {secretLabelExternal, epoch.ExternalSecret}, 146 | {secretLabelConfirm, epoch.ConfirmationKey}, 147 | {secretLabelMembership, epoch.MembershipKey}, 148 | {secretLabelResumption, epoch.ResumptionPSK}, 149 | } 150 | for _, secret := range secrets { 151 | sec, err := ctx.cipherSuite.deriveSecret(epochSecret, secret.label) 152 | if err != nil { 153 | t.Errorf("deriveSecret(%v) = %v", string(secret.label), err) 154 | } else if !bytes.Equal(sec, []byte(secret.want)) { 155 | t.Errorf("deriveSecret(%v) = %v, want %v", string(secret.label), sec, secret.want) 156 | } 157 | } 158 | 159 | externalSecret := []byte(epoch.ExternalSecret) 160 | kem, kdf, _ := ctx.cipherSuite.hpke().Params() 161 | // TODO: drop the seed size check, see: 162 | // https://github.com/cloudflare/circl/issues/486 163 | if kem.Scheme().SeedSize() == kdf.ExtractSize() { 164 | pub, _ := kem.Scheme().DeriveKeyPair(externalSecret) 165 | if b, err := pub.MarshalBinary(); err != nil { 166 | t.Errorf("kem.PublicKey.MarshalBinary() = %v", err) 167 | } else if !bytes.Equal(b, epoch.ExternalPub) { 168 | t.Errorf("kem.PublicKey.MarshalBinary() = %v, want %v", b, epoch.ExternalPub) 169 | } 170 | } 171 | 172 | exporterSecret := []byte(epoch.ExporterSecret) 173 | b, err := deriveExporter(ctx.cipherSuite, exporterSecret, []byte(epoch.Exporter.Label), []byte(epoch.Exporter.Context), epoch.Exporter.Length) 174 | if err != nil { 175 | t.Errorf("deriveExporter() = %v", err) 176 | } else if !bytes.Equal(b, epoch.Exporter.Secret) { 177 | t.Errorf("deriveExporter() = %v, want %v", b, epoch.Exporter.Secret) 178 | } 179 | } 180 | } 181 | 182 | func TestKeySchedule(t *testing.T) { 183 | var tests []keyScheduleTest 184 | loadTestVector(t, "testdata/key-schedule.json", &tests) 185 | 186 | for i, tc := range tests { 187 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 188 | testKeySchedule(t, &tc) 189 | }) 190 | } 191 | } 192 | 193 | type transcriptHashesTest struct { 194 | CipherSuite cipherSuite `json:"cipher_suite"` 195 | 196 | ConfirmationKey testBytes `json:"confirmation_key"` 197 | AuthenticatedContent testBytes `json:"authenticated_content"` 198 | InterimTranscriptHashBefore testBytes `json:"interim_transcript_hash_before"` 199 | 200 | ConfirmedTranscriptHashAfter testBytes `json:"confirmed_transcript_hash_after"` 201 | InterimTranscriptHashAfter testBytes `json:"interim_transcript_hash_after"` 202 | } 203 | 204 | func testTranscriptHashes(t *testing.T, tc *transcriptHashesTest) { 205 | cs := tc.CipherSuite 206 | 207 | var authContent authenticatedContent 208 | if err := unmarshal([]byte(tc.AuthenticatedContent), &authContent); err != nil { 209 | t.Fatalf("unmarshal() = %v", err) 210 | } else if authContent.content.contentType != contentTypeCommit { 211 | t.Fatalf("contentType = %v, want %v", authContent.content.contentType, contentTypeCommit) 212 | } 213 | 214 | if !authContent.auth.verifyConfirmationTag(cs, []byte(tc.ConfirmationKey), []byte(tc.ConfirmedTranscriptHashAfter)) { 215 | t.Errorf("verifyConfirmationTag() failed") 216 | } 217 | 218 | confirmedTranscriptHashAfter, err := authContent.confirmedTranscriptHashInput().hash(cs, []byte(tc.InterimTranscriptHashBefore)) 219 | if err != nil { 220 | t.Fatalf("confirmedTranscriptHashInput.hash() = %v", err) 221 | } else if !bytes.Equal(confirmedTranscriptHashAfter, []byte(tc.ConfirmedTranscriptHashAfter)) { 222 | t.Errorf("confirmedTranscriptHashInput.hash() = %v, want %v", confirmedTranscriptHashAfter, tc.ConfirmedTranscriptHashAfter) 223 | } 224 | 225 | interimTranscriptHashAfter, err := nextInterimTranscriptHash(cs, confirmedTranscriptHashAfter, authContent.auth.confirmationTag) 226 | if err != nil { 227 | t.Fatalf("nextInterimTranscriptHash() = %v", err) 228 | } else if !bytes.Equal(interimTranscriptHashAfter, []byte(tc.InterimTranscriptHashAfter)) { 229 | t.Errorf("nextInterimTranscriptHash() = %v, want %v", interimTranscriptHashAfter, tc.InterimTranscriptHashAfter) 230 | } 231 | } 232 | 233 | func TestTranscriptHashes(t *testing.T) { 234 | var tests []transcriptHashesTest 235 | loadTestVector(t, "testdata/transcript-hashes.json", &tests) 236 | 237 | for i, tc := range tests { 238 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 239 | testTranscriptHashes(t, &tc) 240 | }) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /mls.go: -------------------------------------------------------------------------------- 1 | // Package mls implements the Messaging Layer Security protocol. 2 | // 3 | // MLS is specified in RFC 9420. 4 | package mls 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | 10 | "golang.org/x/crypto/cryptobyte" 11 | ) 12 | 13 | func readVarint(s *cryptobyte.String, out *uint32) bool { 14 | var b uint8 15 | if !s.ReadUint8(&b) { 16 | return false 17 | } 18 | 19 | prefix := b >> 6 20 | if prefix == 3 { 21 | return false // invalid variable length integer prefix 22 | } 23 | 24 | n := 1 << prefix 25 | v := uint32(b & 0x3F) 26 | for i := 0; i < n-1; i++ { 27 | if !s.ReadUint8(&b) { 28 | return false 29 | } 30 | v = (v << 8) + uint32(b) 31 | } 32 | 33 | if prefix >= 1 && v < uint32(1)<<(8*(n/2)-2) { 34 | return false // minimum encoding was not used 35 | } 36 | 37 | *out = v 38 | return true 39 | } 40 | 41 | func writeVarint(b *cryptobyte.Builder, n uint32) { 42 | switch { 43 | case n < 1<<6: 44 | b.AddUint8(uint8(n)) 45 | case n < 1<<14: 46 | b.AddUint16(0b01<<14 | uint16(n)) 47 | case n < 1<<30: 48 | b.AddUint32(0b10<<30 | n) 49 | default: 50 | b.SetError(fmt.Errorf("mls: varint exceeds 30 bits")) 51 | } 52 | } 53 | 54 | func readOpaqueVec(s *cryptobyte.String, out *[]byte) bool { 55 | var n uint32 56 | if !readVarint(s, &n) { 57 | return false 58 | } 59 | 60 | b := make([]byte, n) 61 | if !s.CopyBytes(b) { 62 | return false 63 | } 64 | 65 | *out = b 66 | return true 67 | } 68 | 69 | func writeOpaqueVec(b *cryptobyte.Builder, value []byte) { 70 | if len(value) >= 1<<32 { 71 | b.SetError(fmt.Errorf("mls: opaque size exceeds maximum value of uint32")) 72 | return 73 | } 74 | writeVarint(b, uint32(len(value))) 75 | b.AddBytes(value) 76 | } 77 | 78 | func readVector(s *cryptobyte.String, f func(s *cryptobyte.String) error) error { 79 | var n uint32 80 | if !readVarint(s, &n) { 81 | return io.ErrUnexpectedEOF 82 | } 83 | var vec []byte 84 | if !s.ReadBytes(&vec, int(n)) { 85 | return io.ErrUnexpectedEOF 86 | } 87 | ss := cryptobyte.String(vec) 88 | for !ss.Empty() { 89 | if err := f(&ss); err != nil { 90 | return err 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | func writeVector(b *cryptobyte.Builder, n int, f func(b *cryptobyte.Builder, i int)) { 97 | // We don't know the total size in advance, and the vector is prefixed with 98 | // a varint, so we can't avoid the temporary buffer here 99 | var child cryptobyte.Builder 100 | for i := 0; i < n; i++ { 101 | f(&child, i) 102 | } 103 | 104 | raw, err := child.Bytes() 105 | if err != nil { 106 | b.SetError(err) 107 | return 108 | } 109 | 110 | writeOpaqueVec(b, raw) 111 | } 112 | 113 | func readOptional(s *cryptobyte.String, present *bool) bool { 114 | var u8 uint8 115 | if !s.ReadUint8(&u8) { 116 | return false 117 | } 118 | switch u8 { 119 | case 0: 120 | *present = false 121 | case 1: 122 | *present = true 123 | default: 124 | return false 125 | } 126 | return true 127 | } 128 | 129 | func writeOptional(b *cryptobyte.Builder, present bool) { 130 | u8 := uint8(0) 131 | if present { 132 | u8 = 1 133 | } 134 | b.AddUint8(u8) 135 | } 136 | 137 | type unmarshaler interface { 138 | unmarshal(*cryptobyte.String) error 139 | } 140 | 141 | type marshaler interface { 142 | marshal(*cryptobyte.Builder) 143 | } 144 | 145 | func unmarshal(raw []byte, v unmarshaler) error { 146 | s := cryptobyte.String(raw) 147 | if err := v.unmarshal(&s); err != nil { 148 | return err 149 | } 150 | if !s.Empty() { 151 | return fmt.Errorf("mls: input for %T contains %v excess bytes", v, len(s)) 152 | } 153 | return nil 154 | } 155 | 156 | func marshal(v marshaler) ([]byte, error) { 157 | var b cryptobyte.Builder 158 | v.marshal(&b) 159 | return b.Bytes() 160 | } 161 | -------------------------------------------------------------------------------- /mls_test.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "encoding" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "testing" 10 | 11 | "golang.org/x/crypto/cryptobyte" 12 | ) 13 | 14 | type testBytes []byte 15 | 16 | var _ encoding.TextUnmarshaler = (*testBytes)(nil) 17 | 18 | func (out *testBytes) UnmarshalText(text []byte) error { 19 | *out = make([]byte, hex.DecodedLen(len(text))) 20 | _, err := hex.Decode(*out, text) 21 | return err 22 | } 23 | 24 | func (tb testBytes) ByteString() *cryptobyte.String { 25 | s := cryptobyte.String(tb) 26 | return &s 27 | } 28 | 29 | func loadTestVector(t *testing.T, filename string, v interface{}) { 30 | f, err := os.Open(filename) 31 | if err != nil { 32 | t.Fatalf("failed to open test vector %q: %v", filename, err) 33 | } 34 | defer f.Close() 35 | 36 | if err := json.NewDecoder(f).Decode(v); err != nil { 37 | t.Fatalf("failed to load test vector %q: %v", filename, err) 38 | } 39 | } 40 | 41 | type deserializationTest struct { 42 | VLBytesHeader testBytes `json:"vlbytes_header"` 43 | Length uint32 `json:"length"` 44 | } 45 | 46 | func TestDeserialization(t *testing.T) { 47 | var tests []deserializationTest 48 | loadTestVector(t, "testdata/deserialization.json", &tests) 49 | 50 | for i, tc := range tests { 51 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 52 | var length uint32 53 | s := tc.VLBytesHeader.ByteString() 54 | if !readVarint(s, &length) { 55 | t.Fatalf("readVarint() = false") 56 | } else if !s.Empty() { 57 | t.Errorf("byte string should be empty after readVarint()") 58 | } 59 | if length != tc.Length { 60 | t.Errorf("got %v, want %v", length, tc.Length) 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /passive_client_test.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/cloudflare/circl/hpke" 10 | ) 11 | 12 | type passiveClientTest struct { 13 | CipherSuite cipherSuite `json:"cipher_suite"` 14 | 15 | ExternalPSKs []struct { 16 | PSKID testBytes `json:"psk_id"` 17 | PSK testBytes `json:"psk"` 18 | } `json:"external_psks"` 19 | KeyPackage testBytes `json:"key_package"` 20 | SignaturePriv testBytes `json:"signature_priv"` 21 | EncryptionPriv testBytes `json:"encryption_priv"` 22 | InitPriv testBytes `json:"init_priv"` 23 | 24 | Welcome testBytes `json:"welcome"` 25 | RatchetTree testBytes `json:"ratchet_tree"` 26 | InitialEpochAuthenticator testBytes `json:"initial_epoch_authenticator"` 27 | 28 | Epochs []struct { 29 | Proposals []testBytes `json:"proposals"` 30 | Commit testBytes `json:"commit"` 31 | EpochAuthenticator testBytes `json:"epoch_authenticator"` 32 | } `json:"epochs"` 33 | } 34 | 35 | type pendingProposal struct { 36 | ref proposalRef 37 | proposal *proposal 38 | sender leafIndex 39 | } 40 | 41 | func testPassiveClient(t *testing.T, tc *passiveClientTest) { 42 | cs := tc.CipherSuite 43 | initPriv := normalizePriv(cs, []byte(tc.InitPriv)) 44 | encryptionPriv := normalizePriv(cs, []byte(tc.EncryptionPriv)) 45 | signaturePriv := normalizePriv(cs, []byte(tc.SignaturePriv)) 46 | 47 | // TODO: drop the seed size check, see: 48 | // https://github.com/cloudflare/circl/issues/486 49 | kem, kdf, _ := cs.hpke().Params() 50 | if kem.Scheme().SeedSize() != kdf.ExtractSize() { 51 | t.Skip("TODO: kem.Scheme().SeedSize() != kdf.ExtractSize()") 52 | } 53 | 54 | msg, err := unmarshalMLSMessage(tc.Welcome, wireFormatMLSWelcome) 55 | if err != nil { 56 | t.Fatalf("unmarshal(welcome) = %v", err) 57 | } 58 | welcome := msg.welcome 59 | if welcome.cipherSuite != cs { 60 | t.Fatalf("welcome.cipherSuite = %v, want %v", welcome.cipherSuite, cs) 61 | } 62 | 63 | msg, err = unmarshalMLSMessage(tc.KeyPackage, wireFormatMLSKeyPackage) 64 | if err != nil { 65 | t.Fatalf("unmarshal(keyPackage) = %v", err) 66 | } 67 | keyPkg := msg.keyPackage 68 | if keyPkg.cipherSuite != welcome.cipherSuite { 69 | t.Fatalf("keyPkg.cipherSuite = %v, want %v", keyPkg.cipherSuite, welcome.cipherSuite) 70 | } 71 | 72 | if err := checkEncryptionKeyPair(cs, keyPkg.initKey, initPriv); err != nil { 73 | t.Errorf("invalid init keypair: %v", err) 74 | } 75 | if err := checkEncryptionKeyPair(cs, keyPkg.leafNode.encryptionKey, encryptionPriv); err != nil { 76 | t.Errorf("invalid encryption keypair: %v", err) 77 | } 78 | if err := checkSignatureKeyPair(cs, []byte(keyPkg.leafNode.signatureKey), signaturePriv); err != nil { 79 | t.Errorf("invalid signature keypair: %v", err) 80 | } 81 | 82 | keyPkgRef, err := keyPkg.generateRef() 83 | if err != nil { 84 | t.Fatalf("keyPackage.generateRef() = %v", err) 85 | } 86 | 87 | groupSecrets, err := welcome.decryptGroupSecrets(keyPkgRef, initPriv) 88 | if err != nil { 89 | t.Fatalf("welcome.decryptGroupSecrets() = %v", err) 90 | } 91 | 92 | if !groupSecrets.verifySingleReinitOrBranchPSK() { 93 | t.Errorf("groupSecrets.verifySingleReinitOrBranchPSK() failed") 94 | } 95 | 96 | var psks [][]byte 97 | for _, pskID := range groupSecrets.psks { 98 | if pskID.pskType != pskTypeExternal { 99 | t.Fatalf("group secrets contain a non-external PSK ID") 100 | } 101 | 102 | found := false 103 | for _, epsk := range tc.ExternalPSKs { 104 | if bytes.Equal([]byte(epsk.PSKID), pskID.pskID) { 105 | psks = append(psks, []byte(epsk.PSK)) 106 | found = true 107 | break 108 | } 109 | } 110 | if !found { 111 | t.Fatalf("PSK ID %v not found", pskID.pskID) 112 | } 113 | } 114 | 115 | pskSecret, err := extractPSKSecret(cs, groupSecrets.psks, psks) 116 | if err != nil { 117 | t.Fatalf("extractPSKSecret() = %v", err) 118 | } 119 | 120 | groupInfo, err := welcome.decryptGroupInfo(groupSecrets.joinerSecret, pskSecret) 121 | if err != nil { 122 | t.Fatalf("welcome.decryptGroupInfo() = %v", err) 123 | } 124 | 125 | rawTree := []byte(tc.RatchetTree) 126 | if rawTree == nil { 127 | rawTree = findExtensionData(groupInfo.extensions, extensionTypeRatchetTree) 128 | } 129 | if rawTree == nil { 130 | t.Fatalf("missing ratchet tree") 131 | } 132 | 133 | var tree ratchetTree 134 | if err := unmarshal(rawTree, &tree); err != nil { 135 | t.Fatalf("unmarshal(ratchetTree) = %v", err) 136 | } 137 | 138 | signerNode := tree.getLeaf(groupInfo.signer) 139 | if signerNode == nil { 140 | t.Errorf("signer node is blank") 141 | } else if !groupInfo.verifySignature(signerNode.signatureKey) { 142 | t.Errorf("groupInfo.verifySignature() failed") 143 | } 144 | if !groupInfo.verifyConfirmationTag(groupSecrets.joinerSecret, pskSecret) { 145 | t.Errorf("groupInfo.verifyConfirmationTag() failed") 146 | } 147 | if groupInfo.groupContext.cipherSuite != keyPkg.cipherSuite { 148 | t.Errorf("groupInfo.cipherSuite = %v, want %v", groupInfo.groupContext.cipherSuite, keyPkg.cipherSuite) 149 | } 150 | 151 | disableLifetimeCheck := func() time.Time { return time.Time{} } 152 | if err := tree.verifyIntegrity(&groupInfo.groupContext, disableLifetimeCheck); err != nil { 153 | t.Errorf("tree.verifyIntegrity() = %v", err) 154 | } 155 | 156 | myLeafIndex, ok := tree.findLeaf(&keyPkg.leafNode) 157 | if !ok { 158 | t.Errorf("tree.findLeaf() = false") 159 | } 160 | 161 | privTree := make([][]byte, len(tree)) 162 | privTree[int(myLeafIndex.nodeIndex())] = encryptionPriv 163 | 164 | if groupSecrets.pathSecret != nil { 165 | nodeIndex := commonAncestor(myLeafIndex.nodeIndex(), groupInfo.signer.nodeIndex()) 166 | nodePriv, err := nodePrivFromPathSecret(cs, groupSecrets.pathSecret, tree.get(nodeIndex).encryptionKey()) 167 | if err != nil { 168 | t.Fatalf("failed to derive node %v private key from path secret: %v", nodeIndex, err) 169 | } 170 | privTree[int(nodeIndex)] = nodePriv 171 | 172 | pathSecret := groupSecrets.pathSecret 173 | for { 174 | nodeIndex, ok = tree.numLeaves().parent(nodeIndex) 175 | if !ok { 176 | break 177 | } 178 | 179 | pathSecret, err := cs.deriveSecret(pathSecret, []byte("path")) 180 | if err != nil { 181 | t.Fatalf("deriveSecret(pathSecret[n-1]) = %v", err) 182 | } 183 | 184 | nodePriv, err := nodePrivFromPathSecret(cs, pathSecret, tree.get(nodeIndex).encryptionKey()) 185 | if err != nil { 186 | t.Fatalf("failed to derive node %v private key from path secret: %v", nodeIndex, err) 187 | } 188 | privTree[int(nodeIndex)] = nodePriv 189 | } 190 | } 191 | 192 | // TODO: perform other group info verification steps 193 | 194 | groupCtx := groupInfo.groupContext 195 | 196 | epochSecret, err := groupCtx.extractEpochSecret(groupSecrets.joinerSecret, pskSecret) 197 | if err != nil { 198 | t.Fatalf("groupContext.extractEpochSecret() = %v", err) 199 | } 200 | epochAuthenticator, err := cs.deriveSecret(epochSecret, secretLabelAuthentication) 201 | if err != nil { 202 | t.Errorf("deriveSecret(authentication) = %v", err) 203 | } else if !bytes.Equal(epochAuthenticator, []byte(tc.InitialEpochAuthenticator)) { 204 | t.Errorf("deriveSecret(authentication) = %v, want %v", epochAuthenticator, tc.InitialEpochAuthenticator) 205 | } 206 | 207 | initSecret, err := cs.deriveSecret(epochSecret, secretLabelInit) 208 | if err != nil { 209 | t.Errorf("deriveSecret(init) = %v", err) 210 | } 211 | 212 | interimTranscriptHash, err := nextInterimTranscriptHash(cs, groupCtx.confirmedTranscriptHash, groupInfo.confirmationTag) 213 | if err != nil { 214 | t.Errorf("nextInterimTranscriptHash() = %v", err) 215 | } 216 | 217 | for i, epoch := range tc.Epochs { 218 | t.Logf("epoch %v", i) 219 | 220 | var pendingProposals []pendingProposal 221 | for _, rawProposal := range epoch.Proposals { 222 | var msg mlsMessage 223 | if err := unmarshal([]byte(rawProposal), &msg); err != nil { 224 | t.Fatalf("unmarshal(proposal) = %v", err) 225 | } else if msg.wireFormat != wireFormatMLSPublicMessage { 226 | t.Fatalf("TODO: wireFormat = %v", msg.wireFormat) 227 | } 228 | pubMsg := msg.publicMessage 229 | 230 | // TODO: public message checks 231 | 232 | authContent := pubMsg.authenticatedContent() 233 | 234 | if authContent.content.contentType != contentTypeProposal { 235 | t.Errorf("contentType = %v, want %v", authContent.content.contentType, contentTypeProposal) 236 | } 237 | proposal := authContent.content.proposal 238 | 239 | ref, err := authContent.generateProposalRef(cs) 240 | if err != nil { 241 | t.Fatalf("proposal.generateRef() = %v", err) 242 | } 243 | 244 | pendingProposals = append(pendingProposals, pendingProposal{ 245 | ref: ref, 246 | proposal: proposal, 247 | sender: pubMsg.content.sender.leafIndex, 248 | }) 249 | } 250 | 251 | var msg mlsMessage 252 | if err := unmarshal([]byte(epoch.Commit), &msg); err != nil { 253 | t.Fatalf("unmarshal(commit) = %v", err) 254 | } else if msg.wireFormat != wireFormatMLSPublicMessage { 255 | t.Fatalf("TODO: wireFormat = %v", msg.wireFormat) 256 | } 257 | pubMsg := msg.publicMessage 258 | 259 | if pubMsg.content.epoch != groupCtx.epoch { 260 | t.Errorf("epoch = %v, want %v", pubMsg.content.epoch, groupCtx.epoch) 261 | } 262 | 263 | if pubMsg.content.sender.senderType != senderTypeMember { 264 | t.Fatalf("TODO: senderType = %v", pubMsg.content.sender.senderType) 265 | } 266 | senderLeafIndex := pubMsg.content.sender.leafIndex 267 | // TODO: check tree length 268 | senderNode := tree.getLeaf(senderLeafIndex) 269 | if senderNode == nil { 270 | t.Fatalf("blank leaf node for sender") 271 | } 272 | 273 | authContent := pubMsg.authenticatedContent() 274 | if !authContent.verifySignature(cs, []byte(senderNode.signatureKey), &groupCtx) { 275 | t.Errorf("verifySignature() failed") 276 | } 277 | 278 | membershipKey, err := cs.deriveSecret(epochSecret, secretLabelMembership) 279 | if err != nil { 280 | t.Errorf("deriveSecret(membership) = %v", err) 281 | } else if !pubMsg.verifyMembershipTag(cs, membershipKey, &groupCtx) { 282 | t.Errorf("publicMessage.verifyMembershipTag() failed") 283 | } 284 | 285 | if authContent.content.contentType != contentTypeCommit { 286 | t.Errorf("contentType = %v, want %v", authContent.content.contentType, contentTypeCommit) 287 | } 288 | commit := authContent.content.commit 289 | 290 | var ( 291 | proposals []proposal 292 | senders []leafIndex 293 | ) 294 | for _, propOrRef := range commit.proposals { 295 | switch propOrRef.typ { 296 | case proposalOrRefTypeProposal: 297 | proposals = append(proposals, *propOrRef.proposal) 298 | senders = append(senders, senderLeafIndex) 299 | case proposalOrRefTypeReference: 300 | var found bool 301 | for _, pp := range pendingProposals { 302 | if pp.ref.Equal(propOrRef.reference) { 303 | found = true 304 | proposals = append(proposals, *pp.proposal) 305 | senders = append(senders, pp.sender) 306 | break 307 | } 308 | } 309 | if !found { 310 | t.Fatalf("cannot find proposal reference %v", propOrRef.reference) 311 | } 312 | } 313 | } 314 | 315 | if err := verifyProposalList(proposals, senders, senderLeafIndex); err != nil { 316 | t.Errorf("verifyProposals() = %v", err) 317 | } 318 | // TODO: additional proposal list checks 319 | 320 | newTree := make(ratchetTree, len(tree)) 321 | copy(newTree, tree) 322 | newTree.apply(proposals, senders) 323 | 324 | newPrivTree := make([][]byte, len(newTree)) 325 | for i := range tree { 326 | if i < len(newPrivTree) { 327 | newPrivTree[i] = privTree[i] 328 | } 329 | } 330 | 331 | if proposalListNeedsPath(proposals) && commit.path == nil { 332 | t.Errorf("proposal list needs update path") 333 | } 334 | 335 | var ( 336 | pskIDs []preSharedKeyID 337 | psks [][]byte 338 | ) 339 | for _, prop := range proposals { 340 | if prop.proposalType != proposalTypePSK { 341 | continue 342 | } 343 | 344 | pskID := prop.preSharedKey.psk 345 | if pskID.pskType != pskTypeExternal { 346 | t.Skipf("TODO: PSK ID type = %v", pskID.pskType) 347 | } 348 | 349 | found := false 350 | for _, epsk := range tc.ExternalPSKs { 351 | if bytes.Equal([]byte(epsk.PSKID), pskID.pskID) { 352 | pskIDs = append(pskIDs, pskID) 353 | psks = append(psks, []byte(epsk.PSK)) 354 | found = true 355 | break 356 | } 357 | } 358 | if !found { 359 | t.Fatalf("PSK ID %v not found", pskID.pskID) 360 | } 361 | } 362 | 363 | newGroupCtx := groupCtx 364 | newGroupCtx.epoch++ 365 | 366 | _, kdf, _ := cs.hpke().Params() 367 | commitSecret := make([]byte, kdf.ExtractSize()) 368 | if commit.path != nil { 369 | if commit.path.leafNode.leafNodeSource != leafNodeSourceCommit { 370 | t.Errorf("commit path leaf node source must be commit") 371 | } 372 | 373 | // The same signature key can be re-used, but the encryption key 374 | // must change 375 | signatureKeys, encryptionKeys := newTree.keys() 376 | delete(signatureKeys, string(senderNode.signatureKey)) 377 | err := commit.path.leafNode.verify(&leafNodeVerifyOptions{ 378 | cipherSuite: cs, 379 | groupID: groupCtx.groupID, 380 | leafIndex: senderLeafIndex, 381 | supportedCreds: newTree.supportedCreds(), 382 | signatureKeys: signatureKeys, 383 | encryptionKeys: encryptionKeys, 384 | now: func() time.Time { return time.Time{} }, 385 | }) 386 | if err != nil { 387 | t.Errorf("leafNode.verify() = %v", err) 388 | } 389 | 390 | for _, updateNode := range commit.path.nodes { 391 | if _, dup := encryptionKeys[string(updateNode.encryptionKey)]; dup { 392 | t.Errorf("encryption key in update path already used in ratchet tree") 393 | break 394 | } 395 | } 396 | 397 | if err := newTree.mergeUpdatePath(cs, senderLeafIndex, commit.path); err != nil { 398 | t.Errorf("ratchetTree.mergeUpdatePath() = %v", err) 399 | } 400 | 401 | newGroupCtx.treeHash, err = newTree.computeRootTreeHash(cs) 402 | if err != nil { 403 | t.Fatalf("ratchetTree.computeRootTreeHash() = %v", err) 404 | } 405 | 406 | // TODO: update group context extensions 407 | 408 | commitSecret, err = newTree.decryptPathSecrets(cs, &newGroupCtx, senderLeafIndex, myLeafIndex, commit.path, newPrivTree) 409 | if err != nil { 410 | t.Fatalf("ratchetTree.decryptPathSecrets() = %v", err) 411 | } 412 | } 413 | 414 | newGroupCtx.confirmedTranscriptHash, err = authContent.confirmedTranscriptHashInput().hash(cs, interimTranscriptHash) 415 | if err != nil { 416 | t.Fatalf("confirmedTranscriptHashInput.hash() = %v", err) 417 | } 418 | 419 | newInterimTranscriptHash, err := nextInterimTranscriptHash(cs, newGroupCtx.confirmedTranscriptHash, authContent.auth.confirmationTag) 420 | if err != nil { 421 | t.Fatalf("nextInterimTranscriptHash() = %v", err) 422 | } 423 | 424 | newPSKSecret, err := extractPSKSecret(cs, pskIDs, psks) 425 | if err != nil { 426 | t.Fatalf("extractPSKSecret() = %v", err) 427 | } 428 | 429 | newJoinerSecret, err := newGroupCtx.extractJoinerSecret(initSecret, commitSecret) 430 | if err != nil { 431 | t.Fatalf("groupContext.extractJoinerSecret() = %v", err) 432 | } 433 | 434 | newEpochSecret, err := newGroupCtx.extractEpochSecret(newJoinerSecret, newPSKSecret) 435 | if err != nil { 436 | t.Fatalf("groupContext.extractEpochSecret() = %v", err) 437 | } 438 | epochAuthenticator, err := cs.deriveSecret(newEpochSecret, secretLabelAuthentication) 439 | if err != nil { 440 | t.Fatalf("deriveSecret(authentication) = %v", err) 441 | } else if !bytes.Equal(epochAuthenticator, []byte(epoch.EpochAuthenticator)) { 442 | t.Errorf("deriveSecret(authentication) = %v, want %v", epochAuthenticator, epoch.EpochAuthenticator) 443 | } 444 | 445 | newInitSecret, err := cs.deriveSecret(newEpochSecret, secretLabelInit) 446 | if err != nil { 447 | t.Fatalf("deriveSecret(init) = %v", err) 448 | } 449 | 450 | confirmationKey, err := cs.deriveSecret(newEpochSecret, secretLabelConfirm) 451 | if err != nil { 452 | t.Fatalf("deriveSecret(confirm) = %v", err) 453 | } 454 | confirmationTag := cs.signMAC(confirmationKey, newGroupCtx.confirmedTranscriptHash) 455 | if !bytes.Equal(confirmationTag, authContent.auth.confirmationTag) { 456 | t.Errorf("invalid confirmation tag: got %v, want %v", confirmationTag, authContent.auth.confirmationTag) 457 | } 458 | 459 | tree = newTree 460 | privTree = newPrivTree 461 | groupCtx = newGroupCtx 462 | interimTranscriptHash = newInterimTranscriptHash 463 | pskSecret = newPSKSecret 464 | epochSecret = newEpochSecret 465 | initSecret = newInitSecret 466 | } 467 | } 468 | 469 | // normalizePriv ensures that private keys in test vectors have the correct 470 | // size according to the HPKE specification. See: 471 | // https://github.com/mlswg/mls-implementations/issues/176 472 | func normalizePriv(cs cipherSuite, priv []byte) []byte { 473 | kem, _, _ := cs.hpke().Params() 474 | privSize := kem.Scheme().PrivateKeySize() 475 | if kem != hpke.KEM_P521_HKDF_SHA512 || len(priv) >= privSize { 476 | return priv 477 | } 478 | b := make([]byte, privSize) 479 | copy(b[privSize-len(priv):], priv) 480 | return b 481 | } 482 | 483 | func unmarshalMLSMessage(raw testBytes, wf wireFormat) (*mlsMessage, error) { 484 | var msg mlsMessage 485 | if err := unmarshal([]byte(raw), &msg); err != nil { 486 | return nil, err 487 | } else if msg.wireFormat != wf { 488 | return nil, fmt.Errorf("invalid wireFormat: got %v, want %v", msg.wireFormat, wf) 489 | } 490 | return &msg, nil 491 | } 492 | 493 | func checkEncryptionKeyPair(cs cipherSuite, pub, priv []byte) error { 494 | wantPlaintext := []byte("foo") 495 | label := []byte("bar") 496 | 497 | kemOutput, ciphertext, err := cs.encryptWithLabel(pub, label, nil, wantPlaintext) 498 | if err != nil { 499 | return err 500 | } 501 | 502 | plaintext, err := cs.decryptWithLabel(priv, label, nil, kemOutput, ciphertext) 503 | if err != nil { 504 | return err 505 | } 506 | 507 | if !bytes.Equal(plaintext, wantPlaintext) { 508 | return fmt.Errorf("got plaintext %v, want %v", plaintext, wantPlaintext) 509 | } 510 | 511 | return nil 512 | } 513 | 514 | func checkSignatureKeyPair(cs cipherSuite, pub, priv []byte) error { 515 | content := []byte("foo") 516 | label := []byte("bar") 517 | 518 | signature, err := cs.signWithLabel(priv, label, content) 519 | if err != nil { 520 | return err 521 | } 522 | 523 | if !cs.verifyWithLabel(pub, label, content, signature) { 524 | return fmt.Errorf("signature verification failed") 525 | } 526 | 527 | return nil 528 | } 529 | 530 | func TestPassiveClientWelcome(t *testing.T) { 531 | var tests []passiveClientTest 532 | loadTestVector(t, "testdata/passive-client-welcome.json", &tests) 533 | 534 | for i, tc := range tests { 535 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 536 | testPassiveClient(t, &tc) 537 | }) 538 | } 539 | } 540 | 541 | func TestPassiveClientCommit(t *testing.T) { 542 | var tests []passiveClientTest 543 | loadTestVector(t, "testdata/passive-client-handling-commit.json", &tests) 544 | 545 | for i, tc := range tests { 546 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 547 | testPassiveClient(t, &tc) 548 | }) 549 | } 550 | } 551 | 552 | func TestPassiveClientRandom(t *testing.T) { 553 | var tests []passiveClientTest 554 | loadTestVector(t, "testdata/passive-client-random.json", &tests) 555 | 556 | for i, tc := range tests { 557 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 558 | t.Skip("TODO") // "mls: invalid UpdatePathNode.encrypted_path_secret length" 559 | testPassiveClient(t, &tc) 560 | }) 561 | } 562 | } 563 | -------------------------------------------------------------------------------- /secret_tree.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "golang.org/x/crypto/cryptobyte" 5 | ) 6 | 7 | type ratchetLabel []byte 8 | 9 | var ( 10 | ratchetLabelHandshake = ratchetLabel("handshake") 11 | ratchetLabelApplication = ratchetLabel("application") 12 | ) 13 | 14 | func ratchetLabelFromContentType(ct contentType) ratchetLabel { 15 | switch ct { 16 | case contentTypeApplication: 17 | return ratchetLabelApplication 18 | case contentTypeProposal, contentTypeCommit: 19 | return ratchetLabelHandshake 20 | default: 21 | panic("unreachable") 22 | } 23 | } 24 | 25 | // secretTree holds tree node secrets used for the generation of encryption 26 | // keys and nonces. 27 | type secretTree [][]byte 28 | 29 | func deriveSecretTree(cs cipherSuite, n numLeaves, encryptionSecret []byte) (secretTree, error) { 30 | tree := make(secretTree, int(n.width())) 31 | tree.set(n.root(), encryptionSecret) 32 | err := tree.deriveChildren(cs, n.root()) 33 | return tree, err 34 | } 35 | 36 | func (tree secretTree) deriveChildren(cs cipherSuite, x nodeIndex) error { 37 | l, r, ok := x.children() 38 | if !ok { 39 | return nil 40 | } 41 | 42 | parentSecret := tree.get(x) 43 | _, kdf, _ := cs.hpke().Params() 44 | nh := uint16(kdf.ExtractSize()) 45 | leftSecret, err := cs.expandWithLabel(parentSecret, []byte("tree"), []byte("left"), nh) 46 | if err != nil { 47 | return err 48 | } 49 | rightSecret, err := cs.expandWithLabel(parentSecret, []byte("tree"), []byte("right"), nh) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | tree.set(l, leftSecret) 55 | tree.set(r, rightSecret) 56 | 57 | if err := tree.deriveChildren(cs, l); err != nil { 58 | return err 59 | } 60 | if err := tree.deriveChildren(cs, r); err != nil { 61 | return err 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func (tree secretTree) get(ni nodeIndex) []byte { 68 | secret := tree[int(ni)] 69 | if secret == nil { 70 | panic("empty node in secret tree") 71 | } 72 | return secret 73 | } 74 | 75 | func (tree secretTree) set(ni nodeIndex, secret []byte) { 76 | tree[int(ni)] = secret 77 | } 78 | 79 | // deriveRatchetRoot derives the root of a ratchet for a tree node. 80 | func (tree secretTree) deriveRatchetRoot(cs cipherSuite, ni nodeIndex, label ratchetLabel) (ratchetSecret, error) { 81 | _, kdf, _ := cs.hpke().Params() 82 | nh := uint16(kdf.ExtractSize()) 83 | root, err := cs.expandWithLabel(tree.get(ni), []byte(label), nil, nh) 84 | return ratchetSecret{root, 0}, err 85 | } 86 | 87 | type ratchetSecret struct { 88 | secret []byte 89 | generation uint32 90 | } 91 | 92 | func (secret ratchetSecret) deriveNonce(cs cipherSuite) ([]byte, error) { 93 | _, _, aead := cs.hpke().Params() 94 | nn := uint16(aead.NonceSize()) 95 | return deriveTreeSecret(cs, secret.secret, []byte("nonce"), secret.generation, nn) 96 | } 97 | 98 | func (secret ratchetSecret) deriveKey(cs cipherSuite) ([]byte, error) { 99 | _, _, aead := cs.hpke().Params() 100 | nk := uint16(aead.KeySize()) 101 | return deriveTreeSecret(cs, secret.secret, []byte("key"), secret.generation, nk) 102 | } 103 | 104 | func (secret ratchetSecret) deriveNext(cs cipherSuite) (ratchetSecret, error) { 105 | _, kdf, _ := cs.hpke().Params() 106 | nh := uint16(kdf.ExtractSize()) 107 | next, err := deriveTreeSecret(cs, secret.secret, []byte("secret"), secret.generation, nh) 108 | return ratchetSecret{next, secret.generation + 1}, err 109 | } 110 | 111 | func deriveTreeSecret(cs cipherSuite, secret, label []byte, generation uint32, length uint16) ([]byte, error) { 112 | var b cryptobyte.Builder 113 | b.AddUint32(generation) 114 | context := b.BytesOrPanic() 115 | 116 | return cs.expandWithLabel(secret, label, context, length) 117 | } 118 | -------------------------------------------------------------------------------- /secret_tree_test.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | type secretTreeTest struct { 10 | CipherSuite cipherSuite `json:"cipher_suite"` 11 | 12 | SenderData struct { 13 | SenderDataSecret testBytes `json:"sender_data_secret"` 14 | Ciphertext testBytes `json:"ciphertext"` 15 | Key testBytes `json:"key"` 16 | Nonce testBytes `json:"nonce"` 17 | } `json:"sender_data"` 18 | 19 | EncryptionSecret testBytes `json:"encryption_secret"` 20 | Leaves [][]secretTreeTestGen `json:"leaves"` 21 | } 22 | 23 | type secretTreeTestGen struct { 24 | Generation uint32 `json:"generation"` 25 | HandshakeKey testBytes `json:"handshake_key"` 26 | HandshakeNonce testBytes `json:"handshake_nonce"` 27 | ApplicationKey testBytes `json:"application_key"` 28 | ApplicationNonce testBytes `json:"application_nonce"` 29 | } 30 | 31 | func testSecretTree(t *testing.T, tc *secretTreeTest) { 32 | senderDataSecret := []byte(tc.SenderData.SenderDataSecret) 33 | ciphertext := []byte(tc.SenderData.Ciphertext) 34 | 35 | key, err := expandSenderDataKey(tc.CipherSuite, senderDataSecret, ciphertext) 36 | if err != nil { 37 | t.Errorf("expandSenderDataKey() = %v", err) 38 | } else if !bytes.Equal(key, []byte(tc.SenderData.Key)) { 39 | t.Errorf("expandSenderDataKey() = %v, want %v", key, tc.SenderData.Key) 40 | } 41 | 42 | nonce, err := expandSenderDataNonce(tc.CipherSuite, senderDataSecret, ciphertext) 43 | if err != nil { 44 | t.Errorf("expandSenderDataNonce() = %v", err) 45 | } else if !bytes.Equal(nonce, []byte(tc.SenderData.Nonce)) { 46 | t.Errorf("expandSenderDataNonce() = %v, want %v", nonce, tc.SenderData.Nonce) 47 | } 48 | 49 | tree, err := deriveSecretTree(tc.CipherSuite, numLeaves(len(tc.Leaves)), []byte(tc.EncryptionSecret)) 50 | if err != nil { 51 | t.Fatalf("generateSecretTree() = %v", err) 52 | } 53 | 54 | for i, gens := range tc.Leaves { 55 | li := leafIndex(i) 56 | t.Run(fmt.Sprintf("leaf-%v/handshake", li), func(t *testing.T) { 57 | testRatchetSecret(t, tc.CipherSuite, tree, li, ratchetLabelHandshake, gens) 58 | }) 59 | t.Run(fmt.Sprintf("leaf-%v/application", li), func(t *testing.T) { 60 | testRatchetSecret(t, tc.CipherSuite, tree, li, ratchetLabelApplication, gens) 61 | }) 62 | } 63 | } 64 | 65 | func testRatchetSecret(t *testing.T, cs cipherSuite, tree secretTree, li leafIndex, label ratchetLabel, gens []secretTreeTestGen) { 66 | secret, err := tree.deriveRatchetRoot(cs, li.nodeIndex(), label) 67 | if err != nil { 68 | t.Fatalf("deriveRatchetRoot() = %v", err) 69 | } 70 | 71 | for _, gen := range gens { 72 | if gen.Generation < secret.generation { 73 | panic("unreachable") 74 | } 75 | 76 | for secret.generation != gen.Generation { 77 | secret, err = secret.deriveNext(cs) 78 | if err != nil { 79 | t.Fatalf("deriveNext() = %v", err) 80 | } 81 | } 82 | 83 | var wantKey, wantNonce testBytes 84 | switch string(label) { 85 | case string(ratchetLabelHandshake): 86 | wantKey, wantNonce = gen.HandshakeKey, gen.HandshakeNonce 87 | case string(ratchetLabelApplication): 88 | wantKey, wantNonce = gen.ApplicationKey, gen.ApplicationNonce 89 | default: 90 | panic("unreachable") 91 | } 92 | 93 | key, err := secret.deriveKey(cs) 94 | if err != nil { 95 | t.Fatalf("deriveKey() = %v", err) 96 | } else if !bytes.Equal(key, []byte(wantKey)) { 97 | t.Errorf("deriveKey() = %v, want %v", key, wantKey) 98 | } 99 | 100 | nonce, err := secret.deriveNonce(cs) 101 | if err != nil { 102 | t.Fatalf("deriveNonce() = %v", err) 103 | } else if !bytes.Equal(nonce, []byte(wantNonce)) { 104 | t.Errorf("deriveNonce() = %v, want %v", nonce, wantNonce) 105 | } 106 | } 107 | } 108 | 109 | func TestSecretTree(t *testing.T) { 110 | var tests []secretTreeTest 111 | loadTestVector(t, "testdata/secret-tree.json", &tests) 112 | 113 | for i, tc := range tests { 114 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 115 | testSecretTree(t, &tc) 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /testdata/crypto-basics.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "cipher_suite": 1, 4 | "derive_secret": { 5 | "label": "DeriveSecret", 6 | "out": "3b08c195a246c4ad469c1d11c10e62890d8fa6b684494ff925409efdb1ff0464", 7 | "secret": "1a9ce178a53f8752d2513c27efe9c85133f6c0a97f7b35ac200695024a77228e" 8 | }, 9 | "derive_tree_secret": { 10 | "generation": 2694881440, 11 | "label": "DeriveTreeSecret", 12 | "length": 32, 13 | "out": "8461f3ccc603eae52149a23a4134d29c880a1ad1ba70441e5d586e3521ec7b25", 14 | "secret": "5133c6f8bad297f5d3beacdf477f0c45ec51b02de659d305220c5f9385c6eb43" 15 | }, 16 | "encrypt_with_label": { 17 | "ciphertext": "15c80ea2bc37db221baa530ef5aea88650f0ce0f262803d6f78f3a1392f7ccd960eff94ca081ee54efa4c3acfa0eb591", 18 | "context": "26347dd7f218d1de8673d6a66646ce06ac5fd3aa8d5c33f65d86aeefdcf4a31e", 19 | "kem_output": "0a144e8fbf2d6dcf6fe9d2e2b8aeca5461ff5b0ea9c0ede1040c3dc7ed1dfd1c", 20 | "label": "EncryptWithLabel", 21 | "plaintext": "8f55dd30f03d64335c22b53ea7670bb1becf49b04021f706368fe93eeb358f46", 22 | "priv": "fb1ade7939987ff12a9d620772b1f9f7caeba26f8a3ecea9617d9402cd862444", 23 | "pub": "ecea6564da58d6c6cff6c733bd4ae0815b1f60bb911b73e4ef1d06263ec4ce58" 24 | }, 25 | "expand_with_label": { 26 | "context": "2ff8c1f9d9c1248f82e372ddb5791c771695e01882abca6a64097bd2f04c971f", 27 | "label": "ExpandWithLabel", 28 | "length": 16, 29 | "out": "c1e8eb360391526c0c64039f13e0c5b1", 30 | "secret": "1499360a561335f4ef51d0a1b0d586900dc8007ae405b1ab79bf4207bb3d67e4" 31 | }, 32 | "ref_hash": { 33 | "label": "RefHash", 34 | "out": "e8027fffc5f9bb469f29172538dc0f3a78f14f323495bbd2217eba7a77fb242a", 35 | "value": "40312db83f651883c05ab26fa12c6af61930015c81947cfd0f129e6d99210bb2" 36 | }, 37 | "sign_with_label": { 38 | "content": "cd289cc7ba2869f64f3c32ffd133f500d17abace919a5ffe7faa974200d81932", 39 | "label": "SignWithLabel", 40 | "priv": "a2f640dd5005fcad6adb8e9bd8b60d70946bb802e1e788307929fdac81e1ec74", 41 | "pub": "85600e54e5c2919ccbd0742126e5d837cf7a2ba50d75a69b3f35dcfe4a50ffe2", 42 | "signature": "996bd223ddb4d55a2b57d85cb2944f21facc95696053ddf66d590060fdc719f4a26c6212ce605414e0d5e66a55921dd99d11122218c35bc23408b0076e8bc40b" 43 | } 44 | }, 45 | { 46 | "cipher_suite": 2, 47 | "derive_secret": { 48 | "label": "DeriveSecret", 49 | "out": "1ecafd3d40cb32cae416e09bc01da56357d00cb094f74e3c69c2969216e50afc", 50 | "secret": "7165576616048aa145d20f7c1d460e2d9d8bbde882bc3bb0750d50e369809f99" 51 | }, 52 | "derive_tree_secret": { 53 | "generation": 2694881440, 54 | "label": "DeriveTreeSecret", 55 | "length": 32, 56 | "out": "298ab27d2e621d9fc079126d9ffce5259fa0d58697267b40bfadf805b01d0d3c", 57 | "secret": "62db8a06300e98dbfd2c831e18544873cec3a2c1ba852fcee8423ff3c08e397e" 58 | }, 59 | "encrypt_with_label": { 60 | "ciphertext": "98b7d4edc79f4a90f65434050f75e31a3bc6501c584d41abd6ed5fe3c2db9de22d25ef40c45cce7d0fcc881d7d27af2f", 61 | "context": "e27e3b0104990cca866751732c82787af4dcf265c893f77e31bcfd679370e24e", 62 | "kem_output": "041fae8a8173cad50c0cc6d55f148ff8edda63083b3673cf6dce6a0ae6ee3f0b61505470309dade87ac5ccdb581da3e9ddf6726949d5a92b65dcad6c8679c7313e", 63 | "label": "EncryptWithLabel", 64 | "plaintext": "38a6b327573639d654b5b729336cf74d01728cf4fa9af81a0ef1814ffc1d492f", 65 | "priv": "ff21771424dadd640e05c67983aafe19b4d8df50783a0c2decc17d0c7ca4cc17", 66 | "pub": "047b27b0be346d14d7b4df30296a030deeba088746da7cfda43d0ec739df3ce90d3c96d5f302e41f935ac9020651285c7bcf073172d375c5abcfc9e491b3491f88" 67 | }, 68 | "expand_with_label": { 69 | "context": "453c420d63ecb1a8da89d3569770c42bad572864f3709d1e9dd19a0961cbf5e1", 70 | "label": "ExpandWithLabel", 71 | "length": 16, 72 | "out": "5710680c556304f4aec67aab4abbc1b1", 73 | "secret": "194d4e81c3f9fcfbc40e2cf3f5b104d0f51e71b5a7f1e4ddd70cc8d4a3620e5f" 74 | }, 75 | "ref_hash": { 76 | "label": "RefHash", 77 | "out": "8f508c2f89d2797b6882ee487dc2832b2b6d59b27681293c2c2c3f1f0e6cd54f", 78 | "value": "feb64672017685dc26d0b7fc41cd96d8bef40af09002eb5aa9a52580c80f0b2e" 79 | }, 80 | "sign_with_label": { 81 | "content": "a0dda617ce4685d764c762b11186b6d60ff8de85ff01eca2413bd4ecfd3b3a57", 82 | "label": "SignWithLabel", 83 | "priv": "207c472d3efaf6737a6f5ae14a3c33a139034865364a128bca5475c85cc02fe0", 84 | "pub": "04448971fc06de011d780cf68fd27e2570322d04079f529c3deb48a2015fdd828162c570264e051b5856e8111171fb0341907173aefa665682c2549af982a31483", 85 | "signature": "304402206042e397e1bb78951709790e3446b13bf17f9c641aaed92fb1768b5bc99dd98402202465153d91dd79e808286088768d8ed150381f3675498d6e150ae713be43387b" 86 | } 87 | }, 88 | { 89 | "cipher_suite": 3, 90 | "derive_secret": { 91 | "label": "DeriveSecret", 92 | "out": "aad859818ca5f2a9896d4d3ee2dccc0cefcd69b666bdb16b52f1de15fb1a5567", 93 | "secret": "cae460c779ebaa3e81c061a371486dff1ed1ff273bea369cc0fc46550b83c407" 94 | }, 95 | "derive_tree_secret": { 96 | "generation": 2694881440, 97 | "label": "DeriveTreeSecret", 98 | "length": 32, 99 | "out": "2095d6a81ab87095d1df26f6bdf012ec06f197e418381c1795a7b758603c936d", 100 | "secret": "c994e257b53f726087ddd7121876f558f1fbd6f807e5ff010830d618d7bab6f2" 101 | }, 102 | "encrypt_with_label": { 103 | "ciphertext": "40dd09ad4c5dc29d373f814bf054c9359cb75a468bc4d2c8bbcffb072a73105c4d9416ebd4fafeb62e59a9dea55da3cd", 104 | "context": "0d6a5cf9ee88b1f8c79d8512477d9bfc5496c207c8173f8dcac0368b4dba7407", 105 | "kem_output": "f26e9e5a94396a90f85a5f72eedf3dacfb1b7f4164e0573edeb9c6c912e1cb49", 106 | "label": "EncryptWithLabel", 107 | "plaintext": "1dd4c1904996ce7d42cee7de68881459fa7a345da59a02040ade37103505baf6", 108 | "priv": "9d122ad4638fcb301b6eb5f4073414afb44bb34d37b4ddee9975b2941d700edb", 109 | "pub": "7a5544b59f5940bf093c921469a00a170a7c92ba56c173d74db32713608d8a40" 110 | }, 111 | "expand_with_label": { 112 | "context": "2e07148f4340c62a55e7608c20d73fddf1f3b8dafb2c7ef24eceb70e136c0d8c", 113 | "label": "ExpandWithLabel", 114 | "length": 32, 115 | "out": "1df5ba7996a34f75d717916a094a14083c03a75e80f0330a8095f5f11cfe1e1f", 116 | "secret": "55aa3ae5242564782567ce097beafe19510230660008b2cc064a78387fa16f36" 117 | }, 118 | "ref_hash": { 119 | "label": "RefHash", 120 | "out": "f11019703c8b630060839b12a475fd39c6a30f8a866790ff46a35f9c65e1df3c", 121 | "value": "4f0c86f9c82fba0a896bd7eecf79a29856e98a7e4f13b9f841ae285d70ed8b68" 122 | }, 123 | "sign_with_label": { 124 | "content": "df308cf2dbf471edf2c29d30e3daf161b5b87d350ee3b2c715c298ec3d10d432", 125 | "label": "SignWithLabel", 126 | "priv": "4e312160ee4981358db479aa877412847abc7f7054b5605511256c395404d054", 127 | "pub": "18275f892ee0ca6f4687ff26c990776387502646ff658c3f572b324faecb05c5", 128 | "signature": "4f56851c2c47f5115a61ff0ab6121b4a4732d4e94805fc7135a5132f87d5ca5f1dc7408816c1ea4f25887725cf5914b48c427a52cabcfeb746a2b8a12e821f08" 129 | } 130 | }, 131 | { 132 | "cipher_suite": 4, 133 | "derive_secret": { 134 | "label": "DeriveSecret", 135 | "out": "ffed9b67c5b8388af9ba51bfa983400dc6cee5c1d79b4cc45a400611075139b842642f1a8d4d92ed3aa9f7497d8a3ce5516552230a883c57dd2bb277ef1e571c", 136 | "secret": "92c5ae58c8aeed565d772847a41f4901a999572ae53740b56a2a3df94c62e57c60f1107c88c70d8aa902049d5f33d36cc4d1f7c233b1406947deb6c1fb243007" 137 | }, 138 | "derive_tree_secret": { 139 | "generation": 2694881440, 140 | "label": "DeriveTreeSecret", 141 | "length": 64, 142 | "out": "dd738960f713469ad0599222c8caa031bbe9438491e79d901a6f7151e2bf8c57948cc27dbc5561a54e600e469f51037d0e1609116bce8b6520e21ee6135d5791", 143 | "secret": "44ecd457f6280bcb1ce1a525aeb3d6b32eb24a81628cdd07c7c663d7790cbbbc1805ec90bb28df27c0567b6ccdc39eabb0b1d3c66033c1355aac4397ea7459ac" 144 | }, 145 | "encrypt_with_label": { 146 | "ciphertext": "194059f738cabf5fb568373030024fd73b19cb7bb45b61173639e85309879287e8327c8afd09d784018770bd4994c0b050df3e608430151419fc6886c57f847c8f68e001b39f2346dae17108d1fa36aa", 147 | "context": "a73f953ea54f3785af7ee8c8928e5d71baf489e425d1068b1407d4a6efff3c5ed0f8f7d60f58af1856d7990fe9e04bd0b827bda913aa22f10fba0e5f2b1c8c50", 148 | "kem_output": "c487db6afe43e4537890be46ef126bd804510fbc8f53bb3e60c39e439d8cfce804575ebea1b1c298d9fb31e84677b5bc3270a74feb051209", 149 | "label": "EncryptWithLabel", 150 | "plaintext": "d1e409fcb7b955e2b368314dc6a34816142448627958f2ce1a103bc2b38ce2a2c1b05678606ae44b2300c959da77b1287228cd9c10e62e8c665db83665a857b4", 151 | "priv": "f98a74e98ed6f217513be360d6d36378fcbbf548f3e4292cf35078f3cc23855fb370fb0ddce879fa864e5a20fd43ab6eed8d8f606b705b92", 152 | "pub": "ab23487f3028c6a5dc5f12eb9bb65a2f93d913962ecc60f6505b6ba82db7d292b1af2106404c829192a14854fff94780cfa325995afc22f7" 153 | }, 154 | "expand_with_label": { 155 | "context": "5837a1b11e1feaba3d403c151e78d233bb4a914b6f7b207607c2c824288cbcc729922d551e829d1727e6875b81b1aad65b7b8398040c09bdc1228d2fab9263f2", 156 | "label": "ExpandWithLabel", 157 | "length": 32, 158 | "out": "0422601b4c3a04ec67e1196eb1f0549d53f0639509309d84f88c265ebc383d18", 159 | "secret": "cf0fb7f2b30b491a94cc2ce32cfd8c46ff0725c7c82376dd4f2ca25f4242ac87fdf02b59482c646d026975a7f7076111875f0b377dc5ca437799735e78d13464" 160 | }, 161 | "ref_hash": { 162 | "label": "RefHash", 163 | "out": "4040acc515e1e0881ab971ea6a6ad0916f756c6edcdfcdd9656d87e942dc1c756b9b6f28aa2659d5342b2244afe4c545fa890529d041dfd4b9256cc9e535ccef", 164 | "value": "cfb75a56c741a7373cfdf7b575f1686db3f9ab05b5fb54f1279488f5ca5145086540477cecc2e79aef5ec4a5942e6b06513121b91f46786e7d8b935ea9ae0783" 165 | }, 166 | "sign_with_label": { 167 | "content": "0b1aee9793837d23fcdd58cdfd3bdd72666deb49dab282b2e1891ce4e7665d3544787a5eb9663bdbc4397bb111ad182dc5a305e9720687812fda4dc988461621", 168 | "label": "SignWithLabel", 169 | "priv": "e6e78b6be2d3a2ef0be99f62c2d3c57a3f392bf53084f276c9675ca1890e46dbdd6922cafff540c7dae1af237ba8272659e1c64cfc8c141aad", 170 | "pub": "7ed080c31aa1c4ba05aea3bd75efe01825a5f219fb7e1ea67b1c171f01c19339873b7bc29614903888ba49f954c9f0c6f0b6051a1ed4ab5880", 171 | "signature": "0b363568911eb4c34d89a01007bab0585d06cc24937cea982d330429802d4d91fae03766f8f0cae6c3067a369ea9b9e87c8015e91a424dcd80377df71eaf4b5ad2b98cea049ba3e29faf66eee0a9289f8ad5f9b38d52ce9c653f2b86002803e1c918bb5ba511ee2784c5ced7df3e59bf0200" 172 | } 173 | }, 174 | { 175 | "cipher_suite": 5, 176 | "derive_secret": { 177 | "label": "DeriveSecret", 178 | "out": "53b38636e19310b7a4b7c03b2e908307aea080763c223504ed21b7821bdcaad7da9ca68d5196eed9337b2f36b5e23a40404c8a1adf5c92c9bf82d4bdcbbb1243", 179 | "secret": "3efe299abc9a73a264098c9beecd6923d8b07bb842537eb21f16e90c4a094512b46f3c1cbef3b86018b2d038f8ba6e874ea3972cebcd21a4ba4eb9474cccabb8" 180 | }, 181 | "derive_tree_secret": { 182 | "generation": 2694881440, 183 | "label": "DeriveTreeSecret", 184 | "length": 64, 185 | "out": "6b9050104fff086270280532ae5403112071605b27b898359c25c67ff811ce68e7c484e9c2fc6bf7fcf3f4203cd6ca6b5ad4d623a82f65307d8e16a345aa5bff", 186 | "secret": "2d837ce0a4b3277564a9b5895b6cc1795e1c5374e0738a1da67ed4edb32eabc7d8825e8bf3391e3ec8ea7212e68147fccdac6ee5f915b75f091e122d9036efba" 187 | }, 188 | "encrypt_with_label": { 189 | "ciphertext": "28205aeb628df17121ea4627d2da4a98b632322be1da7c5abdf47d7fe747f13c575a5d1f8f2812cd9dfd7386edc6ea1fdb9767e5e8b49f09a0808bd3264f960b3ef496a27651760be88360045aaadcf9", 190 | "context": "64ea1f98161c229460d04c46d78c0861e3f0189597102e4c1ee2a6776d3e6b54bbcb1a43fc24ed9dba742d553dfc583f2ca24c696644a19975cc10b0e9992b0f", 191 | "kem_output": "04009e3cec5e8cbb05ed3aa5b9d71a4afbd855082852bf1d46c9c65f9229f7cd4005031cff7b642c7bbefb351271f2280d1fb01f10796a3ee25b3c9b902dfa74ff2a0d017618797aff7c24686625b93ec12d4994d25cffc7d64542578e96f02b4ea0665ac2c5649137b1beea44569fbd691aeb14c290eb6f6245c149c60d702be2518080c3", 192 | "label": "EncryptWithLabel", 193 | "plaintext": "d9f4f8a07e52230c13b55e5d930230421812ed5602d2bbc1b3c6eab80ac453fb6b36bd66054e11369f421d5db19703be5525bb0c30ba295a5cfe443cdbed99be", 194 | "priv": "0140731573ff1b849df07d9077d49242b2c1a8e9dc845fc38fc75c708be74bfa9f7c3f3f4294d6f4f309e9ad23fbaf8d308a483c4e6cc52483c192f7e0cb62b8f7d5", 195 | "pub": "0400d9488728d90e262fc67d8d4efbd76005dad0b29f695b075dbd1dcac0d792aafd4b550b3c92406e8efd97a9b5dc84b6534b4749febef436fbd974986649da38fae5017db4eadbee6600599cbb8d5be2c77b97f6c784fc5883d1c9207ab7157694e63189abec4b5198684089228700658093922c45a8d1d90d657f069282a94274531e1e" 196 | }, 197 | "expand_with_label": { 198 | "context": "5d40fba2326929a96e6b25731a26736b268b0cfc9c70a5fdb6797d3bc024ecb0c98ee22bbd77c743b9d295b5011b5febdbd48c73a299aacbc4d4555403fde017", 199 | "label": "ExpandWithLabel", 200 | "length": 32, 201 | "out": "fe1ad7af92705b6eeed83698913e13a43f229f4c22869d301d12aaccdb691375", 202 | "secret": "6d58f215bcd087e3d759cca1079ab0caa7f7f46cd877ac616fdcc1d7001beda434dfa1bc94db58da6cbc50f866aee57c9b8c03853335d10898a4d89d2c5362c1" 203 | }, 204 | "ref_hash": { 205 | "label": "RefHash", 206 | "out": "e41c93df994c4a6195b08e6155022aa8b0f85aa795fd90e789f20709fb6ca5b9a348a461e41d155f829e31beb2fee95046fcd25ff8aad476bd237b4a6d62e6ae", 207 | "value": "d033d0d1de66630c0eb70bffa7ad36a170fe00fe11fe1e595910c3ce7d2ab1ec21b6a6c1815d6bf6731248908ddcaaf15cbda9cb8cfc9be6a8b5746b12d2910d" 208 | }, 209 | "sign_with_label": { 210 | "content": "3ccae9578cddec157c33b42712316cd2b360fa1020c21aa763bff71c6116cf93ce9db1ee95028f85c6711675f340f36e1382cfedf59e5b8ae6d85b659cda883d", 211 | "label": "SignWithLabel", 212 | "priv": "0075aac2317c4cd22c101bee131cf4eae76960435356436c46ea76cc821f66e0c59db66f8f6524cddc621019079b57ec5f105def6c08dcfaa0122480a088a5c44b5e", 213 | "pub": "040049f6755dc251dea75df263286c9617cb676434c1167e14bd48222a15e10daa02b9251c432f31c59511e9d9a6132dc2e3a5ee0d001925f4cbf6c9aff007e38c61f601501a72177aa3d8836ee7adbce0ad5fedd59d578b1eb5d85dfadcae518a8a3d1549c625f47db54040da9325ad5e1273d5df18e99c2a6d03d974f404862bf2102054", 214 | "signature": "308186024160b6fe7407a52fb499a54e7a4b8a095c38c9f7f52dd18c3342692027592252fda87b3476d7f433a79c9426b05d66b1d58291e625912483e04581fba7cd64c63a920241760fe9615de08c694e7a443a33f2df2ee25f55527500616434ff5ba19778f3afac9d5b300a46cfb53cb317e7eb4f10679b09828edf62f529317a8c0d612103b1c5" 215 | } 216 | }, 217 | { 218 | "cipher_suite": 6, 219 | "derive_secret": { 220 | "label": "DeriveSecret", 221 | "out": "c8413f81c9f94350ff784f043f98ce8e49f6549eb14301c287e8d8f6d4449776da1006e706145a3e8cf222802bdaee170b540d8f5d7513e2440736d3c4f1d894", 222 | "secret": "082065745545cd8112e5385fa15321ad34e55c97971ec6ef4510720054d03ddfa610f04656996980256b171d25565fb7d34899c1e98db4f69614dfaf72e8838c" 223 | }, 224 | "derive_tree_secret": { 225 | "generation": 2694881440, 226 | "label": "DeriveTreeSecret", 227 | "length": 64, 228 | "out": "96245ee6e051ebeb58dbcc791e76ee88c09b83cb44bcb1c9d304499a5be7ed5b4d4480760708ed46c0876cd796abb8c05e7b249249d7ca71e41d60da7013d253", 229 | "secret": "141aad224cbe429c4dfd382e587c74d9577fcd428d0739e0670c7d3996d2182e55f4dd3f10902ef3524427db47af375e596727b57519f95a00d20426c35b2659" 230 | }, 231 | "encrypt_with_label": { 232 | "ciphertext": "1cf0b7459dd15bec64bf97584990ee9848dc5474a38557ba07ef17f5b9503be2adb47ec68df6ac4d9b11e6e6df0d64a60127d7caa109a894b58d7c3e9fba61b3ea2345fb65ae0413e9797580c6ab4a1a", 233 | "context": "edade7087f93e8c34b5949bfb7baeb71f32a0c39d1ccec077625381716bf551275b4ac51019ff290211d81160acf090a89000e3a72eb471a670bd8cbd7a34808", 234 | "kem_output": "d79688d9adb95186b5d475f5745f1e38905ce89fab7c6d890cf6fa257ed24681116e6dc6f3b86aafc47be6c4fd14614b5f05ffb0e64c29be", 235 | "label": "EncryptWithLabel", 236 | "plaintext": "749869e8f80f186e02881c59cf538ffc6de6d2a6c0e90e6b0362b14f37bd510a82b769ab1544d980c37f294ed68fc0cf7934c058921274908bf2f13f7a18166a", 237 | "priv": "34c86cc64b825321869ee81bcb38bd7baf01a1491fb52f671451b32ed7e89847f3968783a3e351b582b4520179b1a0d710c47b1442ac07f8", 238 | "pub": "0193786e5197b8ee594a0d2f9554907216e2923367113406c8664a7985df1f1e818e22224eb51a66eae888b527f3cd1ace7296f8e9109ed6" 239 | }, 240 | "expand_with_label": { 241 | "context": "20303270ec0b655674f8d8f7d736c27c55f0de1a6b8b098a2e73ca7010d43293e33c8d52887d1918e2b4bac75a633770a8677fbaaec8d6cc4bd00d3bc67dbedc", 242 | "label": "ExpandWithLabel", 243 | "length": 32, 244 | "out": "d57df8c0a0a746d0423f7cce96c90137013daed93c3d40967e18be44f535107c", 245 | "secret": "ecdee9131293e97eccd92c133d0b81bd5c311ea513195275f6ba711810dd19a687096c2021c270976595ca1095932bb837c0e98191e749dccb7fec4941d6d29b" 246 | }, 247 | "ref_hash": { 248 | "label": "RefHash", 249 | "out": "f7991c51bf10ad1e65529d49d6e61f45c0495730584842ebef42ad8e287e0a1af797b1b19fbb10fc60723ff92b18bb1aa1d883df258415cb33e434ce550b59f8", 250 | "value": "fd2cf8bba40e11a2a1853b7c311034dbef4ceff58352a71f55c722911cb3b6e8959601b05f11e1e9c20b7b56cf3d49df688c16a41b4f123142c073068007fd0f" 251 | }, 252 | "sign_with_label": { 253 | "content": "975cf2f26ea067f9d7bcb034d6fe7a28016f0a84e60e6178a1b7cb1f6d6381da17bb1c2681bf51a7d351cb6e5e40a0379ec2de285cb94964bb18a9373153a25e", 254 | "label": "SignWithLabel", 255 | "priv": "e1c591839b257097e473c5b4185fdfbb627ec85283fc0834302545000f8feb3d7a3d3105e8b1ae7af00c6fac145ee9329920254ac349c722c1", 256 | "pub": "2ea45851d68149bc5837eeec4db1d91e43e36ce3f4e3c2bd70e4a365f6915eec182173c5aed688cda8c266cd929d7b53bc9639f6c3592e7500", 257 | "signature": "a03644bf9dadd9c96977aeef5b891bd857318ccc3899bf05c525186c1a7b70e0bffc98a250b4c561b893670a897a3d6f97105debfbed421c80a9356b21a54b84d580fa86c52952e4791ef58ab54671a1f2ee25721c21925da57937c3b2d683b5b36b63de253abec596406b8485df91622d00" 258 | } 259 | }, 260 | { 261 | "cipher_suite": 7, 262 | "derive_secret": { 263 | "label": "DeriveSecret", 264 | "out": "ad64a2748a2894e725825fbb97d8e9e11b30c7df09056b825a9328d7dd95a35436c326929b964afb181c747c1e95c3da", 265 | "secret": "ee9426e58f77d9e2c16a18a8758097143645c523a1e3050c7e61c54b6703c721d3747961660a163230a03515f0ca0746" 266 | }, 267 | "derive_tree_secret": { 268 | "generation": 2694881440, 269 | "label": "DeriveTreeSecret", 270 | "length": 48, 271 | "out": "db3fd2753dc762875a927ed98e18f7cc68e143939a552252c2bebc1d42f78fb4413c12c2913c49e9dc0c9a85d913b8cf", 272 | "secret": "428ded6823d274edd5fee63ec1a9a5e18cf364f5a4fdf3efb21e927af2b8915d0ef28e48673f96054aae87cc59dd78b4" 273 | }, 274 | "encrypt_with_label": { 275 | "ciphertext": "f9caffe1b567c1860b70de5676c19c81b5927d9af22b3a0361cd936865b519d35f06e8f81dd85cb79cb6b43abdf944c2663f79f82677c71979ce7fc457e56d47", 276 | "context": "fb91549fe16bc4540788e61d4bc7f9704cc7af3657cc96ad2940500ab9fb749bbc63065477b336bb4b1c80d212536030", 277 | "kem_output": "04e7f79fb8cb58a43ad5897c5696d5ec6b2b5e8b84fd65fa24e5ef98aaccadcd95df930f04462ec378e501f60c79baeaf199f4f752cee808efd55e9a4ce32010f1624314eae209cde57a4a6c9c10b3a26c681e5555419994490d8829cf9c3bc96c", 278 | "label": "EncryptWithLabel", 279 | "plaintext": "fd137dab4ff57f2e9064a0fe2b2d8c72dd0f162d8ca3550e3e0561d5ee45a1e37494fc1f2547dd9f38977fc01ada7e50", 280 | "priv": "cc20f4f626228151e2f75d623198087e14cf8fecb5eb7e780bd6a177d31fafa295e6ea24e601d52c7fc4e7a9fb8907ff", 281 | "pub": "046b377162761a08807035865b8e6122beb118cb640df3468e30fe4249daaeb60e1ee9eff15bc9b95cc08839b35ff48b6848b12de26e96d671cab4926235739bd972ad08cd36574c22e884f3e9c761c3a28a9c8e140b887bb7460b8b657b57b28b" 282 | }, 283 | "expand_with_label": { 284 | "context": "2588d6b1ca306a9fc348a8b1b4de45c7111c4fb86ee362086ecf3f8f18f0b869df492fc5d30af6dd2d53e7b1eddf7d2f", 285 | "label": "ExpandWithLabel", 286 | "length": 32, 287 | "out": "a2b96cd5edc41d0d56ce7a2282ce52aa792c7990483a52cbb044ee3f1f14f159", 288 | "secret": "39bb10928726fffb2b9b61fa4b07fed7ef5e554021a83c9d3af3aac9a8abab3db0bae67df71e8b3dd7ab3cf17dc783ce" 289 | }, 290 | "ref_hash": { 291 | "label": "RefHash", 292 | "out": "d614f284ccf7854f081964581fb0fbe315a5e12cc3f10b929adfa1e94f06b35dbb28f4b4a8195d97c43ede073a8cabb4", 293 | "value": "bd9dfd5d309a2ff67a5a28ba0f75b365b52376626c372e115d30ebf3540858f83d0f70406e588d0a6c3e39d460f62074" 294 | }, 295 | "sign_with_label": { 296 | "content": "80e54bf4518194eb9034dade8753ed06b4763a2f7b1df80320b00d8e7c3d1f6181f56fbf663b272592ae6d90e30816fd", 297 | "label": "SignWithLabel", 298 | "priv": "3fa2573fe417aba24c2bf0df290c6cd4425ad12cd95ce81871b15aee5ae34f1c49531b9ca392f3fc6443c7917381b77e", 299 | "pub": "04fe2469e05e1fae9874891e3c05907e16eba323ed97680187ab8321504b78c3159a6747fb6dcbc600e7e95cfaf1c623be2a4e4a4855d7bd9a3104c6238397f526de1e61b62e48cac51a5a413ee435b6d15158100649c69d0abbaa4200f806334c", 300 | "signature": "3066023100c0847e70396150fdbe01fd2b192166ccfdfa398dd256855d6485c243235b32276b40ca285a1aa8adf7257732e8f25f47023100b2aa141a67e034d333b29690d705668350c637220836c29042a146383a07b2438ffa732afc272351cb3bddaf6a26afcb" 301 | } 302 | } 303 | ] 304 | -------------------------------------------------------------------------------- /testdata/deserialization.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "vlbytes_header": "00", 4 | "length": 0 5 | }, 6 | { 7 | "vlbytes_header": "0d", 8 | "length": 13 9 | }, 10 | { 11 | "vlbytes_header": "36", 12 | "length": 54 13 | }, 14 | { 15 | "vlbytes_header": "3f", 16 | "length": 63 17 | }, 18 | { 19 | "vlbytes_header": "4040", 20 | "length": 64 21 | }, 22 | { 23 | "vlbytes_header": "40ff", 24 | "length": 255 25 | }, 26 | { 27 | "vlbytes_header": "4185", 28 | "length": 389 29 | }, 30 | { 31 | "vlbytes_header": "4aaa", 32 | "length": 2730 33 | }, 34 | { 35 | "vlbytes_header": "4fff", 36 | "length": 4095 37 | }, 38 | { 39 | "vlbytes_header": "7fff", 40 | "length": 16383 41 | }, 42 | { 43 | "vlbytes_header": "80004000", 44 | "length": 16384 45 | }, 46 | { 47 | "vlbytes_header": "8000beef", 48 | "length": 48879 49 | }, 50 | { 51 | "vlbytes_header": "8000dead", 52 | "length": 57005 53 | }, 54 | { 55 | "vlbytes_header": "bfffffff", 56 | "length": 1073741823 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /testdata/message-protection.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "cipher_suite": 1, 4 | "group_id": "42e4c3a73738d838cb4f9dc550cb81406206943f9e6870ee150f2000ae8aa780", 5 | "epoch": 1184274, 6 | "tree_hash": "a257a326c7b632ce7ccdea8d3a3b5c3c2daa53a21029b3673ee05e9a3cea5934", 7 | "confirmed_transcript_hash": "59b10f7a137e853c4ef7ce43d2fe0a481bb80652f648c5efab11c3141a1ba60c", 8 | "signature_priv": "3091b9149866552f1bc8452aacd58b0a0b2ef87f2bb606d03ef6c06da0ca5cfc", 9 | "signature_pub": "62087f5b047e5292d5d29ded0977442788633c196a607988238b8c4374b4b4be", 10 | "encryption_secret": "4db33574f514024e61e2a15e71527182f62561d84b4d8230501b623d848df998", 11 | "sender_data_secret": "b21dfbbf69da2fec93299d8bd0795ad0aec22b42d83ff10a2e5e3f997672b8b3", 12 | "membership_key": "1f338b39337d019ab52c797bc836875998387e88fe711287df2258bc8f9967fb", 13 | "proposal": "000300000002", 14 | "proposal_priv": "000100022042e4c3a73738d838cb4f9dc550cb81406206943f9e6870ee150f2000ae8aa780000000000012121202001cf8f3a79aa9f3cbc60ea1e886e46dd6dcbcde71e80f7e46a68b88edeb4058b7761be40499be70b7a0b5f4e2b151b20250153fe6af4599853b1b351ad6c68a7d7490cd58301a3ed08b2bfc254feee2f14f63df0bafa7994b6957dc449384b06b6cd24bd228d8870d5ba8a346fcca8dcaacadd95959443c", 15 | "proposal_pub": "000100012042e4c3a73738d838cb4f9dc550cb81406206943f9e6870ee150f2000ae8aa7800000000000121212010000000100020003000000024040932e43773e992f7b0c8ec9d82f72c9395ad62120f19a764e50ec150ca48ba94dbb139a1f9c6901aab353d67f97f68210027818f634ac9cf8a738fe35b7404906201be6bf2e5e22e38a6f908d534a04ae231c3f6de72750d74bbf626a2c0bfcf1ec", 16 | "commit": "404601000401205ff3e28b8183cba127d0b74459b5d8a81286c299ba02d68c7d62f6101c3782dc20e0fbf69e4e80c4b2b8d43ffac9c0863a25300d0a6880aa9287d3916c467ae3ab00", 17 | "commit_priv": "000100022042e4c3a73738d838cb4f9dc550cb81406206943f9e6870ee150f2000ae8aa780000000000012121203001c97a2cc4f511ed694db9bd5ca4aeb06b5eb5fb92765f41feeece83a4040bc6c0e40ad742111fe68c17b0848c047f6909e0675dce492f4e2635fda105d20aac769b1b3916267a396235cbf955e1843072f62466f6c328f535bf7ecc326c224ae1b24c574ba543c0d1dc27328dfe10b221a43c29a0ea76e3e475980995e92cdf0c7b21bde1fcbb6bc5cc92b920b4b8bced424ccd518d2b7e738fe4c582040ef866cbe8afe72e76a8bae2e19e20a5c901d12baf43c2f01d98813a775ec5c6fdba27c8ccd1cc024dc95e4138bf8dd0135998c598240a0c87d5fe225b2", 18 | "commit_pub": "000100012042e4c3a73738d838cb4f9dc550cb81406206943f9e6870ee150f2000ae8aa780000000000012121201000000010003404601000401205ff3e28b8183cba127d0b74459b5d8a81286c299ba02d68c7d62f6101c3782dc20e0fbf69e4e80c4b2b8d43ffac9c0863a25300d0a6880aa9287d3916c467ae3ab0040403fe4fb435c134950173cf4244a899e3a3fedb6d77dde1279b8a1b03a507109661d82e6e08f9a476ebad8983ce83a1218dbf4df0b538e88d3d8ffb0cbc5fffb0120b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad2096f30e3de73f8f49037897e7e2ff6964857d7e662c6823cea2b5c4b78c3b03a2", 19 | "application": "a1ab266714fdb6d121f4c7f248271fb824a3e61dd3f91835e68fc8789f17f754a86233781fb59d23811b", 20 | "application_priv": "000100022042e4c3a73738d838cb4f9dc550cb81406206943f9e6870ee150f2000ae8aa780000000000012121201001cf18ad2e0ad4462390d72714d68b2a845ba336c3e8d398d21334f8d0f407dab47ce77c5fff3107b2f4e4bf99936c0ec680364333403663b7a955708a73d91e921a4fb3ffd50292a51ec31d4ec684231eb6f765cfbd5cca5b78fbe42bbf1b04dd1682fc87678b3a4f1599770839b171caff15e5e264957f3d16c31ba3ca16416f4665115244291536ddb438528cdb8502de654be9ca093915d36b1ab" 21 | }, 22 | { 23 | "cipher_suite": 2, 24 | "group_id": "e0b4d1bc9c0a23d09a816899c0fe01b06e8a1d72eacf0367c1f0f09a12730f36", 25 | "epoch": 1184274, 26 | "tree_hash": "eef060eca7674ec7f6fcaac9121425b96da8fd1aebd4332cc64943164079ea7d", 27 | "confirmed_transcript_hash": "e6ef10bc2d749d8837e3fcf26f52e4a0180e03047a25c01b29a26821f3c6801e", 28 | "signature_priv": "a95ae08cb5156432ae2e3f04cef1fc30b2cf5aa6bb0dc2abec68dd18ea0dd0cf", 29 | "signature_pub": "04e3426cca272cb55ffc026e1ce9fc58641cc8455c5e3aa4cb1e553a0581344b667efc72414c9013ea74cb0da9a38c771a80cac3597a9698650ee3bf0443929d7e", 30 | "encryption_secret": "ea4ab83fbd97640be9661375271b0b3bdaf9a131ebb97385b88877cdc7b05685", 31 | "sender_data_secret": "b884593d3fe0371a60324e3fc1e24a9400895fc4217eac1bc48ee63f82db76b8", 32 | "membership_key": "437af9098e9fe51afe4bb2e3fedd6fd83b6042156005dcfdaf189744f3c569d8", 33 | "proposal": "000300000002", 34 | "proposal_priv": "0001000220e0b4d1bc9c0a23d09a816899c0fe01b06e8a1d72eacf0367c1f0f09a12730f36000000000012121202001c9b3e51c35eb7d5e9af63d8979e8f3dcd87505fa84ac78302e2995cc04060cef258b9b4dd6c198e9358745711ecee01b0334dec1e89bbda05f191adfada2b8dfa999bdff963f814f01294eac45a787910552017b2fea83ca4663ed64ae379a916426d9d61951c6e74089d8c01c7fa74fd801cbe11d90661cd2b9fa9d59c3e", 35 | "proposal_pub": "0001000120e0b4d1bc9c0a23d09a816899c0fe01b06e8a1d72eacf0367c1f0f09a12730f360000000000121212010000000100020003000000024046304402203f0dcad38c6a291b51977ec59d3752432b6187877927ece1a905a187a4e0ed8b022051df5e37ba11dd99ae75116183473c6f4082a682bbe324361bc8656c982e613e20b50e0bce4d54bdc381aa6e72f4454049233c76a8b26210cf277f30986271fb94", 36 | "commit": "404601000401208785123b1533bdf9a4865fdddd6e223b4d629cd35e1b725472c6f3e7a0139ed120bc761ba5cc518fa1a77157f88b7e2b54e12d81f0268d772ac857191159529db900", 37 | "commit_priv": "0001000220e0b4d1bc9c0a23d09a816899c0fe01b06e8a1d72eacf0367c1f0f09a12730f36000000000012121203001c099e2e5218664fb5a71605a0bfb7658692c7c6873ff855ac28362dab40c3bb9fa5be21a2f67e4186c3290f27304e66b71cc7a5c15727afe8a594fd2753063f268e1ffbe659bafc32bedf6429899461c6d74fbe4e08c765aedf4f90721ae3690d815199b842597c2a85b55c510bda667f08e475d3d242ac3f2a724bca52a776a7f3c25a924bda2e591efb5bef07fc6277c28d04cff81a7b31bb3af65ac3848334150f4551f6201838732b15cc68cc0e216fc56853902e6db7165bc02d9d99da7074b79c6e1a1de307f2fa42d4da30284cafeab3963d4be30f0f3915c371b6227576", 38 | "commit_pub": "0001000120e0b4d1bc9c0a23d09a816899c0fe01b06e8a1d72eacf0367c1f0f09a12730f36000000000012121201000000010003404601000401208785123b1533bdf9a4865fdddd6e223b4d629cd35e1b725472c6f3e7a0139ed120bc761ba5cc518fa1a77157f88b7e2b54e12d81f0268d772ac857191159529db90040483046022100f34cdab7ba0aac6b624c7f3071d01f81d21959bd5f1f2affd2cd3ccf7cb9fdc4022100c8213303ee2aae421bb154703531ad874fc22e90d8a9118d91680225cdea807220b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad2046f571e58675e310ad13cf8a8b8d01a0da9350f654df6e7cc8a24d9d4f06c109", 39 | "application": "4010f2812003eda38e18ace515ce9edac4d53ec4c2c2beb2e28d5a9fe19766f8a1242a5d9e12a05fc62f", 40 | "application_priv": "0001000220e0b4d1bc9c0a23d09a816899c0fe01b06e8a1d72eacf0367c1f0f09a12730f36000000000012121201001ce3aa03e3a175dfa88af8a2a8d6007e0aeb0ea10fdc734132ad6bf8ee4084f252be09e4c23df5b6dfcbe3cfe9274cdb8f8cd603d594a4f7ba17395b99e18224bcb60f90ebc4d72bbf843efe45a1a97d8997290756ccfdc570e2b5800bf19791b5f4185ed4965a669395c946504bf1bb075780e47b43682862ba3a8d015cda88a2dce5bbaccf98eeeb3b676c99e938a11fb4743962e5d52dfac944bd82d0d423c1bd44" 41 | }, 42 | { 43 | "cipher_suite": 3, 44 | "group_id": "fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf", 45 | "epoch": 1184274, 46 | "tree_hash": "5ae6ee5f282786b753d55f15938248f6212c69b2f677319e5fcdb2ce0bfbf48a", 47 | "confirmed_transcript_hash": "0ba35f0d36007bc44df41db074b9d98a1314e3c690cb345ceeae152fbe5b6afe", 48 | "signature_priv": "d48bc8fcb54ad6e48eb55e773577030224af3a68308cd4a2a2c3f939a774c1a0", 49 | "signature_pub": "b1d7f2e45d6c94fa8bff3b63308bbba38024c5b70c22234834ee24294b6e0a28", 50 | "encryption_secret": "ce02c4dd80a8ed57503e3a50ec11b926a549d65e840417c0635427a838b3cf29", 51 | "sender_data_secret": "85fc31fac878dbe2850e30a8fa88bdc63dacd890756aade03433791b50a510f5", 52 | "membership_key": "82f73c829938c5d1a19e19de2b2c109214455ee6671cf918af426f364162678e", 53 | "proposal": "000300000002", 54 | "proposal_priv": "0001000220fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf000000000012121202001cc15e1b90a18448376191078a6b66567941ce2472f110def1215eb1114058f9bf6e2c2dd19ff11b450ce94d9990ca3dbaf94edf8873c0f7cbf8feeefd362ac674b32d4c1c6ca646b8282fec7aaa51dcdd402345792aa77d98eaa97da5bf13969e44845f33114c10e1edd765d1c738059757570366687b", 55 | "proposal_pub": "0001000120fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf00000000001212120100000001000200030000000240402aef68e55e2092800105a0a963c84afb2039093f5431f9c59fce19d3f2bca20689a617deb8fa59085f21b609649b0373e62c0c099e9bfc66f939e623a851d3022048451581d2796c68ca95ddbf06fe4cc69f5b1360f12dce5947e095ab26c259a3", 56 | "commit": "4046010004012017e89d9ff20be622654e2b82b1e55d5b51c9e01d3b02cf0fa179c14e8697c11620d3ad3e549ffe3fe157d4d2e6627b8d35be6b5b8f37ffb10ed358bfb47c6c829500", 57 | "commit_priv": "0001000220fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf000000000012121203001c71db9299c28f6e4feec4ddd63660f8ee186769ba51f2721953c48b9440bc32b3c5b162267da077973a9966c1c1bf6406d4326d9b0649bffeceec9ebe4dcf8603aa291254ce1954471cb01670b8e8e2f111ed54f8c818b348bd0300eb174602b484899f2fa95bbebdae6807de23355e2a988cf30cdeb2327759b2274205f485ff15b668b00a4f36ead53e21091be1133c2dd9a635bd6a942509921e33647e864a26af80aa5e54e15e695d2156209c2da2a7b8fe1938632192fce5e75736249c2fb6a7c2af46b29bfb5a83699d568f6b5614bf4edca36dc290f71d", 58 | "commit_pub": "0001000120fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf0000000000121212010000000100034046010004012017e89d9ff20be622654e2b82b1e55d5b51c9e01d3b02cf0fa179c14e8697c11620d3ad3e549ffe3fe157d4d2e6627b8d35be6b5b8f37ffb10ed358bfb47c6c82950040402ad9df6c6e068cfb07cd8a6e49455f256ca630341744128cdc2bd180c8dd76b8e2b3affc5e8c0b3a70d55e349e46adf95e2641299b6c1ac78336e69ec243860520b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad20aa0af8115702fcd155306e3f0091dbc40c772a97547ee9398157106ad10e9bf1", 59 | "application": "725b6517ff1e5ed74224de23777a9b5f4a2dd74fe4a816d19ea71baa069d74c0975b7f10e61b7c3ef339", 60 | "application_priv": "0001000220fdbada2ddce02e7493673caab793308bdeb5cdb0558f151f647ab6e2ca0c63cf000000000012121201001c22d286c12043af1e8093bd213bf707b9b675ee98a171d0eeb206de91407d9d099e17b2aa1de37065098feb8efa598721128ec5bada3a6a07e06123eb61ced1dd265a0e7468f05f39b549ede80606f4795425de91acaee0b0bd840d0eabd284ae30adf04a5bf97027180836adee1203636c2cfd36f477747e9f0549dc99f5b3ef3a3db4f4def426625757365cf871061383a6dbd516127a11f83cc4" 61 | }, 62 | { 63 | "cipher_suite": 4, 64 | "group_id": "79aceba7efc2fdbeb7dba9087d0c2317ef9a1150439b439979ca76564507822ba5aa20f035c4a866c4437b68401d62d8748940c3924ec07b03a10f27a599c362", 65 | "epoch": 1184274, 66 | "tree_hash": "644b7525c3a4693e3598988f4cfec0df372eeec860ed82d50356cdf66213041fd723d9e65b2cd7ba5a7551165addaf0902a2ece733cd8a3480466d5917243a71", 67 | "confirmed_transcript_hash": "fbe89de69aee2b43a93921e0809e3d11b305fb985d40f7346fc96197ce0f4c5c7c19460512c72e95ecda8d9158514bc6022f90275c9261119eacc68b5e2cb8ff", 68 | "signature_priv": "75fd38581c7eaada442fd0a89b71aa912ede2c77518ad8ac83dd3f5e6a994faf958c200ab68e4e84a4a5ffb8c3fece436101fbf161585b9710", 69 | "signature_pub": "d82d0cfbe1ca129087fc5e21307106b8455961ccb0fa52d4085ac5f0bd38bd01fd6aaafcfb91989a237be03c411b7c707c77e32047213b1d80", 70 | "encryption_secret": "299566ab0a25d7d246bc35366b23b4d5af3818682139d91123efb2e7ac44441396d9487c74a5a0754b6442cf794dd2c06c08870170ea78214e71b5e1c546a122", 71 | "sender_data_secret": "3f3d0a2186d16c03f98f8e200d68f7626278a68b6c908b8972def6b088432d7b5ea9d85856b78a9c871687b2d81295bc561cf166ff1480b99eb63b745905d145", 72 | "membership_key": "6c07d3a6c6d9db5c68f491352cd869cfed2cfb3d7e5044fce7e7dc91c0c812bf148c0f82c9e7de850a1daa9c6cb90f9b99a000358e163b97f34fef157f70a0f8", 73 | "proposal": "000300000002", 74 | "proposal_priv": "00010002404079aceba7efc2fdbeb7dba9087d0c2317ef9a1150439b439979ca76564507822ba5aa20f035c4a866c4437b68401d62d8748940c3924ec07b03a10f27a599c362000000000012121202001c4eae29575d125c662ee7387e5ef91247e9663d23239e79e6a66032e7408adfb433942a7cb5f14122b0274399a4ca571940f013e3cb4d26950e13211c92fcd0cf819ec01f144e3da9d407bfcb623904980e24a9b310df0fe1a72cade435b9711a87f7227c5f283179c01b43646485a370c74fc4ba0bd77166b6bde82803876714c420d8665ef10339bc7dff2809c897ef6c56c3b975cc793618c9ab54fde6a500f021856986bcfb8c", 75 | "proposal_pub": "00010001404079aceba7efc2fdbeb7dba9087d0c2317ef9a1150439b439979ca76564507822ba5aa20f035c4a866c4437b68401d62d8748940c3924ec07b03a10f27a599c3620000000000121212010000000100020003000000024072f2b2c084ce2ca92b1dc5bea63f0b1ebc3bf360e40119b18f1e9b39fd2dcf3f6add1d7bb92fd7ebbc08822e1406fad0deadc720706a4d9a668045c138eda32f4570bae9ebc5aa9cb4e2d24f2884d9b39b233328dd89a2d76fd073b95fb0d44e5d943c1d01bb8607d92054981a75edd2253400404039c8ed1865af67bd3ea9056479cc670bc010e0ecd58175f3fba6762d95b486b83b262442e0e76e1f32655445c7b83a531a7b395082a6d6dc2d58a731d6c46e8e", 76 | "commit": "40880100040140400f21ffbd22707e1411a09e26e524e9b551ff58b16e63049011cd6b18e1195581f1b2f3cc846ecc732c540124104a020a51311aa1ff75c2dbd752c08d2d874ce34040a44d40f39327ff9c3b7b60bc94224f3245353514b37715458883398667df5bc563c441650ca83affac2ff152c7fad1f699262b9b3fb254f7b6f40ffe8089a97400", 77 | "commit_priv": "00010002404079aceba7efc2fdbeb7dba9087d0c2317ef9a1150439b439979ca76564507822ba5aa20f035c4a866c4437b68401d62d8748940c3924ec07b03a10f27a599c362000000000012121203001cdfb59e66ac31671929477ef87e1d0b8a6672d6830c8d70d219b4925141517eddcc1daa6ff0d54b6f49cb141e89c5a2e7d6eee6883ed5ed0b1c2b371b14fc7aef7f0c0ebf7016a6ac65de558fe8782d53d251b31153fb993fceb91beb19e450b8dfe1f33242a61ac5aa1109ca11290282209cb49be2da2d3441331007da27bac3f49d381b6a347e67dc704dcddcd3509da2c341dbfee3ec4e077f68d264fce799cbcc408ffa00ee1aa352be71e261da2054dd29c27b7e8777fcd4ab051b20736a402d1e5d259aea8ed57fb98661b3215cd0db48696df7ae1df502575fe20994216a2214bb69d73e077b726b03feadf07c1fa3cdf623d3b1a4b7215321b3fd711697f7da293ba129ddb2ea65b1591712b3aa6626fd595b2e670f85c273ec516aa0052d215c7e8962c6ec6d9d2c682965fcadda725888df78fe09d6fad36c666ef596d12e53a515ff7b927ae2ff11bd95ecb8236e98501f58529ca9864d6549c849b7c970ebda7f0a0baf026b5ad5c70f", 78 | "commit_pub": "00010001404079aceba7efc2fdbeb7dba9087d0c2317ef9a1150439b439979ca76564507822ba5aa20f035c4a866c4437b68401d62d8748940c3924ec07b03a10f27a599c36200000000001212120100000001000340880100040140400f21ffbd22707e1411a09e26e524e9b551ff58b16e63049011cd6b18e1195581f1b2f3cc846ecc732c540124104a020a51311aa1ff75c2dbd752c08d2d874ce34040a44d40f39327ff9c3b7b60bc94224f3245353514b37715458883398667df5bc563c441650ca83affac2ff152c7fad1f699262b9b3fb254f7b6f40ffe8089a974004072de713163fd7af85b8d5f9ed5f0253fbbef29c5fec2d9c4b860de8068e5aae0cd15374226863f514d2d96a373d6edc247021023845778fea080fd6a1f8ff0d079a961ea48bd0d31d02ba2c9eb357aa5937146b4b2bfef80f03318e4dd3592f273e513127b967d5cfb7ed1e0ae6b6457a128004040b936cee86c9f87aa5d3c6f2e84cb5a4239a5fe50480a6ec66b70ab5b1f4ac6730c6c515421b327ec1d69402e53dfb49ad7381eb067b338fd7b0cb22247225d4740409ebd123a98b0cea3f591199e0ff2a256533eaddfe3dd66144abd4fe8c4718351718bb30fd2248c696f49621892160d2d52deb3994f18026792afb2a62a936b88", 79 | "application": "04c6e8e549e5e1e5ec7aff322272d4b6cfafe5171cbe601ab8d2d03fa6299cee38c1f06f1518fd1ede30", 80 | "application_priv": "00010002404079aceba7efc2fdbeb7dba9087d0c2317ef9a1150439b439979ca76564507822ba5aa20f035c4a866c4437b68401d62d8748940c3924ec07b03a10f27a599c362000000000012121201001c404d04dc5a3b315b5b3908750cebe651ff602f8bdfffa15b267f9c9240af3efb6032639bda6e1120e4f3d884c1b7048bdcbbb87c45c92ad93a174efa5a7bae84a9cba934f40c42456d13e6c9ec0b5b4b330e8ee5a89507931d7e94d89c3da73ad57dafa38e5aa0ea9e2a11a4fa79568373e4f9cc4bfad141848bd86d3515f4ea12b75fd166b87d70fd00dab18b6bc2482c21ce864e2ce44b377c436a71159bc504e8ecd7147003cd744d16551a2a574b1ce09738c897f14f7254a71814efb378efa96605ca059539d2db8344d0" 81 | }, 82 | { 83 | "cipher_suite": 5, 84 | "group_id": "06d7dc76a63f418e028f3897fb34fa5eeeddab2190070d67f77f36a6dc0116f3c0d7854c675d0a98940447d3449708106ae55942eb4fbdb16e6582b998e00dbb", 85 | "epoch": 1184274, 86 | "tree_hash": "59e530256271b91e4777df9e34c1d9857f6d91df100c501b91e1ace690b62d2f628919f3ea895c6e495037c7d2b068bacc0e9c4cefa001125b86035f488495a5", 87 | "confirmed_transcript_hash": "175683d8d7da0a26ce09a4ef129cf369848c6c6cd1e18175afd66d6ca26e8d23c3ea648b896487defb28078fed61c27f799f2943acaefd0924e67ad0fb965d21", 88 | "signature_priv": "0beee7d4e812a02538473225803aca13f8dea26718f188f2e1de8357a0037df621230cf4593885f282b858ac301e54c0643f5d07b6e85f237baa13b574000cd821", 89 | "signature_pub": "0400bd899c19f74c9200f2e7d1b88a681ad78df3bdec43bce09f08cafd7405b41709b618c9af3343d5adc2bd893be18b17825ca6ebe5deb647350274979dd8208cc863016b2cc8a367596139b384252184771083881d66557ca469945ac8d0d3af08c048ccffb51d44ed70efaf8cb8e238912a7751ed74a5e33bddc7cef0d6ec37422a9df2", 90 | "encryption_secret": "96a6068bdb5e3cc957e96864206152be593c8437b17ca369de4ed63ac801dd432a601a69732f7c234505dcaffb737e9b8041239605ef253614c6857383e73ea2", 91 | "sender_data_secret": "294caf7baafff902437743b87bf83b6ce1c35ca81b39e07c950731d57a0ec1576b9ec995364001b73ff9f11d4b40c479a9609024dcba0eeed2900f4e2ac1b496", 92 | "membership_key": "91b4d653b8397027a9da0589846b1a00622ebce84cae044e882a553f141297cb12637731ebdbd97971bdd9dd459a9168ac3f2c1b81f29d44e710afdbca9bd037", 93 | "proposal": "000300000002", 94 | "proposal_priv": "00010002404006d7dc76a63f418e028f3897fb34fa5eeeddab2190070d67f77f36a6dc0116f3c0d7854c675d0a98940447d3449708106ae55942eb4fbdb16e6582b998e00dbb000000000012121202001cb687027f36c530034a181574a6c29d30afd5b6414d967c242b50c62c40a330910d301b99ade6c0b9605fb044a22329b8ed17f4ee992187f1be91f10b5c2a91f4e52edd6da127b35ed1e211d18529a11fbdaadf6274cf591aaac6cf5094c3f0743ad10004c9610f803969ec0e9d10ff4c7effe1b7c2050684e7a19e756e27c22f35494a7de8d7d1386e8004340409e97b4871f5e09ed6968054c9348425e8b428839dd93b6caded910aac2e1efb4e38798256ad4f33d8c3eaaf1dcb994b7832d856", 95 | "proposal_pub": "00010001404006d7dc76a63f418e028f3897fb34fa5eeeddab2190070d67f77f36a6dc0116f3c0d7854c675d0a98940447d3449708106ae55942eb4fbdb16e6582b998e00dbb000000000012121201000000010002000300000002408a30818702410ba0d868b9225b75459e0b17a1e3e850eb9caaf312a92a85b1b144cb9b5fd88e09c98480b0c5ba765b7499cfb5079b18f9671404e08a7af009f58ee3754a0438c1024201ab76d80427da4b491a1289b1bcc4763c7cf5e695f20a2a3b2608c481d2a4cb6c865318e1ed0747098041c52bbdfc4e1597136dcab2703fe14875629bc2061a145c4040331599d99b1a0a12d4858fda021b5bf317cbb9171d82cf54a9c939e8bf4796ed34789d135f424a39474c0a63d0ac5a94693e2bd5864d31f10d4700d05b7fe91d", 96 | "commit": "40880100040140401d03b01592ec9cf5bf6f13b127fd83c4cc5e8a1e833d2a10abc08dea7920a57141c629839e6ea2e8a42c9907da63adbe1195381cd43ea7166b0a698c2be88b36404037226d4d2cf1ff02f71ffae949e5c546197ae80180736db45bfc0f565e988d925dac2bdd4acb1daa7f7a9939f564f9eedecf9d04523da477eff77709986e4cc100", 97 | "commit_priv": "00010002404006d7dc76a63f418e028f3897fb34fa5eeeddab2190070d67f77f36a6dc0116f3c0d7854c675d0a98940447d3449708106ae55942eb4fbdb16e6582b998e00dbb000000000012121203001ce0f113892b9d6b655f1dbf8ce9ebe346de19b4e094ad884fcdb07851416860c14d6b045fd6f2e941261afac2f2599bfaf8a8215901ba81ef52ab9f58448b08b94aa7b4a35fdfc377c34359d63dea85b9751c3d6aff9215dfded23717eb27eee429d24da72afdc9664546aa4f345bec15d298b8f967e26b0d10e16bd017ecd6428ee6cab4949145cdf4ab98edcc98b6996f2ee44308917ab5b954b22f078415c8c682865275b5f7be49db752cb906954f36d75f05c393cdc85cc04ecf1d97466edf8602f534329ff0e6d643ec7560a95050229979e24d374bddcb723a2b81ad3d3f5f7dc7ba668068536196f957d36f70c6a06acd5cd4ae3cee04cab49c070084aa27fa2c5f065d08900e5417c1e8b498a6edfd50db510310643420a51099f353f4c44530bd9f2f5dbc50b8acb7cb7b678a7500eaf85839651aa382aef550826f1c8e99088ed108f0dde64592b7e5b44e4a563e561393aa69804870fb57ec6b2d2739d0f3f0f3354892a758f385e351646335605028566cc5a4e989877fd7034693c4f1af3631", 98 | "commit_pub": "00010001404006d7dc76a63f418e028f3897fb34fa5eeeddab2190070d67f77f36a6dc0116f3c0d7854c675d0a98940447d3449708106ae55942eb4fbdb16e6582b998e00dbb00000000001212120100000001000340880100040140401d03b01592ec9cf5bf6f13b127fd83c4cc5e8a1e833d2a10abc08dea7920a57141c629839e6ea2e8a42c9907da63adbe1195381cd43ea7166b0a698c2be88b36404037226d4d2cf1ff02f71ffae949e5c546197ae80180736db45bfc0f565e988d925dac2bdd4acb1daa7f7a9939f564f9eedecf9d04523da477eff77709986e4cc100408a3081870241171109926aef64a5397152a83138545f0766c98e3b66506901a8189c50fe2a7ef1cff56e26cf0fac9698193b72d2a688c5ffbb908cdbb68fcb8f44c40017cbd10d02420177991d4444ce9c0e0d0ebde5915a51333df17af71c34dedc388ebb783edfb920618e6a80a6730d8e324125ec4cbe0b29b44b46e1e193682b01af988953d023b3294040b936cee86c9f87aa5d3c6f2e84cb5a4239a5fe50480a6ec66b70ab5b1f4ac6730c6c515421b327ec1d69402e53dfb49ad7381eb067b338fd7b0cb22247225d474040eaeb7535cad483bb9d3f9d32ce51502382ac6006bb50576278efd611ecf0f5e46136165d2fea1b0ad7ec9941e5f82c418e6c9e35c751c6cead0e2254e68d34ae", 99 | "application": "790a2cf292162a82b0380170120121d04450bbefcf39a7096387737d6d326173042de3d11a0982c321a5", 100 | "application_priv": "00010002404006d7dc76a63f418e028f3897fb34fa5eeeddab2190070d67f77f36a6dc0116f3c0d7854c675d0a98940447d3449708106ae55942eb4fbdb16e6582b998e00dbb000000000012121201001cc62179225aede4d46fe94b5738ed8c939cf812bc666d46f84addebb440c863aa47d154e06efe6da8cf25de9754c4223bea0b66b1b1b2d664776d74074974add73361918a8d28ddfadd7a44c00085bbe28f90513d7dcfb33ce80a5395f083e3b77c8330563c6d3ad0608cbac536314ea73e570ad1650a506217f71a944f0f250e03879d30d657ccb04e122b03a1ff4078958b16f64c5c835181c02dd8ab9b76b6529db4852d99770a5db75a6b4521ae9cc9ef24541245f47a7df2d6680a005e5a4386f74db4f98a1c45fbeaa9c74176ce0883cc2ce57b31ecaf7d581e6c6ec329936863cb9a49" 101 | }, 102 | { 103 | "cipher_suite": 6, 104 | "group_id": "de0215cb6a2ce86be0ed8f3821d8cd8f55f25392e95af652280a950541a7397b35d589ca21d2fc0ebd122e9112dcff3563a8dfd6c986cde072140b164d27e0f8", 105 | "epoch": 1184274, 106 | "tree_hash": "6071a6b389944ee8f41bc87f71f59632f4d3bb6904e4ad6735928a1e1b90dcb9ff59003d92da7a3a80637758116598056389c21dcdaa19a99a05c5b94e341bc9", 107 | "confirmed_transcript_hash": "34f4eb8fbc77abd139cac88c03a6063484ad5afa0e4f9ca34e7ddea7e071b29c0ad4185b22657ac2aab2965682ae7c3a31ede7aec472e431a1063d03e2ed907f", 108 | "signature_priv": "d47fea7274c6dd6172907d0ef4fb625fa3a9adc038aa089b8780783d2b986be7c21390e3ceca12a879ad8998d16ffb3fdcfabffc5f3ad278aa", 109 | "signature_pub": "ac06934afc423e39be6d71ef3c7f10c436f2ebe9f558023805ed9e2a40442203340a8b6cbd864df1ee9e084e9898625ec5d2ec37ccddfa8f80", 110 | "encryption_secret": "bcc9f3d6f4ae418eb1f804c1d1455452c59b65118dc09cf959c45bd4f8239a5a709f9da4f5f2df82fd7da499f7d59cfddbb527aa84911c4738b0fda40c037ef9", 111 | "sender_data_secret": "75654a7cb351b6b0e58b92df101dd51fe20d1364d5950cef091f0f74bed5dca5a1e11d76154abb99b92e867782e8f286bbcc3c0ee57905852a6db21eec6f011c", 112 | "membership_key": "c2b121ec0db354c57c61090a265100ce221225adc21461e04f518207c3fe000b68c7f282d494e1794463e6c076f4a8dd77c85583029118482bac639d3df78d76", 113 | "proposal": "000300000002", 114 | "proposal_priv": "000100024040de0215cb6a2ce86be0ed8f3821d8cd8f55f25392e95af652280a950541a7397b35d589ca21d2fc0ebd122e9112dcff3563a8dfd6c986cde072140b164d27e0f8000000000012121202001c85b71d3268547ccdf76dce7e3090decf5c5cc47c3986af15463f9f01408a9f613bb459dd515aac16b63ac5a1aa6b0a003d673aa7c34f5662a2605ac69d25db4dee562b8bb6cb6660f6b2d5d6007fe5fba7f928f7496b51fee1d44e971ae92951653e9a5081abba7f348c7588aafdf0b63b8fc509b1a8bd48ccc8d914c226304bd8cd61c2afff788b2d62ab66352806e0445af7f669b142ab18fc71582a5312a6c25b65cc2bb7e81a", 115 | "proposal_pub": "000100014040de0215cb6a2ce86be0ed8f3821d8cd8f55f25392e95af652280a950541a7397b35d589ca21d2fc0ebd122e9112dcff3563a8dfd6c986cde072140b164d27e0f80000000000121212010000000100020003000000024072414666b084038fde6af02cae3245809fab67f607d3b68df8dc49882419e0275f46dc73a667a7359a02346a04d74e45fb2550ee329ce6698e00ec3b4a37c14dc506bf02f0357028043721cbd4c499c8b09034c2fab6fc043efa78da88402f2d62cde8328b1fd2b5f074441f076b6c827310004040cbde10fd5c1cb9914342d083d928d6d998b9d4117172e9fab76abf46807f64701953f8f3670a9298bd67a3390e3c786398fa94cf17d2e91351c898b9a976c093", 116 | "commit": "4088010004014040fc76848bdf95a48ee26a8beca0ece18de5e0440f4848e3dc62392e3b311e963a8bebb5cff1e6b6211b0605dc446f460b995656f5bfe1915f693d632e82a293ec4040cdce799060485b09200b4ab02e06e236d3fd5801cc91e1470966463007406c332fdc7c84b09f303a7818f2ebde8c4696c11b64f274c1dd03fb3dfca9ab631f4e00", 117 | "commit_priv": "000100024040de0215cb6a2ce86be0ed8f3821d8cd8f55f25392e95af652280a950541a7397b35d589ca21d2fc0ebd122e9112dcff3563a8dfd6c986cde072140b164d27e0f8000000000012121203001c88b4dd76435bc83765fb42d2d03fe2cea60202ba241ce9a0329c0b144151bd325b50a834d383961c9853d5090c4c3f78a974a9b0a0a9423f40dc5f7b0587377a9ad5210d8ab92150ddf49dfeca4c0acfb1bb9e47a9d295d5245f0592a82d9e80f2f2852af8cfc4a9191bb5f654dd33eb67de876cf6dcb30cd3aed0f8a7cab3b42025d324e669bbf310accbf595f0ba5039f1b40cb35e3659d6c066dcc69246716efd1400957f9d54f2d07c1bd8636b18810603a261c14e62f3a9539056fae14f0e1cfb9cab90dc057c2eee92b43e41c83dcbee491ccbac82e9f87ae0193c798c652f0d63ef8a9b2efd5d502412f52a94409e7cc31994a445030040f111a84a9036b9dd7919fb3d7530518e91fa46a3fd027032938a7324745daa6ce199980622f9ea9a0a09531a41da0b9a311419ab4c9fdbf0dc42c31e721cd1e1b1a969ee99f1f823be54f964276cb55ae02288c345c1e1401e57da9c4544cd20c9dacef8e850bbf626d75e70037871c57da3e167", 118 | "commit_pub": "000100014040de0215cb6a2ce86be0ed8f3821d8cd8f55f25392e95af652280a950541a7397b35d589ca21d2fc0ebd122e9112dcff3563a8dfd6c986cde072140b164d27e0f80000000000121212010000000100034088010004014040fc76848bdf95a48ee26a8beca0ece18de5e0440f4848e3dc62392e3b311e963a8bebb5cff1e6b6211b0605dc446f460b995656f5bfe1915f693d632e82a293ec4040cdce799060485b09200b4ab02e06e236d3fd5801cc91e1470966463007406c332fdc7c84b09f303a7818f2ebde8c4696c11b64f274c1dd03fb3dfca9ab631f4e004072c2349e24d8201c95d17d28ffeb079b8067e1fbb6d32447f86ec1a37d6974403225f0cf1dc58e04cb427b3d4f6e4e07b3022ef35017fd615100747c233975e41a2305d3578ae06eaabe2148bc855bea89cb20dcd06bae6132d9a18368e8b7cc96ceac64cfa5811b7645ea4a032b4ca9993c004040b936cee86c9f87aa5d3c6f2e84cb5a4239a5fe50480a6ec66b70ab5b1f4ac6730c6c515421b327ec1d69402e53dfb49ad7381eb067b338fd7b0cb22247225d474040f48ec717a3dc0c656245c33828f70e9be31b8ef1e5fd9fe548ea4ea3f875fa0bbdef0d724333fb1e9e25abe67a40d134e88c63e96497e76994f75c9195533b62", 119 | "application": "170a35c0d98ce0db033548da15dcf2b1886903f5fcc38ed5217b2e2053de122fec8aa19e1df67d16604a", 120 | "application_priv": "000100024040de0215cb6a2ce86be0ed8f3821d8cd8f55f25392e95af652280a950541a7397b35d589ca21d2fc0ebd122e9112dcff3563a8dfd6c986cde072140b164d27e0f8000000000012121201001c8773ff6b24c5040928f456d51f11aecf5442a237d263b3d1eb7cc08640af132e5f2cd4ffcf040566bcdec15bdb2be8d5b039c9128a2407138c8cffbdd17539eb2227152000d11e43c173a869a570c6236786f5b08566f17486ba12e71e9726ed1914d363b61b394ea92d4020a1e3fe02e8e9522a5db857165abef43d0c8a69489da22c4cda5e95939fb54d76f3f4814b5a2a8a070542c17cf17acf99473d80cdce130d67fea9c16b5555c83fd9f845881409a4440366254f4d515fdb8e6c10703d22c9f0bf978391046499036f" 121 | }, 122 | { 123 | "cipher_suite": 7, 124 | "group_id": "80f486b08d22f42dc899d0d09ded745238129fefed0587d946af22dfb0dbe0cfff07b324ca059d50ae638c5cb0a7d0e9", 125 | "epoch": 1184274, 126 | "tree_hash": "801301618b8e2a4ed32c36f81eb44b98545031c9c2ca22c03248e2ab0afbf91bc9af6009ccc22aba3b4bbd2cbdfce690", 127 | "confirmed_transcript_hash": "190d1fa607662cbc066fd0179b5c9c6c6c041c97b7b55fe91c3b8e0161c2e7e6221cc6af243700cf9a51f224b4cc18f5", 128 | "signature_priv": "5525caacfdf8bf46c469f4293df21df31829cb9953e5e8c6e8bbb769ee5f56763fcfced38b361dc033346b3e2217464e", 129 | "signature_pub": "048a843a13a21a73e7c27b679ccb90d18f36af84cf442aa389c25f13f235b44e7db52e3f02a2359b7502a5f3db2ac28bee8c56419b7d65648364a60971616b5bb48b958c2b89ffe4609af62d51c6531766f216c4ab518e0adc096cd2f27b332016", 130 | "encryption_secret": "17c352d0dc64886b206c3bf6c9337846cbeba5a49d6c41ddcd35e9cf23c6bd3cc71893b825c84ed2ef0a47c7aee92309", 131 | "sender_data_secret": "db07bb890083a19e4bfa008d5bd9b0b4c299669648bc75af7a9ce0f66e7efe5ebb61ddb4f19db1119f0b7b4f2373c154", 132 | "membership_key": "36cd667f286c76823186a16b37a6b37eb7152ec49a8fec866627f00fbc2d003560643f4d458dbbee4693c378fd538d54", 133 | "proposal": "000300000002", 134 | "proposal_priv": "000100023080f486b08d22f42dc899d0d09ded745238129fefed0587d946af22dfb0dbe0cfff07b324ca059d50ae638c5cb0a7d0e9000000000012121202001cd98d935033c4b5f6400870e8f6fe09413257e394f19397866c061f3b4080112bbb973a6249f98b8323bafcf77de4881f98daa292d570991ab54e4872de2a050058443c99c3fbf094d8292c79314760220f185e1e547bd62dbf3e7b477b423d37e2dc6ff6300d4b596bc6e2f47564811d247a00ca2c50f72f2fbfae248129c9223b2c9b95e78eb8e95a96fef020013cbcbd3f64231c9f02861b8d21e9b4df", 135 | "proposal_pub": "000100013080f486b08d22f42dc899d0d09ded745238129fefed0587d946af22dfb0dbe0cfff07b324ca059d50ae638c5cb0a7d0e9000000000012121201000000010002000300000002406730650230211bf540e4de0bd6aee90f1156b80b7361c3b5fc9ba34d5a2a5ff239f0fdf948c73d08067ba4d49c1ca33d516b8aa5fc02310088429f2eab811854d5807b3bab4d507043e795d419f1150a273b9f234c118f588cfd13aaf735792f828ac6b861742b7f303b83c1d8fff040e4d767291ae7a0fa3f8e94272cf4fcf25045c9bf0167291cb2c938706462a1e5ed49c8a9d7503cdd92", 136 | "commit": "406601000401308925ac0a958285db9dcac4b8c483c9b4f251de6fef730371a19ab42cce445cdf6e0de9873809dc165f08d04be63f4ecb30e46aafa71f31d343d994b7106a30575740b10687463328f3a11eecd8ef9d8b67140cabbba7b65d4b0d31a6c60885e16600", 137 | "commit_priv": "000100023080f486b08d22f42dc899d0d09ded745238129fefed0587d946af22dfb0dbe0cfff07b324ca059d50ae638c5cb0a7d0e9000000000012121203001cbdc24924e0700952380843c3a9c0bd6b23c482352a9a9453ec324ec34114122b3b892f9c70cc75c3d17e1712ceaf9d84fc6ad6d7c00d3776451764e413250b831b8524e7ef0b7c156028e79d7e2eda2a9f2cfa3c8b78e4deb0c7db466a8fb737ece544af4431c6973f47f18e11a1e3c7c6bcf24ccf88a3f1550b8fd595f8cc74c82539dfd0e8a703d031ac36c28fe7b116fe5e6aa994828bcda1f62beabcc30b78694ed8f9eda7d1b26ca31dbe72a30a2fb2f363cd9ca9246f55c93a19ebf0af179e8d28c735d3f81fa43165685f1996912caa81fa842a5595e2d363fdab1c715889f519169b4bdf6cbc6281158403153ca30e12dced70e66d02933c850c60aed47c844905ec3e99c69532e4cdea2ca7be073d82350c7511a887a374f31c611248983dc31bd4609c2a0b7ff1b6590288a0f7", 138 | "commit_pub": "000100013080f486b08d22f42dc899d0d09ded745238129fefed0587d946af22dfb0dbe0cfff07b324ca059d50ae638c5cb0a7d0e9000000000012121201000000010003406601000401308925ac0a958285db9dcac4b8c483c9b4f251de6fef730371a19ab42cce445cdf6e0de9873809dc165f08d04be63f4ecb30e46aafa71f31d343d994b7106a30575740b10687463328f3a11eecd8ef9d8b67140cabbba7b65d4b0d31a6c60885e1660040673065023100b3e904ef38842006c2bcd842b96e19b8b9555acf9bcc41e66e25badc254d8bc14c8d5e6f4a5ec83e5ac2bbf84345e26b02303b844194e8d6a8ef1562de33e6a2c0eeb2074a77c6e94b2d0b8ad42d8673dcbe8b1417144fdb89b6b0b19860bc00d446306c1f2ee938fad2e24bd91298474382ca218c75db3d83e114b3d4367776d14d3551289e75e8209cd4b792302840234adc305dc48f171627f5d1eb1fe873c165679348110a8415ed00d774b86db84f62ad01b2229e6bcb7da0580003d27ba5fefb34", 139 | "application": "334360921f1209bd93357c184cd257de0d4799fbeb53a1c6f963cb0f40af432d4c17114a8d0c6f474e69", 140 | "application_priv": "000100023080f486b08d22f42dc899d0d09ded745238129fefed0587d946af22dfb0dbe0cfff07b324ca059d50ae638c5cb0a7d0e9000000000012121201001cd0223b73a6e92917fc3ac4d99f24a4d34070cf75d643e4ffbcb03cfa40a349baf2b5a257f6945cfc63c7c15b7e0de6302a7c707c41337e584bd2e72d61162d529eb92877cd6aae0b365f835254f25a0670be8b50a89bdda359151dfe9b9f059642e9b0951a3c90083735191d75f28df629e8e71386722c0c3756bd53c2d9706601eb8a4889d7ce79ca3bd17583ef80bc000313563f3b51ec2d2e57c9f19915fc710ddb02eccb9479c2d52c8579d009c130e30a83dc959766a3837c38541b9629dc" 141 | } 142 | ] -------------------------------------------------------------------------------- /testdata/transcript-hashes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "cipher_suite": 1, 4 | "confirmation_key": "26ae359a9a9760cd1066cae16f1456ea7eaf52bd85d5dba5bb15cca04a90c83f", 5 | "authenticated_content": "00010567726f7570000000000000345601000000000003220220e740a6faf2db65f5853148d75d9a335d7c4b94ab106fe5f237bc34fdcfc745840040402280c921b6b2836ed4eca496dfa74a13c171f78b80822894032c4c03cc4fb9c24e505c0d0ba4e8edb63a3327442baa12555a57f28e4cfebb4f0c42997030e207203a1d8805d8390f441a622f81bcb7347be86db7cc491d0313163fa63c2f66aa92", 6 | "interim_transcript_hash_before": "2597552c42df799cd70e99c77c4f80e834a9418096a0b2c7bc66afbc48eebaa6", 7 | "confirmed_transcript_hash_after": "51a85b21149c86f3f8c2907017c449e96987242b7ba2be9db1ddd53fb2db0d1d", 8 | "interim_transcript_hash_after": "193f9e11118fd08ff626069543b481ec5f04145680b612bb84d8962a2e609211" 9 | }, 10 | { 11 | "cipher_suite": 2, 12 | "confirmation_key": "6999e1655b7f4bdda3cf2991965d889a331b487526a9c99c19d1060e4d677996", 13 | "authenticated_content": "00010567726f7570000000000000345601000000000003220220e740a6faf2db65f5853148d75d9a335d7c4b94ab106fe5f237bc34fdcfc74584004046304402206f5ac008efb1d7edc106a27b4f3b71aa34821ca679543fd8bde8d728517b53bc0220223fb7226cc477e31ea25910d712fc915ce3df9f0399e0e7615babd593e2cdbb20fc804973ae28d04b9f3b71930414e29aa508f0711df720519f230e43a534b714", 14 | "interim_transcript_hash_before": "de0a78a0008b6c5c921c910d68da44abe0e692e1eea7e9f8226219ca34560f0d", 15 | "confirmed_transcript_hash_after": "e50ae43acf8ba84f712d8f48fa6ccd4768e48fad9c95feaf3061c54fe87a2779", 16 | "interim_transcript_hash_after": "87829eecb1aadfa10cbe2630fcc7ae9769d7fb2520f27b44ef76341c43ad834b" 17 | }, 18 | { 19 | "cipher_suite": 3, 20 | "confirmation_key": "45eb25e0f6024d7dca9a319bbb5d86d22d156613e6319507b54f4844f71bc858", 21 | "authenticated_content": "00010567726f7570000000000000345601000000000003220220e740a6faf2db65f5853148d75d9a335d7c4b94ab106fe5f237bc34fdcfc745840040406d0a302c5106ea9d31f3aec1d43df3fff47c1c0b059f4a0b1884798c5f6c973b4e3933b927b7e5f841813e1c14d8513c579b3662a3ce5e4c50adf53c4d60df022043646bfab3e0513b627b89997a2c3192afec8c9b6e9ef839dbb5fc7c0b90c9cd", 22 | "interim_transcript_hash_before": "0a56a7ac12b2fdab74b9356d100f5458dbce550c1fe69d43283001496fd08901", 23 | "confirmed_transcript_hash_after": "93e90c899f8a485dad5521123de058cefde35d5659521efd9b19c16fbdd8b42c", 24 | "interim_transcript_hash_after": "4184f0b59dc75aa1771efb5601124785ed6d590fe8cd58c398f4ccdbef46f3f0" 25 | }, 26 | { 27 | "cipher_suite": 4, 28 | "confirmation_key": "dba36f71716934f071b72fbe8d7a2cc226e3fc81490d88a9a0c91cffcc18667b4b97ecad4b2a0b3d85de08af0e1c1b152e9a60f45f6d3b8512b35949dc0cf9fe", 29 | "authenticated_content": "00010567726f75700000000000003456010000000000034043024040f2efb619b16c8a50925b397b427283ce18e4bd3f252eed2ebb1ed50f388be97000b9f502d2333ab89c69d1223a43fc519216e4df888d4a5060287d917ec92409004072b8b32fd646350886db3bc7fda8d1c3f2c1405eb836d2115a89d350e4dce9e10841051e8c3a530325e72db0a38a1154b95991d7833b3094ed80dd28d546412fa406dd133f281aafa0a40c9cae575d48ab1d568db6d10008704500ce934ec6b0be2c08e2fb49b8f96c692981644cf9ec4d3d004040b2ce522a8d03bf5a3f3a0e6b3ec0a10f6d31fab3868e5aabff0f0fc75110d5fc8e5e89175a7443c9f4d0329c25147793a1aa857c055f137e1f79c75c9a6d04a2", 30 | "interim_transcript_hash_before": "817d930807489c5338c69087bc43506f6a26b86ddb53635bb32b45a49a98e88959a35938262689f33ba673f902eeb7c59443565dac2cc9378bdb6e6e96432fda", 31 | "confirmed_transcript_hash_after": "970789a954c5c4780f48bba738f60222dc8378c916fc24bb6b76b40724aa7fb5d366ea15e2337173c307f657095e417f7dc3d9200ad1ebcd7cf13e5bb64d107b", 32 | "interim_transcript_hash_after": "29c66c251bfab15b12542cff0de7a43f319b3a6cd1a1392444bd9cfd3de3ff4b5c7b82c466bd98a548b6f7989ee010af0ef8dcca427dd088d6b575075b24af83" 33 | }, 34 | { 35 | "cipher_suite": 5, 36 | "confirmation_key": "5517432725b5ced63b9426a6a8bf8d2fa418687900ca43d76a7703427d480af24e5ead9a0f1c1fdbd3771fdf4e1ed66ada5dab6ae243ea01472b6a8e83aaf733", 37 | "authenticated_content": "00010567726f75700000000000003456010000000000034043024040f2efb619b16c8a50925b397b427283ce18e4bd3f252eed2ebb1ed50f388be97000b9f502d2333ab89c69d1223a43fc519216e4df888d4a5060287d917ec92409004089308186024170074d08546241ca8d2645057d9f2d37d33e558d10de8912e9162d5415f41049e63f55d97f752a5b7a36019e942d27fa2c198b4978b538167518af6544df653f8d02411e109db56bade664c91ba482d513d5deae4c98b012fd05b66071b4a2bebbc15af11f2ef14c2dcaa774ce2a6c707a139498774dbdf28c154631a5abfd84feb0c9c24040e4a8d5aa4d9587d97b26065a434c13e6aa270c414d255c8729965f051bbb56d0e6da7541e90bafef69414a5f1ba58d065e1e6ac71a2d00ae0ad7fd7306946dbc", 38 | "interim_transcript_hash_before": "4367aba662819b19bbfe567492f5ff12dc5f06f773e06f66f08f031c3ca4198813ba7a72d4ce3163cf4cba131812ba0ea086be5ed52ac8c8d36e9d0e1a5bacc7", 39 | "confirmed_transcript_hash_after": "5fb99f1a775dc2702a71203928794d1ae8c3afd493a682d9aaa247b5d35b4f38658654de80df6dcf6b5d853e413b4f37c4277b5e0fa812115b278c0b7013c5e1", 40 | "interim_transcript_hash_after": "20e58f40e6c5040ee89669ed8b61cb20fe614c81cbe3a955ae2324229cdb0e6457fc9e724dd831970f3b9d59cc46ad1630e2f17f49d746a0581ed3b067edc639" 41 | }, 42 | { 43 | "cipher_suite": 6, 44 | "confirmation_key": "b3fbb1585bf63169bf62b481074e64e825443856e7d2fa9423e4904c3614320a311ba3f88324fd0dace96ab59ffca820787c74a800bcb4cc4ca414549ccd68c7", 45 | "authenticated_content": "00010567726f75700000000000003456010000000000034043024040f2efb619b16c8a50925b397b427283ce18e4bd3f252eed2ebb1ed50f388be97000b9f502d2333ab89c69d1223a43fc519216e4df888d4a5060287d917ec924090040728d711701d873ed19f7758f59ee0055fb31e5ffa5a31f32e050d5f14f128e88f66f52bb367d78e9330ae9810e2e34124ef4604d0dfb393c8a006dfba06e2288e4c952f16cca1db029788b290d3a733c028a0bbe85d662bd6aed37c4223948a7ce483e4a922001c8e92ebd95c1cf3fe60601004040da60819cea3b7bf683767c2e9592ec3d6398c37d861728587ca6870c8e094b9aca370f87533e8ceee6d00929f507e4e5bcfd10bf7c60867be87c1055e5677ed2", 46 | "interim_transcript_hash_before": "33b97e89e3a6af01e6363be3ee5b65ed54ac6bf0e819575ce39d39dc8e84e4aefde0a43ae6317a56bf84d05a1ec87b775e91b68d2dcb5e6282b2bb03ff1b0554", 47 | "confirmed_transcript_hash_after": "2a4da3d1df04477e33689e7d393e8034963de31e28d3c42c842a9f6033738eee75a3aae311895d80da99a3fd09701327afa4edaf0e1247f92b1335767b9a401e", 48 | "interim_transcript_hash_after": "402f1c404fdbbe4a5aec512c8ad2a9cdfd200e592737e2eb223ef2fa6f824edcce9ebf0a6348610753263875e024db56d8efb6efd0de6d36ca3809b205a35b48" 49 | }, 50 | { 51 | "cipher_suite": 7, 52 | "confirmation_key": "92c24eeb327550191152ee1a87fa7824e1bb293b57406c8b9b1897fb4a8dd00c23c03e8b37155a02bbf326161a374983", 53 | "authenticated_content": "00010567726f75700000000000003456010000000000033202301422bfcd03851deea9586fd262d0ed10e7b755690244559ffc453f2feab08d5449c3e73416d08c374ae3b3774b8240eb004066306402302cc549d5eb36696c8c6b1ed4f6cc26d0107b211bd2033457e68ef98dd34f36bef6ea298e7161223558167ca1c2f2895402306dac15ca4e480ec158afda65097cf1069c41b73f513624ce2858791c725c1fd6241e945e56ee876f3d28cb0ac55e89a5300ec2d58ef75efda6c2c7edd5f2e18d47da5d0a7e1d8afd4853664efdff16f1463a5dd4d4d3ae764bf6b908b488908883", 54 | "interim_transcript_hash_before": "7fdf11509f74014de36a3ba22db62e33845459da72c3e44f59674df36456e36a3d96473dd63318ca772a8eaada76808d", 55 | "confirmed_transcript_hash_after": "ff5984f95a10075bd5fbb2e06620502c499ae65a2a07d53af940dbcac491c6dbdf7223f25a111b1a3dbfde12086277c9", 56 | "interim_transcript_hash_after": "da6d7bcc59becfb58511d14b511819d282ae1fe5d89a955216ad7ce367bd60ab0285e56005e9ab76f018ff381e97c880" 57 | } 58 | ] -------------------------------------------------------------------------------- /testdata/welcome.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "cipher_suite": 1, 4 | "init_priv": "c697caf7ae54f695f9411d9dc81a206b91fabcaac62a13763213a045ecd72b1e", 5 | "key_package": "00010005000100012028b2cd6417984dc4708c61a1cce7c0f11d181bd36d6f7a610ea21cb96f79ba6020275d9e6337b11a5e21ba755f2353053a500103efa1c5ac7c07d3a78f8817ad2d203de79c7e370156ce25a88d897a8ea7c8f90fea1f71fbeb5f31855312d8750007000120b640fbb0df8e646b29c83c5ed08aea89f72ab108922827ea76cd3b917d6d99420200010c00010002000300040005000600000400010002010000000000000000ffffffffffffffff004040fd81837a40a9ba774bb44db665081f4d0ff2a8f680ce5c902b17acc4ae6d9a14b9d4e9b4f8e7d74af8ff42032ec9caadf267e85931b550eebbe480150d4b9b0a0040401ec696ab731d5a7b1092b0db9912fe35086e188ce2946996bdf3cec463849f1a32f653b6e246b8b85a486ce3f604891501052c3d7bbee2155fff6a367e5a1f03", 6 | "signer_pub": "4e61ed19803e994259745f59aabd3f0be3c171ae99d49a29974b5a5cee134241", 7 | "welcome": "0001000300014076208e1faada70f08b91ef7f7f79ed1da917d9ce3cea5e5ce22e4a8b10f4311559dd20a87de170e9dc54bd4a8a48f38cd5c949f0cc82fce8ea72232417975ec6bad95033f6701d639694cbb51a4b2d0191f432add5267eea7b33f3c0c7edc65a28650adb0008f08b84a420bf1070516cb079a8e5c4159a40e80bee12b78b86d125155b035f52e8a131469cf1b9645d70e270d3aa21c04945fa80b7fea30ccfceb436e4df23558cdc1a6cd435db3199314795b7c488b4bf0855cb589ad9c7eb43ea8bc9edef6b85ad1c97451b706e5de27aabe664dca132a288b3fc091b9100e470fb506833aaa4ab279a44c92c21e34dd295b6e49978d8c93cf20537bebc1a467177500d7fe6b127d5b3d13bf038cd2e8ec00937db6fd4996b2f2e416b810d0822b77bd71b59bf1e486c1ad74da0de9872f839b63928a03ae11e4dfacb7cf27ea2c35ae233d9c63fe901ddd4e7be7e643912bb39ad8a728792753bc8314317388e" 8 | }, 9 | { 10 | "cipher_suite": 2, 11 | "init_priv": "0c627e5642c6a01adb63f130222b66eea352ebe47b85dfef57d123f7d17fcaf7", 12 | "key_package": "00010005000100024041049e8568620803fb6a37c3167e2e6df927a58e46905aa339d067fa05d9bfb33d6bb22250f44c24bf8441f1924f60f7f281236de1c99c98cbb8ca1f5a26edf286e04041045b2f23e87fdd51c407478878689f647cfeca1e6ddf13bab8315af9485d124943fd297144d1934525ed49397628b2ead0e6bc6e51aa1be12cf1b7c9f24765966f4041041ff15b03864ec390007b543c6e244468a46dcc57378d468722a267db7371c49cb0a9a2e32e864f292b25c29674d7edc37d637edbdf9b41ac8904dd8ca4ee77f1000120b640fbb0df8e646b29c83c5ed08aea89f72ab108922827ea76cd3b917d6d99420200010c00010002000300040005000600000400010002010000000000000000ffffffffffffffff0040483046022100a1168a2d80fb099aba2f983c5c3e344127f0d57e4b57b841ec2dfacdd1be629602210099422833715ad86ed402557d453e359f4c7c7a87dc2438f514047d6fcfb0b31a0040473045022100d973500369913a17440a0491c6119a50e0911d175b588f0cfde3ff41274aa9f5022043e06b3f7c32f2a40f68802b87743d6e1b0828efcedd182fe6363b45f9656e19", 13 | "signer_pub": "04b8d619186ae6aad30a2705941f354e317df3d83aba604c8a852d3db3c08e6cc7a226bcc5ec72be698727b3e27fd39f6fe4a624c3064d99f3967731b22fbfe330", 14 | "welcome": "000100030002409820e25365e70ce3dc73d96d38ff1969f3488e9999ab81403e26437c9332bf0f878d404104d0d237907f851105d0317a02e3bc53006a0632d1e36398d511cc9b8a0847d4397276473fe7183c7c997de9ccd5500d82735c179e03db75cacc82129c37dc796133a9331de12bd6fe7c212b6ade3b2967feff8ed72b1dfbad54b07d7a9c13c9afe00c1c47186af40b33a07670827404f7cd99e86e40ef0bee12b48b86d125155b035f52e8a131469cf1b9645d70e270d3aa21c04945fa80b7fea30ccfceb436e4df23558cdc1a6cd435db3199314795b7c488b4bf0855cb589ad9c7eb43ea8bc9edef6b85ad1c97451b706e5de27aabe664dca132a288b3fc091b9100e470fb506833aaa4ab279a4480b6e01e9d9b502537197c129b98aacbc52a7a440844c4fb153bf0ec32629eca3bf038cd2e89226e953f2cb36171d9f86df078e5bb12fabb90da79deac8a207986089add4da2dd2ad9c7bb5407c80fd1e0e89ece6c328ee9bf27e79a56961e794b4b85d01befdde8e4255a80a62e58ac2b668485b9b3876afdc51e5a63" 15 | }, 16 | { 17 | "cipher_suite": 3, 18 | "init_priv": "c697caf7ae54f695f9411d9dc81a206b91fabcaac62a13763213a045ecd72b1e", 19 | "key_package": "00010005000100032028b2cd6417984dc4708c61a1cce7c0f11d181bd36d6f7a610ea21cb96f79ba6020275d9e6337b11a5e21ba755f2353053a500103efa1c5ac7c07d3a78f8817ad2d203de79c7e370156ce25a88d897a8ea7c8f90fea1f71fbeb5f31855312d8750007000120b640fbb0df8e646b29c83c5ed08aea89f72ab108922827ea76cd3b917d6d99420200010c00010002000300040005000600000400010002010000000000000000ffffffffffffffff004040fd81837a40a9ba774bb44db665081f4d0ff2a8f680ce5c902b17acc4ae6d9a14b9d4e9b4f8e7d74af8ff42032ec9caadf267e85931b550eebbe480150d4b9b0a0040403a9c005610287480d74ece89dba6696a3c1bdb4ffe0da6c60824ad7ff3809e630fde475d1f24e3cb9ea6533367ce8654ab63b59875b905ead1af05bfec282e0e", 20 | "signer_pub": "4e61ed19803e994259745f59aabd3f0be3c171ae99d49a29974b5a5cee134241", 21 | "welcome": "000100030003407620f5c79ed89f7806b7da95df92ff6c760601eceda0d7017b82d69a9df7727d8b4320d2481a99ac83f36f552ab3176394eb739a3985fbac1a62fb5bff9ca4e4964d3833d3073bc3428958710169b048c854cefe52554e88c28a7e6e82c7469b4fd30239fe03a09bb811811162622f11f8b2aa741f63a240e825347868a893c650fa48d34eac9c586c49e41fc33c420e5bf33f6be1aa9acff5b551877391a5f56e29c73e141e2d97e2233bece24f7ca62e89b564722a99898e809c5879f7de08174418315c439078fe8e36765931bd0287043ab543350b53c910ac8e2186ab7a3ca29567e642acf439bafd5181f98c3bf8b3474dd7352893b55977f5c951ae0a40ce602b21c1c8e6d31bd3daff016b6deee26b1eb60bed7637fd791fc3f9b3f508a85945c5bcb78a092f1a3ab71bd1dfc3d3cf96c6b781b6bbe585b40b9b0f03017e9d80bd42088b371a0425367e9bb3e4844572510d8fc89109c85ade70809bd9" 22 | }, 23 | { 24 | "cipher_suite": 4, 25 | "init_priv": "82ed1d24a7ec333e8496e2360d27a64f79cd823e1e887b62e5a406b1f4f39522a5fb7eb2952f013e272dbfe270d4b3cf87a7be304bea7fea", 26 | "key_package": "000100050001000438910899bb8cf5119db62fa7623e2d10b4acead1642d2771076d47e636fafb030a2b13115be8e40fb5ec5a6f2f6bb7fee4932a59db049ea249386fe5b472882ffdf2d66f13e2ffe1e2adffe922590c41db4f2475c6b26970b0ed8818b1b32a98836c1e992a897bcd022926cca3a5d95e365239dd74bf8437798c93b47a329dcfc97abe22f1d6b0ac9323c40253da4ebd2754cb0a0a49509763e19024c5d7c26c45159f11693fc95657fe58800001404059974da22a85557c27beb8e7c1945d8b0359dab007cdb2743bf65a50f683f0eeee79aefab0fa9586ea367aa5d74913d99fb5b02aa270c43bf9a7102aa4f3db720200010c00010002000300040005000600000400010002010000000000000000ffffffffffffffff0040725ac121de82eecd7d1cea387987370a56dd69b70240e7b4893d61606be5bbff6071f4979dfacf5fee8b98f2fa9851cb190ac16dc078272c3c006cc79ae5e6dd64486df7ab19b46d05d8a5b8066b494fe4a3c50111acb11c1d1131f38d929f216b7963796ead44c53738ddf15f3b44ad1a1b0000407279023014eb7c341ce99f996ac6ab73e128ada328832880771995d1951c80846f583348dad19f2270a858df8c48da5ef8712df78a749e4d7b005cf3ca330515a654aa9bb1a9d7323d32ec57ea148f7c21491da4f60ac96e5ba986615b3d4f6d82001af5783c0f46afe931947a91e222e70400", 27 | "signer_pub": "0b07a86c796f9d8c8128aaa9a5294a7ffea59c5bb6eaab0d9c375c1af1db5cfca994bd5ae8e2993083a183add79518450e5dde2da7b6a87500", 28 | "welcome": "00010003000440d14040983a8117c3f7a804ea63072f19fc511103baa666c87c3ad2a31760d3ee728344426335093aeb8dd21447f94e5752d2be430aa39160df31c2fcb50e1d7b4f253438d3590611fde219d2afa77f6028c17a3763fdf504f12f639e6fd22223790b56bcbc3b92fc8cdf34c35736e21f91370e2b08d1c25edaceff5440541e1bdfcf9dfd47d1a747308315dfc26922f6b5ea2039469ad27dfb9f26992586d1fbfa2d8ab1057c373fefb6811f5c55be17e131acbca24e7563ef88932994481ec3a8299542d45d70509c3136d2f0a9819c882e419e7050dd77c949afcf03265f4970293d8ae95d5daade528d62c0acbe2e6a84e2f6f654e62ac10f36dd77333f2ef126376d1bc52c92e7e41ca17cfc5495ca43f84f5224c4f739f3cc4b80e6cf23bde2cb77965f54c325d7048de320241e4e6b3764768fca2e662bcf376f3e667a3619961bac87d6895a09cce256c815c60a11bf09670ee69bbc4d0ace8958a6b70de98cbcd736da22bb841ffd9ecd546e429b0bc318f9d158197e338908236ddf5aff3bc7200a2a5d8aee0ab67c27aaf5a86c3970b0a32f7b1d2a55ee4a9dfbccfbf83c7d2d447f0f43d31e3f148c4637bbdded9c0ad183d862ef8874aa9d030c47744f0c8ec1f7677f824573799d56735b1c3ac6586a2e8408ac3fe535e104844c3917279ff64043c601ba88c198671a229123a6ed7f020624c45082d4e20bff6ace3b2047b7b423cf666dd2a60bee23f7da034e1fdbccc52594db2df1354ecefa4745a0a65bad71f574cabded05c5d415c10d133420433cdcfb862607a65e82c235f55b3c6c2b75aa31ac6bb02189d41c820aafb092c1f30e3b82419f3c4f380e7b90f0deb1de8efab6fe77f3915cf84080" 29 | }, 30 | { 31 | "cipher_suite": 5, 32 | "init_priv": "0111cc38e3c7fee8090bee1f46038c27c5505d8bec0027c1c691d6a93347dfe4a9e3a645949b88a4e75144bebf1ac488f6fbcc488545056c3d013e4d260e971c35ac", 33 | "key_package": "00010005000100054085040148c2b2f048ed84298e707c89577d19e82a50eca5282fb3381cae250d0fff4ffcddb8e404e7254ddb8f1a8a13a18a4915fe485bc5a54a447b397a0a5ea4142e94bc00820ca02e900d8720761b54dea276dc376d110bed4e3645e0e1fdbcc68ed25c1aee33103c2927c53c792d7b762fa93e82d279a4ee81cd4085b26e4d3151ac498e25408504005b14115f7afb69fd5462e97ca7d6cbd09ada7237f80cd6909ef4fb8b5635f2d07fbf937cab33e6a6ddcaf276801b6df820f2b4ebc30789a1f0097a35d01ebf22a8014b0bb1988c04f89710c25a62ab11ca56ef2273810f2767cdf1fa8efb82937c02cf86fee8ba94896b7bf8d3cd1104e61c0313e3b6c46551621617dc21e891a34ed940850401a6fbe44f7f569e8a7fa7eeb176b959f31dbd5ee227de286b4c19e39826516a1ee706aaceede38c12f1daa91016f24aa8af09263f528f8bcbe1c3fa3b4da4840f3401673ebc939d22f21ed3f94636e14afa2972cc66dc844a93e3493feee74b029fd8044955676161b48c4e5c211d9557ff08b8dd6d0af7566a1bfded9347c5984b87220001404059974da22a85557c27beb8e7c1945d8b0359dab007cdb2743bf65a50f683f0eeee79aefab0fa9586ea367aa5d74913d99fb5b02aa270c43bf9a7102aa4f3db720200010c00010002000300040005000600000400010002010000000000000000ffffffffffffffff00408a30818702417a64eb65471f28eaaa8daadedf50fb3eda8db1661fda7806a8ce45ef095fcc831801ecb05564750318c4a8c5e5c9ee5035a69e03aa5fd0de64614215ff0a7cea4c024201df2c050066db2de08fa0f2eb1b74e90df439646d383b1a28d8f3e98d83ac88dc8bb989dbd91637b1bf0a4aa835d2484933904200b79b66cf6aa2ce09f8e09b595b00408a30818702416257b60199249ffb916d6319c408427bad7866fa38f48a0770ea9f2d42bd7ac3f10f6ce4e121d1a8681b40391eef159a801f160aa322d08631c12984ae668d6f1a024200a366b53a99fa6d82729267707dddbded573814fcef6f4f8105974a7e59e210b64b1561f8f39776d7da05c0fd9561932020cebfb76f676c18c84c207edd543a3333", 34 | "signer_pub": "0401ce4a76486a29feace1b62a62729bfa21a8e9ffe7588a2e9c5b5072c9c3c558bc85ed2a52ccfa3eb55e6a035299dcabb3aa359f8ccd0a9dc39f9d1ddd57d5e6dd5b007775fabb2a8e61f67333101a50da312f4d81254d23aa977732c124126b48a0472fa368f3ba94b2d4c7cf72e97c13f004b470afcc6410015a59c5952b1d8c7cf527", 35 | "welcome": "000100030005411f40407d873cae97db858cefd043ec490b4435d81f2d66efb219778c5d9094bddbd1fa5427181068418a106027e993a553b9d60d315ac8ab85f31e5853eb7efc450bc7408504016c79a3d9edd1127d9fc499ed48c94eeedd3e9ac1d2038f2baac5ea11dc112c7d3704fb858a13e643a995c3ab31473d7e5b115525ba0878f6049a00a27d8ad39cf6000d542a91e87d007dd627ee6ad16d5475b8bb99fb8081c11a48d3cac3e8ee7ec449b0ca28e6f4ce9cba97c32a6331646b8b98de67d19835884c49780c0c2ae215fd4054ee60e0cd85c86655553cce7c2f5fe31eb55c4790e958dcc0d5cce421ad44d50808be2fa6c740e2ba0532144972acf9f0289a990c0b3c8d76dc35ee3d59db75d1140067113c5aa7b3883c5b7467fc9a791d51001141b77050dd76c949afcf03265f4970293d8ae95d5daade528d62c0acbe2e6a84e2f6f654e62ac10f36dd77333f2ef126376d1bc52c92e7e41ca17cfc5495ca43f84f5224c4f739f3cc4b80e6cf23bde2cb77965f54c325d7048de320241e4e6b3764768fca2e662bcf376f3e667a3619961bac87d6895a09cce256c815c60a11bf09670ee69bbc4d0ace8958a6b70de98cbcd736da22bb841ffd9ecd546e429b0bc318f9d158197e338908236ddf5aff3bc7200a2a5d8aee0ab67c27aaf5a86c3970b0a32f7b1d2a55ee4a9dfbccfbf83c7d2d447f0f43d3d2d93b35da1ebbe96a03efbfb9991e61c456e50e308d64b704db92455d81e8b194f434186d7085972e22490dee11062523911ed59fc08aea260838257245e4cbba88c19867e32e3511114999a76b57326fb038601d387dde7f6959b995685c2b77b69e77ca98c0f046d01a3e6009229dfdd9a2caf8e23aefed301302744bd30fb964a50e746d283baff9295664f5c9cf5db7e434b88095c211dc89a08fb3efe57ef38ffa4db3840e1878623b66f109b500609519ea58472da7b08aba291f62cb7808396125e8635ef244737f6a9313ad4311348581c6dd6cb1886318027b6d9c97" 36 | }, 37 | { 38 | "cipher_suite": 6, 39 | "init_priv": "82ed1d24a7ec333e8496e2360d27a64f79cd823e1e887b62e5a406b1f4f39522a5fb7eb2952f013e272dbfe270d4b3cf87a7be304bea7fea", 40 | "key_package": "000100050001000638910899bb8cf5119db62fa7623e2d10b4acead1642d2771076d47e636fafb030a2b13115be8e40fb5ec5a6f2f6bb7fee4932a59db049ea249386fe5b472882ffdf2d66f13e2ffe1e2adffe922590c41db4f2475c6b26970b0ed8818b1b32a98836c1e992a897bcd022926cca3a5d95e365239dd74bf8437798c93b47a329dcfc97abe22f1d6b0ac9323c40253da4ebd2754cb0a0a49509763e19024c5d7c26c45159f11693fc95657fe58800001404059974da22a85557c27beb8e7c1945d8b0359dab007cdb2743bf65a50f683f0eeee79aefab0fa9586ea367aa5d74913d99fb5b02aa270c43bf9a7102aa4f3db720200010c00010002000300040005000600000400010002010000000000000000ffffffffffffffff0040725ac121de82eecd7d1cea387987370a56dd69b70240e7b4893d61606be5bbff6071f4979dfacf5fee8b98f2fa9851cb190ac16dc078272c3c006cc79ae5e6dd64486df7ab19b46d05d8a5b8066b494fe4a3c50111acb11c1d1131f38d929f216b7963796ead44c53738ddf15f3b44ad1a1b000040729e458858112377e7706f45a51e8c439495bd0a8535131304e20967998d230768f807df40d6ae704dc2f3d07a11b90dcee22fc5bdef76ef8f805d1bd0c9a05ff160fd99fa150139a1bd4b160cc227d69d9f3684dd4c597936b6d26941bdf56f89b6e72b8776951306dc99e1f96ff8c9772500", 41 | "signer_pub": "0b07a86c796f9d8c8128aaa9a5294a7ffea59c5bb6eaab0d9c375c1af1db5cfca994bd5ae8e2993083a183add79518450e5dde2da7b6a87500", 42 | "welcome": "00010003000640d14040007583d04d617dd7105f4fb76050546c4a899927ae5454f3067145f81c2efea49943e6a9f16cb6b5f1a7e1d1d30985499222651938e9f08cbe653428db33c9f13834398cca168edada2f2718cd1d07dd3da9ca372fdf4c210000de9ba8abb9947d7b6a6ea44e1e9e4364abc5bdffeb131da40b12c0d3096b2d405488c4d9373f8d280a983dc338faa2ef011da00eea51c731ebd2a99e8510dd6a041b8c41a608fcd3a8fcf53fa0ebbc355859a748bcb66a0b976aba442650f36bc5d06abd7d2808ad362c18c26eeb6aa6d1779359dd419e284d08d3bdc800da00a59e82dabed450a4b22cfcbcdf3ed1aae03428e27748ad87966257cfcb20fde56cb8ad62253dbbd278f7ecb93d34502dc21dce5239a186ec4bac865b058a2524de0dbce9db3721bfb36c1b15bba068ee172eb5c177014a318af8d2762f706f6c76c77dc40c894971d778c4f8d4094f5816c81a00c04165e5881d74ed03fb4f22a883d1b99773789e4b53109233eb116de5819cde33057fd556d5eb2fe741b2c86975b6001bf7de4051486572433f3e291453fef42c8153b4252b6fe3f33b5fd3a89d57ea84a128a392c17d1cc4fafa5a32eedf24a5810a24c8fb36383f17bbb66f9b84d86329a591d80357b86a4c8fa571499b9e29a29028f65a39868d81c27c915ebdc7b828bad04c97fa90d10812ff0516a4d363ae2b2f8d8a2e6b6aaa1fbb155d1f9fad947a045117b0f5cd85a27d01b1d526464b86aae5789316d95e7123a6e0c9e16262f585fb2e4f96c2fbfdafce4dc054e99c32308ceb9a6bcab7f4a4db1a8400ee8988c7edfa91b4ed02f5eab4bcab693d4c929b2c23d0dd72a98cc51fad5f15f1da78a461072b0cda41729fee88dc7917" 43 | }, 44 | { 45 | "cipher_suite": 7, 46 | "init_priv": "7030615798d67f97665e9aeb8ac61fb1ef7311a92e5b6e6d97f3f4d9e57dd39537ce886f48d928e09a694c7e0ebaf449", 47 | "key_package": "00010005000100074061043ddeaa6c7571b023bb673ae4883f4110f1bc818fdc67319f02686618761d605db4ae669c0e1c7da064314d9b8acc004a525c2598fc71a73921863cf41af8dcf1ea5e7b1995b649d2d13c9d527411a6884c6c2eba4ae565e126c934a7e8ef5cc7406104519f1802f27c548f9e60d59a6bb6a88f77f91d7f7f4dc0eab34625458a608b0ce17ae1df9098f56eb64ad8ff01a2d1cd6526c2c8db886e5f719c0702fee316d4b8eda193b0e4bfb5edd29b8ca516714ed9c4a2917ad168bc08f13253e2de33d3406104369a04073d00527522f2e284e913463b462f2a589a3a6d879d5881d1e77e845d73eef72e1ea0c1ba55654e66264721e3fb7f8c713163b8faad02f038f8692aad424cd46e32076b795b26e7e8b24ce375128bacff50f37606bd1b969b56199760000130970ef250f11996c7e9cf3171cf592d120f4d99be75df24a28a540520a63d30c8a739ffaf595ff0d851f5befd141d4b2a0200010c00010002000300040005000600000400010002010000000000000000ffffffffffffffff0040683066023100e2acc54499a988546254c8299550f1018cbadce04ee780f2757b5cb2b28ddef4a2f73f5bb3a27dd97929189e39cbbbf802310096c395dd46fb0125d8bb936ffe292c2e7f81329ca24d06fc0df0acf7828ec8d97bf938c63e2f84694c612e640f495bbe004067306502304204f822763edffa411add139d299fbc93d5eae617abbb511408bd15aac789d1c28b70bc598f10cdd50e85bc64941a470231009f3ce922e457040d0adf9e907127fbfefc67f462bb1eee4ca6b1b01dba2ab4a7c020ead5492d73e30be8c5bb6cae2938", 48 | "signer_pub": "0412843440f67d9a5dbe06e37060f8410670ec238bb3f46536e20818babba3080af635e39bf64f84ac0acd9618d3c7087574cbfa4cf7a18dbeb80f33c46ac7f127dfff89abf35b82852dd7beb1f803d790d49c7cc69c9eacd7d61966cc517ed8e5", 49 | "welcome": "00010003000740d930d63c1435d25c71f3e2600ab484fde1598262f3fcb0c3ff1e02ae3352c87fefb0c2179131339a08232acc085c16466a0d4061044b0a44c28b8bb5db12651d91de546579709a51549166e3725ea6c9a68d685f7973f82559d80fcaaad9dbdf26b22edc7a606182faf5cc1a6e75a347184812d7d5092a3b626ad2d3072ae6890cce4c493955ec924d10c51e1f796a8fd001a3cd8c4043f85be20a425e86e8855babdb2da79391c575a0f6092b96f976f3dbb1b7fb98b07a47b2c118cca40945c46e3288cd73760ec91f08a05668aaba8002b67ac8e5cdf09b5d414e4018c0587a230e5a77927dc24359de62a96fd70a352cfb7e1d9ba4853e6b37ae487f1cc56c92fd8310acceb81914f7de6a23fca29760978f40c7187cdbc0c5ea025474d83e5aee96b9e03e866ad747d6ce0f54330adc0ac152fc43d0c55f4bb30982c96f8e4c919d499f77afc48400b557d0be47436f27bb0ff2dbc607129c5d0daf87bf9a8a659da1fedb29a333c1caed2a34359ee3bd7fdc313fe212a12b02fd13efe79ca8eba2a0daf0248466f030e2fb898c2aec9895eba37fc6956326e4cc73174d1ce033a05a19a070b0f96772a5352a0b3825dbd193906af61fad7fc7bf3f8fd77c8bbd29fcffb483aefe749ffbc08389b5ba1474f9c58070693b0b47eb2b2ee4a5fe25f0d50fcc37fdc09e48de694e973f219fa0925ee9c0f34294dbb51623c6eba28de3f7139a2f25214e7c9d50f162bdcd5442da317b89c86c5e275bc32f61623478e42d9751c36da8" 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /tree_math.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | // This package uses an array-based representation of complete balanced binary 4 | // trees, as described in appendix C. For example, a tree with 8 leaves: 5 | // 6 | // X 7 | // | 8 | // .---------+---------. 9 | // / \ 10 | // X X 11 | // | | 12 | // .---+---. .---+---. 13 | // / \ / \ 14 | // X X X X 15 | // / \ / \ / \ / \ 16 | // / \ / \ / \ / \ 17 | // X X X X X X X X 18 | // 19 | // Node: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 20 | // 21 | // Leaf: 0 1 2 3 4 5 6 7 22 | 23 | // numLeaves exposes operations on a tree with a given number of leaves. 24 | type numLeaves uint32 25 | 26 | func numLeavesFromWidth(w uint32) numLeaves { 27 | if w == 0 { 28 | return 0 29 | } 30 | return numLeaves((w-1)/2 + 1) 31 | } 32 | 33 | // width computes the minimum length of the array, ie. the number of nodes. 34 | func (n numLeaves) width() uint32 { 35 | if n == 0 { 36 | return 0 37 | } 38 | return 2*(uint32(n)-1) + 1 39 | } 40 | 41 | // root returns the index of the root node. 42 | func (n numLeaves) root() nodeIndex { 43 | return nodeIndex((1 << log2(n.width())) - 1) 44 | } 45 | 46 | // parent returns the index of the parent node for a non-root node index. 47 | func (n numLeaves) parent(x nodeIndex) (nodeIndex, bool) { 48 | if x == n.root() { 49 | return 0, false 50 | } 51 | lvl := nodeIndex(x.level()) 52 | b := (x >> (lvl + 1)) & 1 53 | p := (x | (1 << lvl)) ^ (b << (lvl + 1)) 54 | return p, true 55 | } 56 | 57 | // sibling returns the index of the other child of the node's parent. 58 | func (n numLeaves) sibling(x nodeIndex) (nodeIndex, bool) { 59 | p, ok := n.parent(x) 60 | if !ok { 61 | return 0, false 62 | } 63 | if x < p { 64 | return p.right() 65 | } else { 66 | return p.left() 67 | } 68 | } 69 | 70 | // directPath computes the direct path of a node, ordered from leaf to root. 71 | func (n numLeaves) directPath(x nodeIndex) []nodeIndex { 72 | var path []nodeIndex 73 | for { 74 | p, ok := n.parent(x) 75 | if !ok { 76 | break 77 | } 78 | path = append(path, p) 79 | x = p 80 | } 81 | return path 82 | } 83 | 84 | // copath computes the copath of a node, ordered from leaf to root. 85 | func (n numLeaves) copath(x nodeIndex) []nodeIndex { 86 | path := n.directPath(x) 87 | if len(path) == 0 { 88 | return nil 89 | } 90 | path = append([]nodeIndex{x}, path...) 91 | path = path[:len(path)-1] 92 | 93 | var copath []nodeIndex 94 | for _, y := range path { 95 | s, ok := n.sibling(y) 96 | if !ok { 97 | panic("unreachable") 98 | } 99 | copath = append(copath, s) 100 | } 101 | 102 | return copath 103 | } 104 | 105 | // nodeIndex is the index of a node in a tree. 106 | type nodeIndex uint32 107 | 108 | // isLeaf returns true if this is a leaf node, false if this is an intermediate 109 | // node. 110 | func (x nodeIndex) isLeaf() bool { 111 | return x%2 == 0 112 | } 113 | 114 | // leafIndex returns the index of the leaf from a node index. 115 | func (x nodeIndex) leafIndex() (leafIndex, bool) { 116 | if !x.isLeaf() { 117 | return 0, false 118 | } 119 | return leafIndex(x) >> 1, true 120 | } 121 | 122 | // left returns the index of the left child for an intermediate node index. 123 | func (x nodeIndex) left() (nodeIndex, bool) { 124 | lvl := x.level() 125 | if lvl == 0 { 126 | return 0, false 127 | } 128 | l := x ^ (1 << (nodeIndex(lvl) - 1)) 129 | return l, true 130 | } 131 | 132 | // right returns the index of the right child for an intermediate node index. 133 | func (x nodeIndex) right() (nodeIndex, bool) { 134 | lvl := x.level() 135 | if lvl == 0 { 136 | return 0, false 137 | } 138 | r := x ^ (3 << (nodeIndex(lvl) - 1)) 139 | return r, true 140 | } 141 | 142 | // children returns the indices of the left and right children for an 143 | // intermediate node index. 144 | func (x nodeIndex) children() (left, right nodeIndex, ok bool) { 145 | l, ok := x.left() 146 | if !ok { 147 | return 0, 0, false 148 | } 149 | r, _ := x.right() 150 | return l, r, true 151 | } 152 | 153 | // level returns the level of a node in the tree. Leaves are at level 0, their 154 | // parents are at level 1, etc. 155 | func (x nodeIndex) level() uint32 { 156 | if x&1 == 0 { 157 | return 0 158 | } 159 | lvl := uint32(0) 160 | for (x>>lvl)&1 == 1 { 161 | lvl++ 162 | } 163 | return lvl 164 | } 165 | 166 | // commonAncestor returns the the lowest node that is in the direct paths of 167 | // both leaves. 168 | func commonAncestor(x, y nodeIndex) nodeIndex { 169 | // Handle cases where one is an ancestor of the other 170 | lx, ly := x.level()+1, y.level()+1 171 | if lx <= ly && x>>ly == y>>ly { 172 | return y 173 | } else if ly <= lx && x>>lx == y>>lx { 174 | return x 175 | } 176 | 177 | // Handle other cases 178 | xn, yn := x, y 179 | k := 0 180 | for xn != yn { 181 | xn, yn = xn>>1, yn>>1 182 | k++ 183 | } 184 | return (xn << k) + (1 << (k - 1)) - 1 185 | } 186 | 187 | type leafIndex uint32 188 | 189 | // nodeIndex returns the index of the node from a leaf index. 190 | func (li leafIndex) nodeIndex() nodeIndex { 191 | return nodeIndex(2 * li) 192 | } 193 | 194 | // log2 computes the exponent of the largest power of 2 less than x. 195 | func log2(x uint32) uint32 { 196 | if x == 0 { 197 | return 0 198 | } 199 | 200 | k := uint32(0) 201 | for x>>k > 0 { 202 | k++ 203 | } 204 | return k - 1 205 | } 206 | 207 | func isPowerOf2(x uint32) bool { 208 | return x != 0 && x&(x-1) == 0 209 | } 210 | -------------------------------------------------------------------------------- /tree_math_test.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type treeMathTest struct { 9 | NLeaves numLeaves `json:"n_leaves"` 10 | 11 | NNodes uint32 `json:"n_nodes"` 12 | Root nodeIndex `json:"root"` 13 | Left []*nodeIndex `json:"left"` 14 | Right []*nodeIndex `json:"right"` 15 | Parent []*nodeIndex `json:"parent"` 16 | Sibling []*nodeIndex `json:"sibling"` 17 | } 18 | 19 | func testTreeMath(t *testing.T, tc *treeMathTest) { 20 | n := tc.NLeaves 21 | if w := n.width(); w != tc.NNodes { 22 | t.Errorf("width(%v) = %v, want %v", n, w, tc.NNodes) 23 | } 24 | if r := n.root(); r != tc.Root { 25 | t.Errorf("root(%v) = %v, want %v", n, r, tc.Root) 26 | } 27 | for i, want := range tc.Left { 28 | x := nodeIndex(i) 29 | l := newOptionalNodeIndex(x.left()) 30 | if !optionalNodeIndexEqual(l, want) { 31 | t.Errorf("left(%v) = %v, want %v", x, l, want) 32 | } 33 | } 34 | for i, want := range tc.Right { 35 | x := nodeIndex(i) 36 | r := newOptionalNodeIndex(x.right()) 37 | if !optionalNodeIndexEqual(r, want) { 38 | t.Errorf("right(%v) = %v, want %v", x, r, want) 39 | } 40 | } 41 | for i, want := range tc.Parent { 42 | x := nodeIndex(i) 43 | p := newOptionalNodeIndex(n.parent(x)) 44 | if !optionalNodeIndexEqual(p, want) { 45 | t.Errorf("parent(%v) = %v, want %v", x, p, want) 46 | } 47 | } 48 | for i, want := range tc.Sibling { 49 | x := nodeIndex(i) 50 | s := newOptionalNodeIndex(n.sibling(x)) 51 | if !optionalNodeIndexEqual(s, want) { 52 | t.Errorf("sibling(%v) = %v, want %v", x, s, want) 53 | } 54 | } 55 | } 56 | 57 | func newOptionalNodeIndex(x nodeIndex, ok bool) *nodeIndex { 58 | if !ok { 59 | return nil 60 | } 61 | return &x 62 | } 63 | 64 | func optionalNodeIndexEqual(x, y *nodeIndex) bool { 65 | if x == nil || y == nil { 66 | return x == nil && y == nil 67 | } 68 | return *x == *y 69 | } 70 | 71 | func TestTreeMath(t *testing.T) { 72 | var tests []treeMathTest 73 | loadTestVector(t, "testdata/tree-math.json", &tests) 74 | 75 | for _, tc := range tests { 76 | t.Run(fmt.Sprintf("numLeaves(%v)", tc.NLeaves), func(t *testing.T) { 77 | testTreeMath(t, &tc) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package mls 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | type treeValidationTest struct { 12 | CipherSuite cipherSuite `json:"cipher_suite"` 13 | 14 | Tree testBytes `json:"tree"` 15 | GroupID testBytes `json:"group_id"` 16 | 17 | Resolutions [][]nodeIndex `json:"resolutions"` 18 | TreeHashes []testBytes `json:"tree_hashes"` 19 | } 20 | 21 | func testTreeValidation(t *testing.T, tc *treeValidationTest) { 22 | var tree ratchetTree 23 | if err := unmarshal([]byte(tc.Tree), &tree); err != nil { 24 | t.Fatalf("unmarshal(tree) = %v", err) 25 | } 26 | 27 | for i, want := range tc.Resolutions { 28 | x := nodeIndex(i) 29 | res := tree.resolve(x) 30 | if len(res) == 0 { 31 | res = make([]nodeIndex, 0) 32 | } 33 | if !reflect.DeepEqual(res, want) { 34 | t.Errorf("resolve(%v) = %v, want %v", x, res, want) 35 | } 36 | } 37 | 38 | for i, want := range tc.TreeHashes { 39 | x := nodeIndex(i) 40 | if h, err := tree.computeTreeHash(tc.CipherSuite, x, nil); err != nil { 41 | t.Errorf("computeTreeHash(%v) = %v", x, err) 42 | } else if !bytes.Equal(h, []byte(want)) { 43 | t.Errorf("computeTreeHash(%v) = %v, want %v", x, h, want) 44 | } 45 | } 46 | 47 | if !tree.verifyParentHashes(tc.CipherSuite) { 48 | t.Errorf("verifyParentHashes() failed") 49 | } 50 | 51 | groupID := GroupID(tc.GroupID) 52 | for i, node := range tree { 53 | if node == nil || node.nodeType != nodeTypeLeaf { 54 | continue 55 | } 56 | li, ok := nodeIndex(i).leafIndex() 57 | if !ok { 58 | t.Errorf("leafIndex(%v) = false", i) 59 | continue 60 | } 61 | if !node.leafNode.verifySignature(tc.CipherSuite, groupID, li) { 62 | t.Errorf("verify(%v) = false", li) 63 | } 64 | } 65 | } 66 | 67 | func TestTreeValidation(t *testing.T) { 68 | var tests []treeValidationTest 69 | loadTestVector(t, "testdata/tree-validation.json", &tests) 70 | 71 | for i, tc := range tests { 72 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 73 | testTreeValidation(t, &tc) 74 | }) 75 | } 76 | } 77 | 78 | type treeKEMTest struct { 79 | CipherSuite cipherSuite `json:"cipher_suite"` 80 | 81 | GroupID testBytes `json:"group_id"` 82 | Epoch uint64 `json:"epoch"` 83 | ConfirmedTranscriptHash testBytes `json:"confirmed_transcript_hash"` 84 | 85 | RatchetTree testBytes `json:"ratchet_tree"` 86 | 87 | LeavesPrivate []struct { 88 | Index leafIndex `json:"index"` 89 | EncryptionPriv testBytes `json:"encryption_priv"` 90 | SignaturePriv testBytes `json:"signature_priv"` 91 | PathSecrets []struct { 92 | Node nodeIndex `json:"node"` 93 | PathSecret testBytes `json:"path_secret"` 94 | } `json:"path_secrets"` 95 | } `json:"leaves_private"` 96 | 97 | UpdatePaths []struct { 98 | Sender leafIndex `json:"sender"` 99 | UpdatePath testBytes `json:"update_path"` 100 | PathSecrets []testBytes `json:"path_secrets"` 101 | CommitSecret testBytes `json:"commit_secret"` 102 | TreeHashAfter testBytes `json:"tree_hash_after"` 103 | } `json:"update_paths"` 104 | } 105 | 106 | func testTreeKEM(t *testing.T, tc *treeKEMTest) { 107 | type privNode struct { 108 | encryptionPriv []byte 109 | signaturePriv []byte 110 | } 111 | 112 | for _, leafPrivate := range tc.LeavesPrivate { 113 | var tree ratchetTree 114 | if err := unmarshal([]byte(tc.RatchetTree), &tree); err != nil { 115 | t.Fatalf("unmarshal(ratchetTree) = %v", err) 116 | } 117 | 118 | privTree := make([]privNode, len(tree)) 119 | privTree[int(leafPrivate.Index.nodeIndex())] = privNode{ 120 | encryptionPriv: leafPrivate.EncryptionPriv, 121 | signaturePriv: leafPrivate.SignaturePriv, 122 | } 123 | 124 | // TODO: drop the seed size check, see: 125 | // https://github.com/cloudflare/circl/issues/486 126 | kem, kdf, _ := tc.CipherSuite.hpke().Params() 127 | if kem.Scheme().SeedSize() != kdf.ExtractSize() { 128 | continue 129 | } 130 | 131 | for _, ps := range leafPrivate.PathSecrets { 132 | priv, err := nodePrivFromPathSecret(tc.CipherSuite, ps.PathSecret, tree.get(ps.Node).encryptionKey()) 133 | if err != nil { 134 | t.Fatalf("failed to derive node %v private key from path secret: %v", ps.Node, err) 135 | } 136 | 137 | privTree[int(ps.Node)] = privNode{ 138 | encryptionPriv: priv, 139 | } 140 | } 141 | 142 | for i, privNode := range privTree { 143 | if privNode.encryptionPriv == nil { 144 | continue 145 | } 146 | 147 | priv, err := kem.Scheme().UnmarshalBinaryPrivateKey(privNode.encryptionPriv) 148 | if err != nil { 149 | t.Fatalf("UnmarshalBinaryPrivateKey() = %v", err) 150 | } 151 | 152 | pub, err := kem.Scheme().UnmarshalBinaryPublicKey(tree[i].encryptionKey()) 153 | if err != nil { 154 | t.Fatalf("UnmarshalBinaryPublicKey() = %v", err) 155 | } 156 | 157 | if !priv.Public().Equal(pub) { 158 | t.Errorf("key pair mismatch for node %v", i) 159 | } 160 | 161 | // TODO: check signature key 162 | } 163 | } 164 | 165 | for _, updatePathTest := range tc.UpdatePaths { 166 | var tree ratchetTree 167 | if err := unmarshal([]byte(tc.RatchetTree), &tree); err != nil { 168 | t.Fatalf("unmarshal(ratchetTree) = %v", err) 169 | } 170 | 171 | var up updatePath 172 | if err := unmarshal([]byte(updatePathTest.UpdatePath), &up); err != nil { 173 | t.Fatalf("unmarshal(updatePath) = %v", err) 174 | } 175 | 176 | // TODO: verify that UpdatePath is parent-hash valid relative to ratchet tree 177 | // TODO: process UpdatePath using private leaves 178 | 179 | if err := tree.mergeUpdatePath(tc.CipherSuite, updatePathTest.Sender, &up); err != nil { 180 | t.Fatalf("ratchetTree.mergeUpdatePath() = %v", err) 181 | } 182 | 183 | treeHash, err := tree.computeRootTreeHash(tc.CipherSuite) 184 | if err != nil { 185 | t.Errorf("ratchetTree.computeRootTreeHash() = %v", err) 186 | } else if !bytes.Equal(treeHash, []byte(updatePathTest.TreeHashAfter)) { 187 | t.Errorf("ratchetTree.computeRootTreeHash() = %v, want %v", treeHash, updatePathTest.TreeHashAfter) 188 | } 189 | 190 | // TODO: create and verify new update path 191 | } 192 | } 193 | 194 | func TestTreeKEM(t *testing.T) { 195 | var tests []treeKEMTest 196 | loadTestVector(t, "testdata/treekem.json", &tests) 197 | 198 | for i, tc := range tests { 199 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 200 | testTreeKEM(t, &tc) 201 | }) 202 | } 203 | } 204 | 205 | type treeOperationsTest struct { 206 | CipherSuite cipherSuite `json:"cipher_suite"` 207 | 208 | TreeBefore testBytes `json:"tree_before"` 209 | Proposal testBytes `json:"proposal"` 210 | ProposalSender leafIndex `json:"proposal_sender"` 211 | 212 | TreeHashBefore testBytes `json:"tree_hash_before"` 213 | TreeAfter testBytes `json:"tree_after"` 214 | TreeHashAfter testBytes `json:"tree_hash_after"` 215 | } 216 | 217 | func testTreeOperations(t *testing.T, tc *treeOperationsTest) { 218 | var tree ratchetTree 219 | if err := unmarshal([]byte(tc.TreeBefore), &tree); err != nil { 220 | t.Fatalf("unmarshal(tree) = %v", err) 221 | } 222 | 223 | treeHash, err := tree.computeRootTreeHash(tc.CipherSuite) 224 | if err != nil { 225 | t.Errorf("ratchetTree.computeRootTreeHash() = %v", err) 226 | } else if !bytes.Equal(treeHash, []byte(tc.TreeHashBefore)) { 227 | t.Errorf("ratchetTree.computeRootTreeHash() = %v, want %v", treeHash, tc.TreeHashBefore) 228 | } 229 | 230 | var prop proposal 231 | if err := unmarshal([]byte(tc.Proposal), &prop); err != nil { 232 | t.Fatalf("unmarshal(proposal) = %v", err) 233 | } 234 | 235 | switch prop.proposalType { 236 | case proposalTypeAdd: 237 | ctx := groupContext{ 238 | version: prop.add.keyPackage.version, 239 | cipherSuite: prop.add.keyPackage.cipherSuite, 240 | } 241 | if err := prop.add.keyPackage.verify(&ctx); err != nil { 242 | t.Errorf("keyPackage.verify() = %v", err) 243 | } 244 | tree.add(&prop.add.keyPackage.leafNode) 245 | case proposalTypeUpdate: 246 | signatureKeys, encryptionKeys := tree.keys() 247 | err := prop.update.leafNode.verify(&leafNodeVerifyOptions{ 248 | cipherSuite: tc.CipherSuite, 249 | groupID: nil, 250 | leafIndex: tc.ProposalSender, 251 | supportedCreds: tree.supportedCreds(), 252 | signatureKeys: signatureKeys, 253 | encryptionKeys: encryptionKeys, 254 | now: func() time.Time { return time.Time{} }, 255 | }) 256 | if err != nil { 257 | t.Errorf("leafNode.verify() = %v", err) 258 | } 259 | tree.update(tc.ProposalSender, &prop.update.leafNode) 260 | case proposalTypeRemove: 261 | if tree.getLeaf(prop.remove.removed) == nil { 262 | t.Errorf("leaf node %v is blank", prop.remove.removed) 263 | } 264 | tree.remove(prop.remove.removed) 265 | default: 266 | panic("unreachable") 267 | } 268 | 269 | rawTree, err := marshal(&tree) 270 | if err != nil { 271 | t.Fatalf("marshal(tree) = %v", err) 272 | } else if !bytes.Equal(rawTree, []byte(tc.TreeAfter)) { 273 | t.Errorf("marshal(tree) = %v, want %v", rawTree, tc.TreeAfter) 274 | } 275 | 276 | treeHash, err = tree.computeRootTreeHash(tc.CipherSuite) 277 | if err != nil { 278 | t.Errorf("ratchetTree.computeRootTreeHash() = %v", err) 279 | } else if !bytes.Equal(treeHash, []byte(tc.TreeHashAfter)) { 280 | t.Errorf("ratchetTree.computeRootTreeHash() = %v, want %v", treeHash, tc.TreeHashAfter) 281 | } 282 | } 283 | 284 | func TestTreeOperations(t *testing.T) { 285 | var tests []treeOperationsTest 286 | loadTestVector(t, "testdata/tree-operations.json", &tests) 287 | 288 | for i, tc := range tests { 289 | t.Run(fmt.Sprintf("[%v]", i), func(t *testing.T) { 290 | testTreeOperations(t, &tc) 291 | }) 292 | } 293 | } 294 | --------------------------------------------------------------------------------