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