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