ciphersuite.mx raw
1 package mls
2
3 // MLS cipher suites (RFC 9420 §5.1, §17.1 + RFC 9180).
4 // Supported:
5 // 0x0003: DHKEM(X25519, HKDF-SHA256) + ChaCha20-Poly1305 + SHA-256 + Ed25519
6 //
7 // Suite 0x0001 (AES-128-GCM) is defined in the wire format but not implemented here.
8 // Smesh hardcodes suite 0x0003 in marmot; receiving a 0x0001 group is an error.
9
10 import (
11 "crypto/ed25519"
12 "errors"
13 "smesh.lol/web/common/crypto/chacha20poly1305"
14 "smesh.lol/web/common/crypto/hkdf"
15 "crypto/hmac"
16 "crypto/sha256"
17 "smesh.lol/web/common/jsbridge/subtle"
18 "smesh.lol/web/common/jsbridge/x25519"
19 )
20
21 var (
22 errInvalidKeySize = errors.New("mls: invalid key size")
23 errAEADOpenFailed = errors.New("mls: AEAD open failed")
24 errHPKEDecryptFailed = errors.New("mls: HPKE decryption failed")
25 )
26
27 // --- Suite parameters (shared) ---
28
29 const (
30 hashSize = 32 // SHA-256 (both suites)
31 aeadNonce = 12 // AEAD nonce size (both suites)
32 aeadTag = 16 // AEAD tag size (both suites)
33 kemKeySize = 32 // X25519 (both suites)
34 )
35
36 func (cs CipherSuite) assertSupported() {
37 switch cs {
38 case CipherSuite0x0003:
39 default:
40 panic("mls: unsupported cipher suite")
41 }
42 }
43
44 // --- Public metadata ---
45
46 func (cs CipherSuite) HashSize() int {
47 cs.assertSupported()
48 return hashSize
49 }
50
51 func (cs CipherSuite) AEADKeySize() int {
52 cs.assertSupported()
53 return 32 // ChaCha20-Poly1305
54 }
55
56 func (cs CipherSuite) AEADNonceSize() int {
57 cs.assertSupported()
58 return aeadNonce
59 }
60
61 func (cs CipherSuite) ExtractSize() int {
62 cs.assertSupported()
63 return hashSize
64 }
65
66 // --- KEM/HPKE suite IDs (RFC 9180 §4, §5) ---
67
68 // kemSuiteID = "KEM" || I2OSP(kem_id=0x0020, 2) — DHKEM(X25519, HKDF-SHA256) for both suites.
69 var kemSuiteID = []byte("KEM\x00\x20")
70
71 // hpkeSuiteID = "HPKE" || I2OSP(kem=0x0020,2) || I2OSP(kdf=0x0001,2) || I2OSP(aead=0x0003,2)
72 // Suite 0x0003: ChaCha20-Poly1305 only.
73 func (cs CipherSuite) hpkeSuiteID() []byte {
74 cs.assertSupported()
75 return []byte("HPKE\x00\x20\x00\x01\x00\x03")
76 }
77
78 // --- Hash ---
79
80 func (cs CipherSuite) hash(data []byte) []byte {
81 cs.assertSupported()
82 h := sha256.Sum(data)
83 return h[:]
84 }
85
86 // --- MAC ---
87
88 func (cs CipherSuite) signMAC(key, message []byte) []byte {
89 cs.assertSupported()
90 mac := hmac.Sum(key, message)
91 return mac[:]
92 }
93
94 func (cs CipherSuite) verifyMAC(key, message, tag []byte) bool {
95 cs.assertSupported()
96 expected := hmac.Sum(key, message)
97 return hmacEqual(tag, expected[:])
98 }
99
100 func hmacEqual(a, b []byte) bool {
101 if len(a) != len(b) {
102 return false
103 }
104 var v byte
105 for i := range a {
106 v |= a[i] ^ b[i]
107 }
108 return v == 0
109 }
110
111 // --- HKDF ---
112
113 func (cs CipherSuite) hkdfExtract(salt, ikm []byte) []byte {
114 cs.assertSupported()
115 h := hkdf.Extract(salt, ikm)
116 return h[:]
117 }
118
119 func (cs CipherSuite) hkdfExpand(prk, info []byte, length int) []byte {
120 cs.assertSupported()
121 return hkdf.Expand(prk, info, length)
122 }
123
124 // --- MLS Labeled Expand/Extract (RFC 9420 §8) ---
125
126 func (cs CipherSuite) expandWithLabel(secret, label, context []byte, length uint16) ([]byte, error) {
127 cs.assertSupported()
128 mlsLabel := append([]byte("MLS 1.0 "), label...)
129
130 // KDFLabel = length (2) ‖ opaqueVec(label) ‖ opaqueVec(context)
131 var w Writer
132 w.addUint16(length)
133 w.writeOpaqueVec(mlsLabel)
134 w.writeOpaqueVec(context)
135 kdfLabel, err := w.bytes()
136 if err != nil {
137 return nil, err
138 }
139
140 return hkdf.Expand(secret, kdfLabel, int(length)), nil
141 }
142
143 func (cs CipherSuite) deriveSecret(secret, label []byte) ([]byte, error) {
144 return cs.expandWithLabel(secret, label, nil, uint16(hashSize))
145 }
146
147 // --- Ref hash ---
148
149 func (cs CipherSuite) refHash(label, value []byte) ([]byte, error) {
150 cs.assertSupported()
151 var w Writer
152 w.writeOpaqueVec(label)
153 w.writeOpaqueVec(value)
154 input, err := w.bytes()
155 if err != nil {
156 return nil, err
157 }
158 h := sha256.Sum(input)
159 return h[:], nil
160 }
161
162 // --- Signatures (Ed25519 for both suites) ---
163
164 func (cs CipherSuite) signWithLabel(signKey signaturePrivateKey, label, content []byte) ([]byte, error) {
165 cs.assertSupported()
166 signContent, err := marshalSignContent(label, content)
167 if err != nil {
168 return nil, err
169 }
170 privkey := ed25519.NewKeyFromSeed([]byte(signKey))
171 return ed25519.Sign(privkey, signContent), nil
172 }
173
174 func (cs CipherSuite) verifyWithLabel(verifKey signaturePublicKey, label, content, signValue []byte) bool {
175 cs.assertSupported()
176 signContent, err := marshalSignContent(label, content)
177 if err != nil {
178 return false
179 }
180 return ed25519.Verify(ed25519.PublicKey(verifKey), signContent, signValue)
181 }
182
183 func marshalSignContent(label, content []byte) ([]byte, error) {
184 mlsLabel := append([]byte("MLS 1.0 "), label...)
185 var w Writer
186 w.writeOpaqueVec(mlsLabel)
187 w.writeOpaqueVec(content)
188 return w.bytes()
189 }
190
191 func (cs CipherSuite) generateSignatureKeyPair() (signaturePublicKey, signaturePrivateKey, error) {
192 cs.assertSupported()
193 seed := []byte{:32}
194 subtle.RandomBytes(seed)
195 privkey := ed25519.NewKeyFromSeed(seed)
196 pub := []byte{:32}
197 copy(pub, []byte(privkey)[32:])
198 return signaturePublicKey(pub), signaturePrivateKey(seed), nil
199 }
200
201 // --- AEAD dispatch ---
202
203 func (cs CipherSuite) aeadSeal(key, nonce, plaintext, aad []byte) ([]byte, error) {
204 cs.assertSupported()
205 if len(key) != cs.AEADKeySize() {
206 return nil, errInvalidKeySize
207 }
208 var k [32]byte
209 var n [12]byte
210 copy(k[:], key)
211 copy(n[:], nonce)
212 return chacha20poly1305.Seal(k, n, plaintext, aad), nil
213 }
214
215 func (cs CipherSuite) aeadOpen(key, nonce, ciphertext, aad []byte) ([]byte, error) {
216 cs.assertSupported()
217 if len(key) != cs.AEADKeySize() {
218 return nil, errInvalidKeySize
219 }
220 var k [32]byte
221 var n [12]byte
222 copy(k[:], key)
223 copy(n[:], nonce)
224 pt, ok := chacha20poly1305.Open(k, n, ciphertext, aad)
225 if !ok {
226 return nil, errAEADOpenFailed
227 }
228 return pt, nil
229 }
230
231 // --- DHKEM(X25519, HKDF-SHA256) — RFC 9180 §4.1 (both suites) ---
232
233 // labeledExtractKEM: HKDF-Extract(salt, "HPKE-v1" || kemSuiteID || label || ikm)
234 func labeledExtractKEM(salt, label, ikm []byte) []byte {
235 labeled := []byte("HPKE-v1")
236 labeled = append(labeled, kemSuiteID...)
237 labeled = append(labeled, label...)
238 labeled = append(labeled, ikm...)
239 h := hkdf.Extract(salt, labeled)
240 return h[:]
241 }
242
243 // labeledExpandKEM: HKDF-Expand(prk, I2OSP(L,2) || "HPKE-v1" || kemSuiteID || label || info, L)
244 func labeledExpandKEM(prk, label, info []byte, length int) []byte {
245 labeled := []byte{byte(length >> 8), byte(length)}
246 labeled = append(labeled, []byte("HPKE-v1")...)
247 labeled = append(labeled, kemSuiteID...)
248 labeled = append(labeled, label...)
249 labeled = append(labeled, info...)
250 return hkdf.Expand(prk, labeled, length)
251 }
252
253 // labeledExtractHPKE: HKDF-Extract(salt, "HPKE-v1" || hpkeSuiteID || label || ikm)
254 func (cs CipherSuite) labeledExtractHPKE(salt, label, ikm []byte) []byte {
255 labeled := []byte("HPKE-v1")
256 labeled = append(labeled, cs.hpkeSuiteID()...)
257 labeled = append(labeled, label...)
258 labeled = append(labeled, ikm...)
259 h := hkdf.Extract(salt, labeled)
260 return h[:]
261 }
262
263 // labeledExpandHPKE: HKDF-Expand(prk, I2OSP(L,2) || "HPKE-v1" || hpkeSuiteID || label || info, L)
264 func (cs CipherSuite) labeledExpandHPKE(prk, label, info []byte, length int) []byte {
265 labeled := []byte{byte(length >> 8), byte(length)}
266 labeled = append(labeled, []byte("HPKE-v1")...)
267 labeled = append(labeled, cs.hpkeSuiteID()...)
268 labeled = append(labeled, label...)
269 labeled = append(labeled, info...)
270 return hkdf.Expand(prk, labeled, length)
271 }
272
273 // extractAndExpand derives the shared secret from DH output per RFC 9180 §4.1.
274 // eae_prk = LabeledExtract("", "eae_prk", dh)
275 // shared_secret = LabeledExpand(eae_prk, "shared_secret", kem_context, Nsecret)
276 func extractAndExpand(dh, kemContext []byte) []byte {
277 prk := labeledExtractKEM(nil, []byte("eae_prk"), dh)
278 return labeledExpandKEM(prk, []byte("shared_secret"), kemContext, hashSize)
279 }
280
281 // --- KEM operations (X25519 for both suites) ---
282
283 func (cs CipherSuite) kemEncap(pkR hpkePublicKey) (sharedSecret, enc []byte, err error) {
284 cs.assertSupported()
285 // Generate ephemeral keypair
286 skE := []byte{:kemKeySize}
287 subtle.RandomBytes(skE)
288 pkE := x25519.ScalarBaseMult(skE)
289
290 // DH(skE, pkR)
291 dh := x25519.ScalarMult(skE, []byte(pkR))
292
293 enc = pkE
294 kemContext := append(enc, []byte(pkR)...)
295 sharedSecret = extractAndExpand(dh, kemContext)
296 return sharedSecret, enc, nil
297 }
298
299 func (cs CipherSuite) kemDecap(enc []byte, skR hpkePrivateKey) ([]byte, error) {
300 cs.assertSupported()
301 pkR := x25519.ScalarBaseMult([]byte(skR))
302 dh := x25519.ScalarMult([]byte(skR), enc)
303 kemContext := append([]byte{:0:len(enc)+len(pkR)}, enc...)
304 kemContext = append(kemContext, pkR...)
305 return extractAndExpand(dh, kemContext), nil
306 }
307
308 // --- HPKE Base mode setup (RFC 9180 §5.1) ---
309
310 type hpkeSealer struct {
311 cs CipherSuite
312 key []byte
313 nonce []byte
314 }
315
316 func (s *hpkeSealer) seal(plaintext, aad []byte) []byte {
317 ct, err := s.cs.aeadSeal(s.key, s.nonce, plaintext, aad)
318 if err != nil {
319 return nil
320 }
321 return ct
322 }
323
324 type hpkeOpener struct {
325 cs CipherSuite
326 key []byte
327 nonce []byte
328 }
329
330 func (o *hpkeOpener) open(ciphertext, aad []byte) ([]byte, bool) {
331 pt, err := o.cs.aeadOpen(o.key, o.nonce, ciphertext, aad)
332 if err != nil {
333 return nil, false
334 }
335 return pt, true
336 }
337
338 func (cs CipherSuite) hpkeKeySchedule(sharedSecret, info []byte) (key, baseNonce []byte) {
339 // mode = 0x00 (Base mode)
340 mode := byte(0x00)
341
342 // psk_id_hash = LabeledExtract("", "psk_id_hash", "")
343 pskIDHash := cs.labeledExtractHPKE(nil, []byte("psk_id_hash"), nil)
344 // info_hash = LabeledExtract("", "info_hash", info)
345 infoHash := cs.labeledExtractHPKE(nil, []byte("info_hash"), info)
346
347 // ks_context = mode || psk_id_hash || info_hash
348 ksContext := []byte{mode}
349 ksContext = append(ksContext, pskIDHash...)
350 ksContext = append(ksContext, infoHash...)
351
352 // secret = LabeledExtract(shared_secret, "secret", psk="")
353 secret := cs.labeledExtractHPKE(sharedSecret, []byte("secret"), nil)
354
355 // key = LabeledExpand(secret, "key", ks_context, Nk)
356 key = cs.labeledExpandHPKE(secret, []byte("key"), ksContext, cs.AEADKeySize())
357 // base_nonce = LabeledExpand(secret, "base_nonce", ks_context, Nn)
358 baseNonce = cs.labeledExpandHPKE(secret, []byte("base_nonce"), ksContext, aeadNonce)
359 return key, baseNonce
360 }
361
362 // encryptWithLabel implements HPKE single-shot encrypt (RFC 9180 §6.1).
363 func (cs CipherSuite) encryptWithLabel(publicKey hpkePublicKey, label, context, plaintext []byte) (kemOutput, ciphertext []byte, err error) {
364 encryptContext, err := marshalEncryptContext(label, context)
365 if err != nil {
366 return nil, nil, err
367 }
368
369 sharedSecret, enc, err := cs.kemEncap(publicKey)
370 if err != nil {
371 return nil, nil, err
372 }
373
374 key, baseNonce := cs.hpkeKeySchedule(sharedSecret, encryptContext)
375 sealer := &hpkeSealer{cs: cs, key: key, nonce: baseNonce}
376 ciphertext = sealer.seal(plaintext, nil)
377 return enc, ciphertext, nil
378 }
379
380 // decryptWithLabel implements HPKE single-shot decrypt (RFC 9180 §6.1).
381 func (cs CipherSuite) decryptWithLabel(privateKey hpkePrivateKey, label, context, kemOutput, ciphertext []byte) ([]byte, error) {
382 encryptContext, err := marshalEncryptContext(label, context)
383 if err != nil {
384 return nil, err
385 }
386 sharedSecret, err := cs.kemDecap(kemOutput, privateKey)
387 if err != nil {
388 return nil, err
389 }
390 key, baseNonce := cs.hpkeKeySchedule(sharedSecret, encryptContext)
391 opener := &hpkeOpener{cs: cs, key: key, nonce: baseNonce}
392 plaintext, ok := opener.open(ciphertext, nil)
393 if !ok {
394 return nil, errHPKEDecryptFailed
395 }
396 return plaintext, nil
397 }
398
399 func marshalEncryptContext(label, context []byte) ([]byte, error) {
400 mlsLabel := append([]byte("MLS 1.0 "), label...)
401 var w Writer
402 w.writeOpaqueVec(mlsLabel)
403 w.writeOpaqueVec(context)
404 return w.bytes()
405 }
406
407 // --- Key generation ---
408
409 func (cs CipherSuite) generateEncryptionKeyPair() (hpkePublicKey, hpkePrivateKey, error) {
410 cs.assertSupported()
411 sk := []byte{:kemKeySize}
412 subtle.RandomBytes(sk)
413 pk := x25519.ScalarBaseMult(sk)
414 return hpkePublicKey(pk), hpkePrivateKey(sk), nil
415 }
416
417 func (cs CipherSuite) deriveEncryptionKeyPair(seed []byte) (hpkePublicKey, hpkePrivateKey, error) {
418 cs.assertSupported()
419 // RFC 9180 §7.1.3: DeriveKeyPair for X25519
420 // dkp_prk = LabeledExtract("", "dkp_prk", seed)
421 dkpPrk := labeledExtractKEM(nil, []byte("dkp_prk"), seed)
422 // sk = LabeledExpand(dkp_prk, "sk", "", Nsk=32)
423 sk := labeledExpandKEM(dkpPrk, []byte("sk"), nil, kemKeySize)
424 pk := x25519.ScalarBaseMult(sk)
425 return hpkePublicKey(pk), hpkePrivateKey(sk), nil
426 }
427
428 func (cs CipherSuite) randomBytes(n int) []byte {
429 buf := []byte{:n}
430 subtle.RandomBytes(buf)
431 return buf
432 }
433