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