package crypto import ( "context" "errors" "sort" "git.mleku.dev/mleku/dendrite/pkg/axiom" "git.mleku.dev/mleku/dendrite/pkg/grow" "git.mleku.dev/mleku/dendrite/pkg/ratio" "git.mleku.dev/mleku/dendrite/pkg/spore" ) // EphemeralMessage is one party's contribution to the authenticated exchange. // It contains a bonding pattern produced by nucleating from the peer's spore // using the sender's private constraint factory, signed by the sender. type EphemeralMessage struct { // Pattern is the bonding pattern from nucleating the peer's spore // with the sender's constraint factory. Pattern []SiteMark // Signature proves the sender produced this pattern using their // private key (constraint factory). Signature *Signature // SenderFingerprint identifies who produced this message. SenderFingerprint SporeFingerprint } // AuthenticatedSecret is the result of a signed ephemeral exchange. type AuthenticatedSecret struct { // CommonTags from both spores. CommonTags []string // Secret is the derived Hamadryad shared key material. Secret Hamadryad // Confidence measures structural overlap. Confidence ratio.Ratio // Authenticated indicates both signatures verified. Authenticated bool } // PrepareExchange generates an ephemeral message for the peer. // The sender nucleates a lattice from the peer's spore using their own // constraint factory, bonds a challenge derived from both fingerprints, // and signs the result. // // This is step 1-2 of the protocol: generate + sign. func PrepareExchange( privkey *PrivateKey, ownSpore *spore.Spore, peerSpore *spore.Spore, params Params, ) (*EphemeralMessage, error) { if privkey == nil || privkey.Lattice == nil { return nil, errors.New("crypto: nil private key") } if ownSpore == nil || peerSpore == nil { return nil, errors.New("crypto: nil spore in exchange") } if !params.Valid() { return nil, errors.New("crypto: invalid parameters") } // 1. Derive ephemeral challenge from both fingerprints. // This ensures both parties derive the same challenge deterministically. ownFP := FingerprintFromSpore(ownSpore) peerFP := FingerprintFromSpore(peerSpore) challenge := deriveExchangeChallenge(ownFP, peerFP) // 2. Nucleate a fresh lattice from the peer's spore. nucleated := peerSpore.Nucleate( params.N/2, // half-size for ephemeral use privkey.ConstraintFactory, ) // 3. Bond challenge elements into the nucleated lattice. s := spore.Extract(nucleated) tags := sortedTags(s.TypeSignature) if len(tags) == 0 { return nil, errors.New("crypto: nucleated lattice has no constraint types") } solution := make(chan axiom.Element, len(challenge)) for i, b := range challenge { solution <- MessageElement{ Index: i, Byte: b, TypeTag: tags[i%len(tags)], } } close(solution) events := make(chan grow.Event, len(challenge)*2) ctx := context.Background() grow.Run(ctx, nucleated, solution, grow.Config{ MaxSteps: params.MaxWalkSteps, Workers: 2, }, events) close(events) for range events { } // 4. Extract bonding pattern. pattern := snapshot(nucleated) // 5. Sign the pattern as a message. // We sign the challenge+pattern hash to bind them together. var signBuf []byte signBuf = append(signBuf, []byte("dendrite-exchange-ephemeral-v2")...) signBuf = append(signBuf, challenge[:]...) for _, site := range pattern { if site.Occupied { signBuf = append(signBuf, []byte(site.TypeTag)...) signBuf = append(signBuf, byte(site.Projection)) signBuf = append(signBuf, byte(site.Perm)) } } sig, err := Sign(privkey, signBuf, params) if err != nil { return nil, err } return &EphemeralMessage{ Pattern: pattern, Signature: sig, SenderFingerprint: ownFP, }, nil } // CompleteExchange verifies the peer's ephemeral message and derives // the shared secret. Both parties must call this with the other's message. // // This is steps 3-5 of the protocol: verify + derive. func CompleteExchange( ownSpore *spore.Spore, peerSpore *spore.Spore, ownMessage *EphemeralMessage, peerMessage *EphemeralMessage, params Params, ) (*AuthenticatedSecret, error) { if ownSpore == nil || peerSpore == nil { return nil, errors.New("crypto: nil spore") } if ownMessage == nil || peerMessage == nil { return nil, errors.New("crypto: nil ephemeral message") } // 1. Verify the peer's signature. peerFP := FingerprintFromSpore(peerSpore) ownFP := FingerprintFromSpore(ownSpore) // Reconstruct what the peer should have signed. challenge := deriveExchangeChallenge(peerFP, ownFP) var signBuf []byte signBuf = append(signBuf, []byte("dendrite-exchange-ephemeral-v2")...) signBuf = append(signBuf, challenge[:]...) for _, site := range peerMessage.Pattern { if site.Occupied { signBuf = append(signBuf, []byte(site.TypeTag)...) signBuf = append(signBuf, byte(site.Projection)) signBuf = append(signBuf, byte(site.Perm)) } } if !Verify(peerFP, signBuf, peerMessage.Signature) { return nil, errors.New("crypto: peer signature verification failed") } // 2. Find common tags. ownTags := tagSet(ownSpore.TypeSignature) peerTags := tagSet(peerSpore.TypeSignature) var common []string for tag := range ownTags { if peerTags[tag] { common = append(common, tag) } } sort.Strings(common) if len(common) == 0 { return &AuthenticatedSecret{ Confidence: ratio.Zero, Authenticated: true, }, nil } // 3. Compute confidence. maxTags := max(len(ownTags), len(peerTags)) confidence := ratio.New(int64(len(common)), int64(maxTags)) // 4. Derive shared secret from authenticated patterns. // The secret combines both parties' bonding patterns with the // exchange challenge. An eavesdropper cannot produce these patterns // because they require the constraint factory (private key). var buf []byte buf = append(buf, []byte("dendrite-exchange-v2-secret")...) // Own contribution (sorted for determinism). ownChallenge := deriveExchangeChallenge(ownFP, peerFP) buf = append(buf, ownChallenge[:]...) for _, site := range ownMessage.Pattern { if site.Occupied { buf = append(buf, site.ValueHash[:]...) } } // Peer contribution. buf = append(buf, challenge[:]...) // peer's challenge for _, site := range peerMessage.Pattern { if site.Occupied { buf = append(buf, site.ValueHash[:]...) } } secret := Hash(buf) return &AuthenticatedSecret{ CommonTags: common, Secret: secret, Confidence: confidence, Authenticated: true, }, nil } // deriveExchangeChallenge produces a deterministic challenge from two // fingerprints. Uses min/max ordering of hashes for commutativity — // but the caller must ensure correct order for the signature binding. func deriveExchangeChallenge(sender, receiver SporeFingerprint) Hamadryad { var buf []byte buf = append(buf, []byte("dendrite-exchange-challenge-v2")...) // Use sender-then-receiver ordering (NOT commutative here — // each party uses their own perspective). buf = append(buf, []byte(sender.Hash)...) buf = append(buf, []byte(receiver.Hash)...) return Hash(buf) }