exchange.go raw

   1  package crypto
   2  
   3  import (
   4  	"errors"
   5  	"sort"
   6  
   7  	"git.mleku.dev/mleku/dendrite/pkg/ratio"
   8  	"git.mleku.dev/mleku/dendrite/pkg/spore"
   9  )
  10  
  11  // SharedSecret is the result of a key exchange between two peers.
  12  type SharedSecret struct {
  13  	// CommonTags are the constraint type tags present in both lattices.
  14  	CommonTags []string
  15  
  16  	// Secret is the derived Hamadryad shared key material (56 bytes).
  17  	Secret Hamadryad
  18  
  19  	// Confidence measures how much structural overlap exists.
  20  	// Higher confidence = more shared constraint structure = stronger key.
  21  	Confidence ratio.Ratio
  22  }
  23  
  24  // Exchange performs a Diffie-Hellman-like key exchange using spores.
  25  //
  26  // The protocol:
  27  //  1. Find the constraint types common to both spores
  28  //  2. For each common type, compute the structural overlap:
  29  //     occupancy rates, connectivity, permutation distributions
  30  //  3. Hash the common structure to derive the shared secret
  31  //
  32  // This works because:
  33  //   - Both parties derive the same set of common tags deterministically
  34  //   - The structural overlap (occupancy * connectivity) is symmetric
  35  //   - An eavesdropper sees only the spores but cannot determine which
  36  //     constraints would admit elements without the factory (private key)
  37  func Exchange(
  38  	ownSpore *spore.Spore,
  39  	peerSpore *spore.Spore,
  40  ) (*SharedSecret, error) {
  41  	if ownSpore == nil || peerSpore == nil {
  42  		return nil, errors.New("crypto: nil spore in exchange")
  43  	}
  44  
  45  	// Find common tags.
  46  	ownTags := tagSet(ownSpore.TypeSignature)
  47  	peerTags := tagSet(peerSpore.TypeSignature)
  48  
  49  	var common []string
  50  	for tag := range ownTags {
  51  		if peerTags[tag] {
  52  			common = append(common, tag)
  53  		}
  54  	}
  55  	sort.Strings(common)
  56  
  57  	if len(common) == 0 {
  58  		return &SharedSecret{
  59  			Confidence: ratio.Zero,
  60  		}, nil
  61  	}
  62  
  63  	// Compute confidence: |common| / max(|own|, |peer|).
  64  	maxTags := max(len(ownTags), len(peerTags))
  65  	confidence := ratio.New(int64(len(common)), int64(maxTags))
  66  
  67  	// Derive shared secret from common structure.
  68  	// The key material is: sorted common tags + their occupancy rates
  69  	// from both spores + connectivity ratios, hashed through Hamadryad.
  70  	var buf []byte
  71  	// Domain separation.
  72  	buf = append(buf, []byte("dendrite-exchange-v1")...)
  73  
  74  	for _, tag := range common {
  75  		buf = append(buf, []byte(tag)...)
  76  
  77  		// Occupancy rates for this tag from both spores.
  78  		// Use min/max ordering for commutativity — both parties
  79  		// must derive the same hash regardless of who is "own" vs "peer".
  80  		ownCount := tagCountLookup(ownSpore.TypeSignature, tag)
  81  		ownRate := ratio.New(int64(ownCount), int64(ownSpore.TotalNodes))
  82  		peerCount := tagCountLookup(peerSpore.TypeSignature, tag)
  83  		peerRate := ratio.New(int64(peerCount), int64(peerSpore.TotalNodes))
  84  
  85  		minRate := ratio.Min(ownRate, peerRate)
  86  		maxRate := ratio.Max(ownRate, peerRate)
  87  		buf = append(buf, []byte(minRate.String())...)
  88  		buf = append(buf, []byte(maxRate.String())...)
  89  
  90  		// Connectivity overlap — also commutative via min/max.
  91  		ownConn := tagRatioLookup(ownSpore.Connectivity, tag)
  92  		peerConn := tagRatioLookup(peerSpore.Connectivity, tag)
  93  
  94  		minConn := ratio.Min(ownConn, peerConn)
  95  		maxConn := ratio.Max(ownConn, peerConn)
  96  		buf = append(buf, []byte(minConn.String())...)
  97  		buf = append(buf, []byte(maxConn.String())...)
  98  	}
  99  
 100  	secret := Hash(buf)
 101  
 102  	return &SharedSecret{
 103  		CommonTags: common,
 104  		Secret:     secret,
 105  		Confidence: confidence,
 106  	}, nil
 107  }
 108  
 109  // DeriveKey produces a fixed-length key from a SharedSecret.
 110  // Uses Hamadryad with domain separation for key derivation.
 111  func DeriveKey(ss *SharedSecret, context string, keyLen int) []byte {
 112  	var buf []byte
 113  	buf = append(buf, []byte("dendrite-derive-")...)
 114  	buf = append(buf, []byte(context)...)
 115  	buf = append(buf, ss.Secret[:]...)
 116  
 117  	derived := Hash(buf)
 118  	result := derived[:]
 119  
 120  	// Extend if needed by iterating.
 121  	for len(result) < keyLen {
 122  		buf = buf[:0]
 123  		buf = append(buf, result...)
 124  		buf = append(buf, []byte(context)...)
 125  		next := Hash(buf)
 126  		result = append(result, next[:]...)
 127  	}
 128  
 129  	return result[:keyLen]
 130  }
 131  
 132  // tagSet returns a set of tags from a TypeSignature.
 133  func tagSet(tc []spore.TagCount) map[string]bool {
 134  	m := make(map[string]bool, len(tc))
 135  	for _, t := range tc {
 136  		m[t.Tag] = true
 137  	}
 138  	return m
 139  }
 140  
 141  // tagCountLookup returns the count for a tag in a TypeSignature.
 142  func tagCountLookup(tc []spore.TagCount, tag string) int {
 143  	for _, t := range tc {
 144  		if t.Tag == tag {
 145  			return t.Count
 146  		}
 147  	}
 148  	return 0
 149  }
 150  
 151  // tagRatioLookup returns the ratio for a tag in a Connectivity slice.
 152  func tagRatioLookup(tr []spore.TagRatio, tag string) ratio.Ratio {
 153  	for _, t := range tr {
 154  		if t.Tag == tag {
 155  			return t.Value
 156  		}
 157  	}
 158  	return ratio.Zero
 159  }
 160