package mtls import ( "context" "crypto/tls" "crypto/x509" "encoding/pem" "testing" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/peer" "google.golang.org/grpc/status" ) // contextWithTLSPeer builds a context carrying a gRPC peer whose TLS state // includes the given verified certificate chain (as the TLS stack would set // after a successful mTLS handshake). func contextWithTLSPeer(chains [][]*x509.Certificate) context.Context { state := tls.ConnectionState{VerifiedChains: chains} p := &peer.Peer{AuthInfo: credentials.TLSInfo{State: state}} return peer.NewContext(context.Background(), p) } // okHandler is a trivial gRPC handler that returns ("ok", nil). func okHandler(_ context.Context, _ interface{}) (interface{}, error) { return "ok", nil } // ─── Unary interceptor ─────────────────────────────────────────────────────── func TestUnaryInterceptor_EnrollWhitelisted(t *testing.T) { interceptor := UnaryServerInterceptor() // Enroll must pass through even without any peer/TLS info. info := &grpc.UnaryServerInfo{FullMethod: enrollFullMethod} _, err := interceptor(context.Background(), nil, info, okHandler) if err != nil { t.Errorf("Enroll should be whitelisted, got error: %v", err) } } func TestUnaryInterceptor_NoPeer_Unauthenticated(t *testing.T) { interceptor := UnaryServerInterceptor() info := &grpc.UnaryServerInfo{FullMethod: "/pangolin.agent.v1.AgentService/Heartbeat"} // Context has no peer at all. _, err := interceptor(context.Background(), nil, info, okHandler) if err == nil { t.Fatal("expected Unauthenticated, got nil") } if code := status.Code(err); code != codes.Unauthenticated { t.Errorf("status code = %v; want Unauthenticated", code) } } func TestUnaryInterceptor_NoCert_Unauthenticated(t *testing.T) { interceptor := UnaryServerInterceptor() // Peer present but no verified chains (client sent no cert). ctx := contextWithTLSPeer(nil) info := &grpc.UnaryServerInfo{FullMethod: "/pangolin.agent.v1.AgentService/Heartbeat"} _, err := interceptor(ctx, nil, info, okHandler) if err == nil { t.Fatal("expected Unauthenticated with no cert, got nil") } if code := status.Code(err); code != codes.Unauthenticated { t.Errorf("status code = %v; want Unauthenticated", code) } } func TestUnaryInterceptor_ValidCert_InjectsUUID(t *testing.T) { dir := t.TempDir() ca := newTestCA(t, dir) csrPEM, _ := newTestCSR(t) nodeUUID := "node-deadbeef-cafe" certPEM, err := ca.SignCSR(csrPEM, nodeUUID) if err != nil { t.Fatalf("SignCSR: %v", err) } block, _ := pem.Decode(certPEM) clientCert, _ := x509.ParseCertificate(block.Bytes) pool := x509.NewCertPool() pool.AddCert(ca.CACert()) chains, err := clientCert.Verify(x509.VerifyOptions{ Roots: pool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, }) if err != nil { t.Fatalf("cert.Verify: %v", err) } ctx := contextWithTLSPeer(chains) info := &grpc.UnaryServerInfo{FullMethod: "/pangolin.agent.v1.AgentService/Heartbeat"} interceptor := UnaryServerInterceptor() var capturedCtx context.Context captureHandler := func(ctx context.Context, req interface{}) (interface{}, error) { capturedCtx = ctx return "ok", nil } _, err = interceptor(ctx, nil, info, captureHandler) if err != nil { t.Fatalf("interceptor with valid cert: %v", err) } got, ok := NodeUUIDFromContext(capturedCtx) if !ok { t.Fatal("NodeUUIDFromContext: not found in context") } if got != nodeUUID { t.Errorf("NodeUUIDFromContext = %q; want %q", got, nodeUUID) } } // ─── Stream interceptor (whitelist only) ───────────────────────────────────── // mockServerStream is a minimal grpc.ServerStream for testing. type mockServerStream struct { grpc.ServerStream ctx context.Context } func (m *mockServerStream) Context() context.Context { return m.ctx } func TestStreamInterceptor_EnrollWhitelisted(t *testing.T) { interceptor := StreamServerInterceptor() info := &grpc.StreamServerInfo{FullMethod: enrollFullMethod} ss := &mockServerStream{ctx: context.Background()} err := interceptor(nil, ss, info, func(_ interface{}, _ grpc.ServerStream) error { return nil }) if err != nil { t.Errorf("Enroll stream should be whitelisted, got: %v", err) } } func TestStreamInterceptor_NoCert_Unauthenticated(t *testing.T) { interceptor := StreamServerInterceptor() info := &grpc.StreamServerInfo{FullMethod: "/pangolin.agent.v1.AgentService/StreamEvents"} ss := &mockServerStream{ctx: contextWithTLSPeer(nil)} err := interceptor(nil, ss, info, func(_ interface{}, _ grpc.ServerStream) error { return nil }) if err == nil { t.Fatal("expected Unauthenticated, got nil") } if code := status.Code(err); code != codes.Unauthenticated { t.Errorf("status code = %v; want Unauthenticated", code) } } // ─── NodeUUIDFromContext ────────────────────────────────────────────────────── func TestNodeUUIDFromContext_Empty(t *testing.T) { _, ok := NodeUUIDFromContext(context.Background()) if ok { t.Error("NodeUUIDFromContext on bare context should return ok=false") } }