diff --git a/internal/identity/BUILD.bazel b/internal/identity/BUILD.bazel index f9625fc..918cd53 100644 --- a/internal/identity/BUILD.bazel +++ b/internal/identity/BUILD.bazel @@ -1,6 +1,6 @@ # SPDX-License-Identifier: Apache-2.0 -load("@rules_go//go:def.bzl", "go_library") +load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "identity", @@ -12,3 +12,9 @@ go_library( "@org_golang_google_protobuf//reflect/protoreflect", ], ) + +go_test( + name = "identity_test", + srcs = ["identity_test.go"], + embed = [":identity"], +) diff --git a/internal/identity/identity.go b/internal/identity/identity.go index 1c7a83f..e961d49 100644 --- a/internal/identity/identity.go +++ b/internal/identity/identity.go @@ -29,17 +29,33 @@ type Identity struct { // Generate creates a new self-signed X.509 certificate and ECDSA private key. func Generate(shortName string) (*Identity, error) { - priv, err := newPrivateKey() + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to generate private key: %w", err) } - template, err := newCertificateTemplate(shortName) + notBefore := time.Now() + notAfter := notBefore.Add(365 * 24 * time.Hour) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to generate serial number: %w", err) } - derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: shortName, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return nil, fmt.Errorf("failed to create certificate: %w", err) } @@ -151,37 +167,6 @@ func MarshalCertificate(cert *x509.Certificate) []byte { return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) } -func newPrivateKey() (*ecdsa.PrivateKey, error) { - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, fmt.Errorf("failed to generate private key: %w", err) - } - return priv, nil -} - -func newCertificateTemplate(shortName string) (*x509.Certificate, error) { - notBefore := time.Now() - notAfter := notBefore.Add(365 * 24 * time.Hour) - - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - return nil, fmt.Errorf("failed to generate serial number: %w", err) - } - - return &x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - CommonName: shortName, - }, - NotBefore: notBefore, - NotAfter: notAfter, - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, - BasicConstraintsValid: true, - }, nil -} - // UnmarshalCertificate imports a certificate from PEM format. func UnmarshalCertificate(data []byte) (*x509.Certificate, error) { block, _ := pem.Decode(data) diff --git a/internal/identity/identity_test.go b/internal/identity/identity_test.go index 21dee9a..b355750 100644 --- a/internal/identity/identity_test.go +++ b/internal/identity/identity_test.go @@ -3,27 +3,85 @@ package identity import ( + "bytes" + "encoding/pem" "testing" ) -func TestGenerate(t *testing.T) { - shortName := "test-agent" - ident, err := Generate(shortName) - if err != nil { - t.Fatalf("Generate() failed: %v", err) - } - - if ident == nil { - t.Fatal("Generate() returned nil identity") - } - - if ident.Certificate == nil { - t.Error("Generate() returned identity with nil certificate") - } else if ident.Certificate.Subject.CommonName != shortName { - t.Errorf("Generate() certificate common name = %q, want %q", ident.Certificate.Subject.CommonName, shortName) - } - - if ident.PrivateKey == nil { - t.Error("Generate() returned identity with nil private key") - } +func TestUnmarshalCertificate(t *testing.T) { + // 1. Success case: Generate a valid identity and round-trip its certificate. + t.Run("Success", func(t *testing.T) { + id, err := Generate("test-agent") + if err != nil { + t.Fatalf("Failed to generate identity: %v", err) + } + + pemData := MarshalCertificate(id.Certificate) + unmarshaled, err := UnmarshalCertificate(pemData) + if err != nil { + t.Fatalf("UnmarshalCertificate failed: %v", err) + } + + if !bytes.Equal(id.Certificate.Raw, unmarshaled.Raw) { + t.Errorf("Unmarshaled certificate RAW bytes do not match original") + } + if id.Certificate.Subject.CommonName != unmarshaled.Subject.CommonName { + t.Errorf("Expected Subject %q, got %q", id.Certificate.Subject.CommonName, unmarshaled.Subject.CommonName) + } + }) + + // 2. Failure: Invalid PEM block. + t.Run("InvalidPEM", func(t *testing.T) { + invalidPEM := []byte("this is not a PEM block") + cert, err := UnmarshalCertificate(invalidPEM) + if err == nil { + t.Errorf("Expected error for invalid PEM data, got nil") + } + if cert != nil { + t.Errorf("Expected nil certificate for invalid PEM data") + } + }) + + // 3. Failure: Empty input. + t.Run("EmptyInput", func(t *testing.T) { + cert, err := UnmarshalCertificate([]byte{}) + if err == nil { + t.Errorf("Expected error for empty input, got nil") + } + if cert != nil { + t.Errorf("Expected nil certificate for empty input") + } + }) + + // 4. Failure: Valid PEM but wrong type. + t.Run("WrongPEMType", func(t *testing.T) { + wrongBlock := &pem.Block{ + Type: "NOT A CERTIFICATE", + Bytes: []byte{0xDE, 0xAD, 0xBE, 0xEF}, + } + wrongData := pem.EncodeToMemory(wrongBlock) + _, err := UnmarshalCertificate(wrongData) + // UnmarshalCertificate doesn't explicitly check the Type field, + // it just calls x509.ParseCertificate(block.Bytes). + // So it will likely fail during parsing. + if err == nil { + t.Errorf("Expected error for wrong PEM type / invalid DER, got nil") + } + }) + + // 5. Failure: Valid PEM "CERTIFICATE" but invalid DER bytes. + t.Run("InvalidDER", func(t *testing.T) { + invalidDERBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: []byte("definitely not a valid X.509 DER certificate"), + } + invalidData := pem.EncodeToMemory(invalidDERBlock) + cert, err := UnmarshalCertificate(invalidData) + if err == nil { + t.Errorf("Expected error for invalid DER bytes, got nil") + } + if cert != nil { + t.Errorf("Expected nil certificate for invalid DER bytes") + } + }) } diff --git a/internal/names/names.go b/internal/names/names.go index bcf764b..f595031 100644 --- a/internal/names/names.go +++ b/internal/names/names.go @@ -4,10 +4,15 @@ package names import ( "math/rand" + "time" ) var loadedNames []string +func init() { + rand.Seed(time.Now().UnixNano()) +} + // GenerateForIndex returns a random name from the pre-loaded list. // It prefers names starting with 'A' for index 0, 'B' for index 1, etc. func GenerateForIndex(index int) string { diff --git a/internal/paxos/cell_test.go b/internal/paxos/cell_test.go index 54c8e61..6c1c19c 100644 --- a/internal/paxos/cell_test.go +++ b/internal/paxos/cell_test.go @@ -195,38 +195,3 @@ func TestCell_Propose_LockCheckerError(t *testing.T) { t.Errorf("expected lock checker to be called exactly once, got %d. This indicates backoff.Permanent is not preventing retries.", callCount) } } - -func TestCell_SetSelfAddress(t *testing.T) { - tmpDir := t.TempDir() - - store, err := state.NewStore(tmpDir) - if err != nil { - t.Fatalf("failed to create store: %v", err) - } - defer store.Close() - - agentID := "agent-1" - acceptor := NewAcceptor(agentID, nil, store) - - cell := NewCell(agentID, store, nil, acceptor, nil, "initial-grpc", "initial-http") - - // Verify initial state - if cell.selfGRPCAddr != "initial-grpc" { - t.Errorf("expected initial grpc addr 'initial-grpc', got %q", cell.selfGRPCAddr) - } - if cell.selfHTTPURL != "initial-http" { - t.Errorf("expected initial http url 'initial-http', got %q", cell.selfHTTPURL) - } - - newGRPC := "new-grpc:50051" - newHTTP := "http://new-http:8080" - cell.SetSelfAddress(newGRPC, newHTTP) - - // Verify updated state - if cell.selfGRPCAddr != newGRPC { - t.Errorf("expected updated grpc addr %q, got %q", newGRPC, cell.selfGRPCAddr) - } - if cell.selfHTTPURL != newHTTP { - t.Errorf("expected updated http url %q, got %q", newHTTP, cell.selfHTTPURL) - } -}