package crypto import ( "context" "errors" "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" "git.mleku.dev/mleku/dendrite/pkg/state" ) // SporeFingerprint is the compact public identity of a signer. // It can be distributed independently of the full spore. type SporeFingerprint struct { TypeSignature []spore.TagCount `json:"type_sig"` PermDist [6]int `json:"perm_dist"` ProjDist [64]int `json:"proj_dist"` Connectivity []spore.TagRatio `json:"connectivity"` Hash string `json:"hash"` } // SporeProof demonstrates that a bonding pattern was produced by // a lattice with the claimed constraint envelope. type SporeProof struct { // LockIns at each bonded site — achievable only with correct constraints. LockIns []ratio.Ratio // NeighborCounts — structural context proving neighborhood awareness. NeighborCounts []int // HexTrace — hexagram states proving consistent lattice dynamics. HexTrace []state.Hexagram } // Signature proves a message was signed by a lattice with the // claimed constraint envelope. type Signature struct { Fingerprint SporeFingerprint Challenge Hamadryad // Hamadryad hash of message Response []SiteMark // bonding pattern of challenge data Proof SporeProof // structural proof of lattice ownership Commitment Hamadryad // Hash(challenge || response) binding } // Sign produces a signature by crystallizing the message challenge // into a disposable clone of the signer's lattice. The private key's // lattice is never mutated. // // Algorithm: // 1. Compute challenge = Hamadryad(message) // 2. Clone the private lattice (topology + occupancy preserved) // 3. Decompose challenge bytes into elements // 4. Bond elements into the clone via Brownian walk // 5. Extract the bonding pattern as Response // 6. Record lock-in depths and hexagram states as Proof // 7. Extract SporeFingerprint from the original lattice state func Sign(privkey *PrivateKey, message []byte, params Params) (*Signature, error) { if privkey.Lattice == nil { return nil, errors.New("crypto: private key has no lattice") } if !params.Valid() { return nil, errors.New("crypto: invalid parameters") } // 1. Challenge. challenge := Hash(message) // 2. Clone lattice — bond into clone, keep original untouched. clone := cloneLattice(privkey.Lattice, privkey.ConstraintFactory) // 3. Decompose challenge into elements. s := spore.Extract(privkey.Lattice) tags := sortedTags(s.TypeSignature) if len(tags) == 0 { return nil, errors.New("crypto: 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) // 4. Bond into the clone. events := make(chan grow.Event, len(challenge)*2) ctx := context.Background() cfg := grow.Config{ MaxSteps: params.MaxWalkSteps, Workers: 2, } grow.Run(ctx, clone, solution, cfg, events) close(events) for range events { } // 5. Extract bonding pattern from clone. response := snapshot(clone) // 6. Build proof from clone. var lockIns []ratio.Ratio var neighborCounts []int var hexTrace []state.Hexagram for _, n := range clone.Nodes() { if n.Occupied() { lockIns = append(lockIns, n.LockIn()) neighborCounts = append(neighborCounts, len(n.Neighbors())) hexTrace = append(hexTrace, n.Hexagram()) } } proof := SporeProof{ LockIns: lockIns, NeighborCounts: neighborCounts, HexTrace: hexTrace, } // 7. Fingerprint from original lattice. fp := FingerprintFromSpore(s) commitment := computeCommitment(challenge, response) return &Signature{ Fingerprint: fp, Challenge: challenge, Response: response, Proof: proof, Commitment: commitment, }, nil } // Verify checks a signature against a claimed fingerprint. // // Algorithm: // 1. Recompute challenge = Hamadryad(message) // 2. Verify the challenge matches the signature's challenge // 3. Check bonding pattern consistency with fingerprint // 4. Verify structural proof consistency (lengths, bounds) // 5. Verify lock-in depths are achievable given connectivity // 6. Verify hexagram trace follows valid transition rules // 7. Verify permutation/projection distributions match func Verify(fingerprint SporeFingerprint, message []byte, sig *Signature) bool { if sig == nil { return false } // 1. Recompute challenge. challenge := Hash(message) if challenge != sig.Challenge { return false } // 2. Verify fingerprint matches signature's claimed fingerprint. if fingerprint.Hash != sig.Fingerprint.Hash { return false } // 3. Check that the response has occupied sites. occupied := 0 for _, site := range sig.Response { if site.Occupied { occupied++ } } if occupied == 0 { return false } // Minimum occupied ratio: at least len(challenge)/4 sites must be occupied. // A real lattice with N=256 will bond most of the 32 challenge bytes. minOccupied := max(len(challenge)/4, 1) if occupied < minOccupied { return false } // 4. Structural proof consistency: all proof slices must have equal length. proofLen := len(sig.Proof.LockIns) if len(sig.Proof.NeighborCounts) != proofLen || len(sig.Proof.HexTrace) != proofLen { return false } // Proof length must match the number of occupied sites in the response. if proofLen != occupied { return false } // 5. Verify lock-in depths are positive and bounded by neighbor count. // A site cannot satisfy more constraints than it has neighbors. for i, li := range sig.Proof.LockIns { if !li.IsPositive() { return false } // Lock-in is a ratio; the numerator should not exceed the neighbor count. // Lock-in = satisfied / total, so it's ≤ 1. But we also check the // neighbor count is at least 1 (isolated nodes cannot bond). if sig.Proof.NeighborCounts[i] < 1 { return false } } // 6. Verify hexagram states are valid (all bits within range). for _, h := range sig.Proof.HexTrace { if h > 63 { return false } } // 7. Verify permutation distribution is consistent. sigPermDist := [6]int{} for _, site := range sig.Response { if site.Occupied && site.Perm < 6 { sigPermDist[site.Perm]++ } } if !permDistCompatible(fingerprint.PermDist, sigPermDist) { return false } // 8. Verify response-challenge binding. // The commitment binds the response to the challenge, preventing // challenge-swap attacks where an attacker replaces the challenge // in a valid signature. if sig.Commitment != computeCommitment(challenge, sig.Response) { return false } return true } // permDistCompatible checks whether two permutation distributions are // statistically compatible. Uses a simple chi-squared-like test: // the sum of squared differences should be within tolerance. func permDistCompatible(expected, observed [6]int) bool { totalExpected := 0 totalObserved := 0 for i := range 6 { totalExpected += expected[i] totalObserved += observed[i] } if totalExpected == 0 || totalObserved == 0 { return true // no data to compare } // Normalize and compare proportions. // Tolerance: each proportion can differ by up to 50%. // This is deliberately loose — tighter bounds require more data. for i := range 6 { expProp := ratio.New(int64(expected[i]), int64(totalExpected)) obsProp := ratio.New(int64(observed[i]), int64(totalObserved)) diff := expProp.Sub(obsProp).Abs() if diff.Greater(ratio.Half) { return false } } return true } // FingerprintFromSpore extracts a SporeFingerprint. func FingerprintFromSpore(s *spore.Spore) SporeFingerprint { return SporeFingerprint{ TypeSignature: s.TypeSignature, PermDist: s.PermDist, ProjDist: s.ProjDist, Connectivity: s.Connectivity, Hash: s.Hash(), } } // computeCommitment binds the challenge to the response, preventing // challenge-swap attacks. It hashes the challenge bytes concatenated // with a deterministic serialization of the occupied response sites. func computeCommitment(challenge Hamadryad, response []SiteMark) Hamadryad { var buf []byte buf = append(buf, []byte("dendrite-commitment-v1")...) buf = append(buf, challenge[:]...) for _, site := range response { if site.Occupied { buf = append(buf, []byte(site.TypeTag)...) buf = append(buf, byte(site.Projection)) buf = append(buf, byte(site.Perm)) buf = append(buf, site.ValueHash[:]...) } } return Hash(buf) } // VerifyCompact checks a signature that has been through compact wire // encoding (Marshal/UnmarshalSignature). The compact format is lossy: // - Fingerprint hash is re-hashed through Hamadryad (different string) // - ValueHash is not stored (zeros after round-trip) // - LockIns are quantized to uint8 // - NeighborCounts are set to 1 (aggregate only) // - HexTrace is reconstructed from a histogram (approximate) // // This function checks only the properties that survive compact encoding: // 1. Challenge matches Hamadryad(message) // 2. Occupied site count meets minimum threshold // 3. Permutation distribution is compatible with fingerprint // 4. Structural proof lengths are consistent // // It does NOT check commitment (depends on ValueHash) or fingerprint // hash equality (re-hashed in compact format). func VerifyCompact(fingerprint SporeFingerprint, message []byte, sig *Signature) bool { if sig == nil { return false } // 1. Recompute challenge. challenge := Hash(message) if challenge != sig.Challenge { return false } // 2. Check occupied site count. occupied := 0 for _, site := range sig.Response { if site.Occupied { occupied++ } } if occupied == 0 { return false } minOccupied := max(len(challenge)/4, 1) if occupied < minOccupied { return false } // 3. Structural proof consistency: lengths must match. proofLen := len(sig.Proof.LockIns) if len(sig.Proof.NeighborCounts) != proofLen || len(sig.Proof.HexTrace) != proofLen { return false } if proofLen != occupied { return false } // 4. Permutation distribution compatibility. sigPermDist := [6]int{} for _, site := range sig.Response { if site.Occupied && site.Perm < 6 { sigPermDist[site.Perm]++ } } if !permDistCompatible(fingerprint.PermDist, sigPermDist) { return false } return true } // VerifyCompactV2 checks a signature decoded from the V2 compact wire format. // The V2 format omits Challenge and Fingerprint (both are supplied by the caller). // The decoded sig has synthetic proof fields (constant LockIns, NeighborCounts, HexTrace). // // Verification steps: // 1. Occupied site count meets minimum threshold (len(challenge)/4) // 2. Structural proof lengths are consistent // 3. Permutation distribution is compatible with fingerprint func VerifyCompactV2(fingerprint SporeFingerprint, message []byte, sig *Signature) bool { if sig == nil { return false } // V2 carries a 16-byte truncated challenge for message binding. // Recompute and compare the first 16 bytes. challenge := Hash(message) for i := range 16 { if sig.Challenge[i] != challenge[i] { return false } } // Check occupied site count. occupied := len(sig.Response) // V2 only stores occupied sites if occupied == 0 { return false } minOccupied := max(len(challenge)/4, 1) if occupied < minOccupied { return false } // Structural proof consistency. proofLen := len(sig.Proof.LockIns) if len(sig.Proof.NeighborCounts) != proofLen || len(sig.Proof.HexTrace) != proofLen { return false } if proofLen != occupied { return false } // Permutation distribution compatibility. sigPermDist := [6]int{} for _, site := range sig.Response { if site.Perm < 6 { sigPermDist[site.Perm]++ } } if !permDistCompatible(fingerprint.PermDist, sigPermDist) { return false } return true } // sortedTags returns sorted tag names from a TagCount slice. func sortedTags(tc []spore.TagCount) []string { tags := make([]string, len(tc)) for i, t := range tc { tags[i] = t.Tag } // Sort is stable for deterministic ordering. for i := 1; i < len(tags); i++ { for j := i; j > 0 && tags[j] < tags[j-1]; j-- { tags[j], tags[j-1] = tags[j-1], tags[j] } } return tags }