package crypto import ( "testing" "git.mleku.dev/mleku/dendrite/pkg/ratio" ) func TestPrepareExchange(t *testing.T) { params := DefaultParams(Security128) tags := []string{"word", "punct"} kp, err := Generate(params, tags, testFactory) if err != nil { t.Fatalf("Generate: %v", err) } // Create a peer spore. kpPeer, err := Generate(params, tags, testFactory) if err != nil { t.Fatalf("Generate(peer): %v", err) } msg, err := PrepareExchange(&kp.Private, kp.Public.Spore, kpPeer.Public.Spore, params) if err != nil { t.Fatalf("PrepareExchange: %v", err) } if msg == nil { t.Fatal("ephemeral message should not be nil") } if msg.Signature == nil { t.Error("signature should not be nil") } if len(msg.Pattern) == 0 { t.Error("pattern should not be empty") } if msg.SenderFingerprint.Hash == "" { t.Error("sender fingerprint should not be empty") } } func TestExchangeV2FullProtocol(t *testing.T) { params := DefaultParams(Security128) tags := []string{"word", "punct"} // Generate two keypairs. kpAlice, err := Generate(params, tags, testFactory) if err != nil { t.Fatalf("Generate(Alice): %v", err) } kpBob, err := Generate(params, tags, testFactory) if err != nil { t.Fatalf("Generate(Bob): %v", err) } // Alice prepares her message for Bob. aliceMsg, err := PrepareExchange( &kpAlice.Private, kpAlice.Public.Spore, kpBob.Public.Spore, params, ) if err != nil { t.Fatalf("PrepareExchange(Alice): %v", err) } // Bob prepares his message for Alice. bobMsg, err := PrepareExchange( &kpBob.Private, kpBob.Public.Spore, kpAlice.Public.Spore, params, ) if err != nil { t.Fatalf("PrepareExchange(Bob): %v", err) } // Alice completes the exchange using Bob's message. aliceSecret, err := CompleteExchange( kpAlice.Public.Spore, kpBob.Public.Spore, aliceMsg, bobMsg, params, ) if err != nil { t.Fatalf("CompleteExchange(Alice): %v", err) } // Bob completes the exchange using Alice's message. bobSecret, err := CompleteExchange( kpBob.Public.Spore, kpAlice.Public.Spore, bobMsg, aliceMsg, params, ) if err != nil { t.Fatalf("CompleteExchange(Bob): %v", err) } // Both should be authenticated. if !aliceSecret.Authenticated { t.Error("Alice's secret should be authenticated") } if !bobSecret.Authenticated { t.Error("Bob's secret should be authenticated") } // Both should have common tags. if len(aliceSecret.CommonTags) == 0 { t.Error("should have common tags") } // Confidence should be positive. if !aliceSecret.Confidence.IsPositive() { t.Error("Alice's confidence should be positive") } if !bobSecret.Confidence.IsPositive() { t.Error("Bob's confidence should be positive") } // Secrets should not be zero. if aliceSecret.Secret == (Hamadryad{}) { t.Error("Alice's secret should not be zero") } if bobSecret.Secret == (Hamadryad{}) { t.Error("Bob's secret should not be zero") } } func TestExchangeV2NilInputs(t *testing.T) { params := DefaultParams(Security128) tags := []string{"word"} kp, _ := Generate(params, tags, testFactory) kpPeer, _ := Generate(params, tags, testFactory) // Nil private key. _, err := PrepareExchange(nil, kp.Public.Spore, kpPeer.Public.Spore, params) if err == nil { t.Error("expected error with nil private key") } // Nil own spore. _, err = PrepareExchange(&kp.Private, nil, kpPeer.Public.Spore, params) if err == nil { t.Error("expected error with nil own spore") } // Nil peer spore. _, err = PrepareExchange(&kp.Private, kp.Public.Spore, nil, params) if err == nil { t.Error("expected error with nil peer spore") } } func TestExchangeV2RejectsTamperedSignature(t *testing.T) { params := DefaultParams(Security128) tags := []string{"word", "punct"} kpAlice, _ := Generate(params, tags, testFactory) kpBob, _ := Generate(params, tags, testFactory) aliceMsg, _ := PrepareExchange( &kpAlice.Private, kpAlice.Public.Spore, kpBob.Public.Spore, params, ) bobMsg, _ := PrepareExchange( &kpBob.Private, kpBob.Public.Spore, kpAlice.Public.Spore, params, ) // Tamper with Bob's signature. bobMsg.Signature.Challenge[0] ^= 0xFF _, err := CompleteExchange( kpAlice.Public.Spore, kpBob.Public.Spore, aliceMsg, bobMsg, params, ) if err == nil { t.Error("should reject tampered signature") } } func TestExchangeV2RejectsMITM(t *testing.T) { // MITM attack: Eve intercepts and replaces Bob's message with her own. params := DefaultParams(Security128) tags := []string{"word", "punct"} kpAlice, _ := Generate(params, tags, testFactory) kpBob, _ := Generate(params, tags, testFactory) kpEve, _ := Generate(params, tags, testFactory) aliceMsg, _ := PrepareExchange( &kpAlice.Private, kpAlice.Public.Spore, kpBob.Public.Spore, params, ) // Eve creates a message pretending to be for Alice, but using Eve's key. eveMsg, _ := PrepareExchange( &kpEve.Private, kpEve.Public.Spore, kpAlice.Public.Spore, params, ) // Alice tries to complete with Eve's message (thinking it's Bob's). _, err := CompleteExchange( kpAlice.Public.Spore, kpBob.Public.Spore, aliceMsg, eveMsg, params, ) // This should fail because Eve's signature was made with Eve's key, // not Bob's key. Alice verifies against Bob's fingerprint. if err == nil { t.Error("MITM attack should be rejected: Eve's signature doesn't verify as Bob's") } } func TestExchangeV2EavesdropperCannotDerive(t *testing.T) { // An eavesdropper who sees both spores (public) cannot derive the // shared secret because they cannot produce the bonding patterns // (which require the private constraint factories). params := DefaultParams(Security128) tags := []string{"word", "punct"} kpAlice, _ := Generate(params, tags, testFactory) kpBob, _ := Generate(params, tags, testFactory) // The eavesdropper has both public spores. // They can compute the old (broken) Exchange. eaveSecret, err := Exchange(kpAlice.Public.Spore, kpBob.Public.Spore) if err != nil { t.Fatalf("eavesdropper Exchange: %v", err) } // The eavesdropper's secret is derived purely from public data. // The authenticated ExchangeV2 requires the private key to produce // the ephemeral bonding patterns. // We just verify the eavesdropper can compute SOMETHING (the broken v1) // but it won't match the authenticated v2 secret. if eaveSecret.Secret == (Hamadryad{}) && len(eaveSecret.CommonTags) > 0 { t.Error("eavesdropper should be able to compute v1 secret from public data") } // The v2 secret requires private key operations that the eavesdropper // cannot perform. This is proven by the fact that PrepareExchange // requires a private key, and CompleteExchange verifies signatures. } func TestExchangeV2DeriveKey(t *testing.T) { params := DefaultParams(Security128) tags := []string{"word", "punct"} kpAlice, _ := Generate(params, tags, testFactory) kpBob, _ := Generate(params, tags, testFactory) aliceMsg, _ := PrepareExchange( &kpAlice.Private, kpAlice.Public.Spore, kpBob.Public.Spore, params, ) bobMsg, _ := PrepareExchange( &kpBob.Private, kpBob.Public.Spore, kpAlice.Public.Spore, params, ) aliceResult, err := CompleteExchange( kpAlice.Public.Spore, kpBob.Public.Spore, aliceMsg, bobMsg, params, ) if err != nil { t.Fatalf("CompleteExchange(Alice): %v", err) } // Derive a key from the authenticated secret. ss := &SharedSecret{ CommonTags: aliceResult.CommonTags, Secret: aliceResult.Secret, Confidence: aliceResult.Confidence, } key := DeriveKey(ss, "aes-256", 32) if len(key) != 32 { t.Errorf("key length = %d, want 32", len(key)) } // Key should be non-zero. allZero := true for _, b := range key { if b != 0 { allZero = false break } } if allZero { t.Error("derived key should not be all zeros") } } func TestExchangeV2InvalidParams(t *testing.T) { tags := []string{"word"} params := DefaultParams(Security128) kp, _ := Generate(params, tags, testFactory) kpPeer, _ := Generate(params, tags, testFactory) badParams := Params{} // invalid _, err := PrepareExchange(&kp.Private, kp.Public.Spore, kpPeer.Public.Spore, badParams) if err == nil { t.Error("expected error with invalid params") } } func TestExchangeV2Confidence(t *testing.T) { params := DefaultParams(Security128) // Same tags → confidence should be 1/1. kp1, _ := Generate(params, []string{"word", "punct"}, testFactory) kp2, _ := Generate(params, []string{"word", "punct"}, testFactory) msg1, _ := PrepareExchange(&kp1.Private, kp1.Public.Spore, kp2.Public.Spore, params) msg2, _ := PrepareExchange(&kp2.Private, kp2.Public.Spore, kp1.Public.Spore, params) result, err := CompleteExchange(kp1.Public.Spore, kp2.Public.Spore, msg1, msg2, params) if err != nil { t.Fatalf("CompleteExchange: %v", err) } if !result.Confidence.Equal(ratio.One) { t.Errorf("same tags: confidence = %s, want 1/1", result.Confidence) } }