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