exchange_v2.go raw

   1  package crypto
   2  
   3  import (
   4  	"context"
   5  	"errors"
   6  	"sort"
   7  
   8  	"git.mleku.dev/mleku/dendrite/pkg/axiom"
   9  	"git.mleku.dev/mleku/dendrite/pkg/grow"
  10  	"git.mleku.dev/mleku/dendrite/pkg/ratio"
  11  	"git.mleku.dev/mleku/dendrite/pkg/spore"
  12  )
  13  
  14  // EphemeralMessage is one party's contribution to the authenticated exchange.
  15  // It contains a bonding pattern produced by nucleating from the peer's spore
  16  // using the sender's private constraint factory, signed by the sender.
  17  type EphemeralMessage struct {
  18  	// Pattern is the bonding pattern from nucleating the peer's spore
  19  	// with the sender's constraint factory.
  20  	Pattern []SiteMark
  21  
  22  	// Signature proves the sender produced this pattern using their
  23  	// private key (constraint factory).
  24  	Signature *Signature
  25  
  26  	// SenderFingerprint identifies who produced this message.
  27  	SenderFingerprint SporeFingerprint
  28  }
  29  
  30  // AuthenticatedSecret is the result of a signed ephemeral exchange.
  31  type AuthenticatedSecret struct {
  32  	// CommonTags from both spores.
  33  	CommonTags []string
  34  
  35  	// Secret is the derived Hamadryad shared key material.
  36  	Secret Hamadryad
  37  
  38  	// Confidence measures structural overlap.
  39  	Confidence ratio.Ratio
  40  
  41  	// Authenticated indicates both signatures verified.
  42  	Authenticated bool
  43  }
  44  
  45  // PrepareExchange generates an ephemeral message for the peer.
  46  // The sender nucleates a lattice from the peer's spore using their own
  47  // constraint factory, bonds a challenge derived from both fingerprints,
  48  // and signs the result.
  49  //
  50  // This is step 1-2 of the protocol: generate + sign.
  51  func PrepareExchange(
  52  	privkey *PrivateKey,
  53  	ownSpore *spore.Spore,
  54  	peerSpore *spore.Spore,
  55  	params Params,
  56  ) (*EphemeralMessage, error) {
  57  	if privkey == nil || privkey.Lattice == nil {
  58  		return nil, errors.New("crypto: nil private key")
  59  	}
  60  	if ownSpore == nil || peerSpore == nil {
  61  		return nil, errors.New("crypto: nil spore in exchange")
  62  	}
  63  	if !params.Valid() {
  64  		return nil, errors.New("crypto: invalid parameters")
  65  	}
  66  
  67  	// 1. Derive ephemeral challenge from both fingerprints.
  68  	// This ensures both parties derive the same challenge deterministically.
  69  	ownFP := FingerprintFromSpore(ownSpore)
  70  	peerFP := FingerprintFromSpore(peerSpore)
  71  	challenge := deriveExchangeChallenge(ownFP, peerFP)
  72  
  73  	// 2. Nucleate a fresh lattice from the peer's spore.
  74  	nucleated := peerSpore.Nucleate(
  75  		params.N/2, // half-size for ephemeral use
  76  		privkey.ConstraintFactory,
  77  	)
  78  
  79  	// 3. Bond challenge elements into the nucleated lattice.
  80  	s := spore.Extract(nucleated)
  81  	tags := sortedTags(s.TypeSignature)
  82  	if len(tags) == 0 {
  83  		return nil, errors.New("crypto: nucleated lattice has no constraint types")
  84  	}
  85  
  86  	solution := make(chan axiom.Element, len(challenge))
  87  	for i, b := range challenge {
  88  		solution <- MessageElement{
  89  			Index:   i,
  90  			Byte:    b,
  91  			TypeTag: tags[i%len(tags)],
  92  		}
  93  	}
  94  	close(solution)
  95  
  96  	events := make(chan grow.Event, len(challenge)*2)
  97  	ctx := context.Background()
  98  	grow.Run(ctx, nucleated, solution, grow.Config{
  99  		MaxSteps: params.MaxWalkSteps,
 100  		Workers:  2,
 101  	}, events)
 102  	close(events)
 103  	for range events {
 104  	}
 105  
 106  	// 4. Extract bonding pattern.
 107  	pattern := snapshot(nucleated)
 108  
 109  	// 5. Sign the pattern as a message.
 110  	// We sign the challenge+pattern hash to bind them together.
 111  	var signBuf []byte
 112  	signBuf = append(signBuf, []byte("dendrite-exchange-ephemeral-v2")...)
 113  	signBuf = append(signBuf, challenge[:]...)
 114  	for _, site := range pattern {
 115  		if site.Occupied {
 116  			signBuf = append(signBuf, []byte(site.TypeTag)...)
 117  			signBuf = append(signBuf, byte(site.Projection))
 118  			signBuf = append(signBuf, byte(site.Perm))
 119  		}
 120  	}
 121  
 122  	sig, err := Sign(privkey, signBuf, params)
 123  	if err != nil {
 124  		return nil, err
 125  	}
 126  
 127  	return &EphemeralMessage{
 128  		Pattern:           pattern,
 129  		Signature:         sig,
 130  		SenderFingerprint: ownFP,
 131  	}, nil
 132  }
 133  
 134  // CompleteExchange verifies the peer's ephemeral message and derives
 135  // the shared secret. Both parties must call this with the other's message.
 136  //
 137  // This is steps 3-5 of the protocol: verify + derive.
 138  func CompleteExchange(
 139  	ownSpore *spore.Spore,
 140  	peerSpore *spore.Spore,
 141  	ownMessage *EphemeralMessage,
 142  	peerMessage *EphemeralMessage,
 143  	params Params,
 144  ) (*AuthenticatedSecret, error) {
 145  	if ownSpore == nil || peerSpore == nil {
 146  		return nil, errors.New("crypto: nil spore")
 147  	}
 148  	if ownMessage == nil || peerMessage == nil {
 149  		return nil, errors.New("crypto: nil ephemeral message")
 150  	}
 151  
 152  	// 1. Verify the peer's signature.
 153  	peerFP := FingerprintFromSpore(peerSpore)
 154  	ownFP := FingerprintFromSpore(ownSpore)
 155  
 156  	// Reconstruct what the peer should have signed.
 157  	challenge := deriveExchangeChallenge(peerFP, ownFP)
 158  	var signBuf []byte
 159  	signBuf = append(signBuf, []byte("dendrite-exchange-ephemeral-v2")...)
 160  	signBuf = append(signBuf, challenge[:]...)
 161  	for _, site := range peerMessage.Pattern {
 162  		if site.Occupied {
 163  			signBuf = append(signBuf, []byte(site.TypeTag)...)
 164  			signBuf = append(signBuf, byte(site.Projection))
 165  			signBuf = append(signBuf, byte(site.Perm))
 166  		}
 167  	}
 168  
 169  	if !Verify(peerFP, signBuf, peerMessage.Signature) {
 170  		return nil, errors.New("crypto: peer signature verification failed")
 171  	}
 172  
 173  	// 2. Find common tags.
 174  	ownTags := tagSet(ownSpore.TypeSignature)
 175  	peerTags := tagSet(peerSpore.TypeSignature)
 176  
 177  	var common []string
 178  	for tag := range ownTags {
 179  		if peerTags[tag] {
 180  			common = append(common, tag)
 181  		}
 182  	}
 183  	sort.Strings(common)
 184  
 185  	if len(common) == 0 {
 186  		return &AuthenticatedSecret{
 187  			Confidence:    ratio.Zero,
 188  			Authenticated: true,
 189  		}, nil
 190  	}
 191  
 192  	// 3. Compute confidence.
 193  	maxTags := max(len(ownTags), len(peerTags))
 194  	confidence := ratio.New(int64(len(common)), int64(maxTags))
 195  
 196  	// 4. Derive shared secret from authenticated patterns.
 197  	// The secret combines both parties' bonding patterns with the
 198  	// exchange challenge. An eavesdropper cannot produce these patterns
 199  	// because they require the constraint factory (private key).
 200  	var buf []byte
 201  	buf = append(buf, []byte("dendrite-exchange-v2-secret")...)
 202  
 203  	// Own contribution (sorted for determinism).
 204  	ownChallenge := deriveExchangeChallenge(ownFP, peerFP)
 205  	buf = append(buf, ownChallenge[:]...)
 206  	for _, site := range ownMessage.Pattern {
 207  		if site.Occupied {
 208  			buf = append(buf, site.ValueHash[:]...)
 209  		}
 210  	}
 211  
 212  	// Peer contribution.
 213  	buf = append(buf, challenge[:]...) // peer's challenge
 214  	for _, site := range peerMessage.Pattern {
 215  		if site.Occupied {
 216  			buf = append(buf, site.ValueHash[:]...)
 217  		}
 218  	}
 219  
 220  	secret := Hash(buf)
 221  
 222  	return &AuthenticatedSecret{
 223  		CommonTags:    common,
 224  		Secret:        secret,
 225  		Confidence:    confidence,
 226  		Authenticated: true,
 227  	}, nil
 228  }
 229  
 230  // deriveExchangeChallenge produces a deterministic challenge from two
 231  // fingerprints. Uses min/max ordering of hashes for commutativity —
 232  // but the caller must ensure correct order for the signature binding.
 233  func deriveExchangeChallenge(sender, receiver SporeFingerprint) Hamadryad {
 234  	var buf []byte
 235  	buf = append(buf, []byte("dendrite-exchange-challenge-v2")...)
 236  	// Use sender-then-receiver ordering (NOT commutative here —
 237  	// each party uses their own perspective).
 238  	buf = append(buf, []byte(sender.Hash)...)
 239  	buf = append(buf, []byte(receiver.Hash)...)
 240  	return Hash(buf)
 241  }
 242