go-p2p/node/identity_test.go

354 lines
9 KiB
Go
Raw Permalink Normal View History

package node
import (
"os"
"path/filepath"
"testing"
)
// setupTestNodeManager creates a NodeManager with paths in a temp directory.
func setupTestNodeManager(t *testing.T) (*NodeManager, func()) {
tmpDir, err := os.MkdirTemp("", "node-identity-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
keyPath := filepath.Join(tmpDir, "private.key")
configPath := filepath.Join(tmpDir, "node.json")
nm, err := NewNodeManagerWithPaths(keyPath, configPath)
if err != nil {
os.RemoveAll(tmpDir)
t.Fatalf("failed to create node manager: %v", err)
}
cleanup := func() {
os.RemoveAll(tmpDir)
}
return nm, cleanup
}
func TestNodeIdentity(t *testing.T) {
t.Run("NewNodeManager", func(t *testing.T) {
nm, cleanup := setupTestNodeManager(t)
defer cleanup()
if nm.HasIdentity() {
t.Error("new node manager should not have identity")
}
})
t.Run("GenerateIdentity", func(t *testing.T) {
nm, cleanup := setupTestNodeManager(t)
defer cleanup()
err := nm.GenerateIdentity("test-node", RoleDual)
if err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
if !nm.HasIdentity() {
t.Error("node manager should have identity after generation")
}
identity := nm.GetIdentity()
if identity == nil {
t.Fatal("identity should not be nil")
}
if identity.Name != "test-node" {
t.Errorf("expected name 'test-node', got '%s'", identity.Name)
}
if identity.Role != RoleDual {
t.Errorf("expected role Dual, got '%s'", identity.Role)
}
if identity.ID == "" {
t.Error("identity ID should not be empty")
}
if identity.PublicKey == "" {
t.Error("public key should not be empty")
}
})
t.Run("LoadExistingIdentity", func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "node-load-test")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
keyPath := filepath.Join(tmpDir, "private.key")
configPath := filepath.Join(tmpDir, "node.json")
// First, create an identity
nm1, err := NewNodeManagerWithPaths(keyPath, configPath)
if err != nil {
t.Fatalf("failed to create first node manager: %v", err)
}
err = nm1.GenerateIdentity("persistent-node", RoleWorker)
if err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
originalID := nm1.GetIdentity().ID
originalPubKey := nm1.GetIdentity().PublicKey
// Create a new manager - should load existing identity
nm2, err := NewNodeManagerWithPaths(keyPath, configPath)
if err != nil {
t.Fatalf("failed to create second node manager: %v", err)
}
if !nm2.HasIdentity() {
t.Error("second node manager should have loaded existing identity")
}
identity := nm2.GetIdentity()
if identity.ID != originalID {
t.Errorf("expected ID '%s', got '%s'", originalID, identity.ID)
}
if identity.PublicKey != originalPubKey {
t.Error("public key mismatch after reload")
}
})
t.Run("DeriveSharedSecret", func(t *testing.T) {
// Create two node managers with separate temp directories
tmpDir1, _ := os.MkdirTemp("", "node1")
tmpDir2, _ := os.MkdirTemp("", "node2")
defer os.RemoveAll(tmpDir1)
defer os.RemoveAll(tmpDir2)
// Node 1
nm1, err := NewNodeManagerWithPaths(
filepath.Join(tmpDir1, "private.key"),
filepath.Join(tmpDir1, "node.json"),
)
if err != nil {
t.Fatalf("failed to create node manager 1: %v", err)
}
err = nm1.GenerateIdentity("node1", RoleDual)
if err != nil {
t.Fatalf("failed to generate identity 1: %v", err)
}
// Node 2
nm2, err := NewNodeManagerWithPaths(
filepath.Join(tmpDir2, "private.key"),
filepath.Join(tmpDir2, "node.json"),
)
if err != nil {
t.Fatalf("failed to create node manager 2: %v", err)
}
err = nm2.GenerateIdentity("node2", RoleDual)
if err != nil {
t.Fatalf("failed to generate identity 2: %v", err)
}
// Derive shared secrets - should be identical
secret1, err := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey)
if err != nil {
t.Fatalf("failed to derive shared secret from node 1: %v", err)
}
secret2, err := nm2.DeriveSharedSecret(nm1.GetIdentity().PublicKey)
if err != nil {
t.Fatalf("failed to derive shared secret from node 2: %v", err)
}
if len(secret1) != len(secret2) {
t.Errorf("shared secrets have different lengths: %d vs %d", len(secret1), len(secret2))
}
for i := range secret1 {
if secret1[i] != secret2[i] {
t.Error("shared secrets do not match")
break
}
}
})
t.Run("DeleteIdentity", func(t *testing.T) {
nm, cleanup := setupTestNodeManager(t)
defer cleanup()
err := nm.GenerateIdentity("delete-me", RoleDual)
if err != nil {
t.Fatalf("failed to generate identity: %v", err)
}
if !nm.HasIdentity() {
t.Error("should have identity before delete")
}
err = nm.Delete()
if err != nil {
t.Fatalf("failed to delete identity: %v", err)
}
if nm.HasIdentity() {
t.Error("should not have identity after delete")
}
})
}
func TestNodeRoles(t *testing.T) {
tests := []struct {
role NodeRole
expected string
}{
{RoleController, "controller"},
{RoleWorker, "worker"},
{RoleDual, "dual"},
}
for _, tt := range tests {
t.Run(string(tt.role), func(t *testing.T) {
if string(tt.role) != tt.expected {
t.Errorf("expected '%s', got '%s'", tt.expected, string(tt.role))
}
})
}
}
func TestChallengeResponse(t *testing.T) {
t.Run("GenerateChallenge", func(t *testing.T) {
challenge, err := GenerateChallenge()
if err != nil {
t.Fatalf("failed to generate challenge: %v", err)
}
if len(challenge) != ChallengeSize {
t.Errorf("expected challenge size %d, got %d", ChallengeSize, len(challenge))
}
// Ensure challenges are unique (not all zeros)
allZero := true
for _, b := range challenge {
if b != 0 {
allZero = false
break
}
}
if allZero {
t.Error("challenge should not be all zeros")
}
// Generate another and ensure they're different
challenge2, err := GenerateChallenge()
if err != nil {
t.Fatalf("failed to generate second challenge: %v", err)
}
same := true
for i := range challenge {
if challenge[i] != challenge2[i] {
same = false
break
}
}
if same {
t.Error("two generated challenges should be different")
}
})
t.Run("SignAndVerifyChallenge", func(t *testing.T) {
challenge, _ := GenerateChallenge()
sharedSecret := []byte("test-secret-key-32-bytes-long!!")
// Sign the challenge
signature := SignChallenge(challenge, sharedSecret)
if len(signature) == 0 {
t.Error("signature should not be empty")
}
// Verify should succeed with correct parameters
if !VerifyChallenge(challenge, signature, sharedSecret) {
t.Error("verification should succeed with correct parameters")
}
// Verify should fail with wrong challenge
wrongChallenge, _ := GenerateChallenge()
if VerifyChallenge(wrongChallenge, signature, sharedSecret) {
t.Error("verification should fail with wrong challenge")
}
// Verify should fail with wrong secret
wrongSecret := []byte("wrong-secret-key-32-bytes-long!")
if VerifyChallenge(challenge, signature, wrongSecret) {
t.Error("verification should fail with wrong secret")
}
// Verify should fail with tampered signature
tamperedSig := make([]byte, len(signature))
copy(tamperedSig, signature)
tamperedSig[0] ^= 0xFF // Flip bits
if VerifyChallenge(challenge, tamperedSig, sharedSecret) {
t.Error("verification should fail with tampered signature")
}
})
t.Run("SignatureIsDeterministic", func(t *testing.T) {
challenge := []byte("fixed-challenge-for-testing")
sharedSecret := []byte("fixed-secret-key-for-testing")
sig1 := SignChallenge(challenge, sharedSecret)
sig2 := SignChallenge(challenge, sharedSecret)
if len(sig1) != len(sig2) {
t.Fatal("signatures should have same length")
}
for i := range sig1 {
if sig1[i] != sig2[i] {
t.Fatal("signatures should be identical for same inputs")
}
}
})
t.Run("IntegrationWithSharedSecret", func(t *testing.T) {
// Create two nodes and test end-to-end challenge-response
tmpDir1, _ := os.MkdirTemp("", "node-challenge-1")
tmpDir2, _ := os.MkdirTemp("", "node-challenge-2")
defer os.RemoveAll(tmpDir1)
defer os.RemoveAll(tmpDir2)
nm1, _ := NewNodeManagerWithPaths(
filepath.Join(tmpDir1, "private.key"),
filepath.Join(tmpDir1, "node.json"),
)
nm1.GenerateIdentity("challenger", RoleDual)
nm2, _ := NewNodeManagerWithPaths(
filepath.Join(tmpDir2, "private.key"),
filepath.Join(tmpDir2, "node.json"),
)
nm2.GenerateIdentity("responder", RoleDual)
// Challenger generates challenge
challenge, err := GenerateChallenge()
if err != nil {
t.Fatalf("failed to generate challenge: %v", err)
}
// Both derive the same shared secret
secret1, _ := nm1.DeriveSharedSecret(nm2.GetIdentity().PublicKey)
secret2, _ := nm2.DeriveSharedSecret(nm1.GetIdentity().PublicKey)
// Responder signs challenge with their derived secret
response := SignChallenge(challenge, secret2)
// Challenger verifies with their derived secret
if !VerifyChallenge(challenge, response, secret1) {
t.Error("challenge-response should verify with matching shared secrets")
}
})
}