m/n/core: factor out gRPC/TLS into rpc and identity libraries

This is an annoying large change, which started its life as me pulling
the 'let's add tests for authentication' thread, and ended up in
unifying a whole bunch of dispersed logic under two new libraries.

Notable changes:

 - m/n/core/identity now contains the NodeCertificate (now called Node)
   and NodeCredentials types. These used to exist in the cluster code,
   but were factored out to prevent loops between the curator, the
   cluster enrolment logic, and other code. They can now be shared by
   nearly all of the node code, removing the need for some conversions
   between subsystems/packages.
 - Alongside Node{,Credentials} types, the identity package contains
   code that creates x509 certificate templates and verifies x509
   certificates, and has functions specific to nodes and users - not
   clients and servers. This allows moving most of the rest of
   certificate checking code into a single set of functions, and allows
   us to test this logic thoroughly.
 - pki.{Client,Server,CA} are not used by the node core code anymore,
   and can now be moved to kubernetes-specific code (as that was their
   original purpose and that's their only current use).
 - m/n/core/rpc has been refactored to deduplicate code between the
   local/external gRPC servers and unary/stream interceptors for these
   servers, also allowing for more thorough testing and unified
   behaviour between all.
 - A PeerInfo structure is now injected into all gRPC handlers, and is
   unified to contain information both about nodes, users, and possibly
   unauthenticated callers.
 - The AAA.Escrow implementation now makes use of PeerInfo in order to
   retrieve the client's certificate, instead of rolling its own logic.
 - The EphemeralClusterCredentials test helper has been moved to the rpc
   library, and now returns identity objects, allowing for simplified
   test code (less juggling of bare public keys and
   {x509,tls}.Certificate objects).

Change-Id: I9284966b4f18c0d7628167ca3168b4b4037808c1
Reviewed-on: https://review.monogon.dev/c/monogon/+/325
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/node/core/rpc/server.go b/metropolis/node/core/rpc/server.go
index 3d6917d..70a0a74 100644
--- a/metropolis/node/core/rpc/server.go
+++ b/metropolis/node/core/rpc/server.go
@@ -1,165 +1,37 @@
 package rpc
 
 import (
-	"context"
-	"crypto/tls"
-	"crypto/x509"
-	"strings"
-
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/codes"
-	"google.golang.org/grpc/credentials"
-	"google.golang.org/grpc/peer"
-	"google.golang.org/grpc/status"
-	"google.golang.org/protobuf/proto"
-	"google.golang.org/protobuf/reflect/protoreflect"
-	"google.golang.org/protobuf/reflect/protoregistry"
-
 	cpb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 	apb "source.monogon.dev/metropolis/proto/api"
 	epb "source.monogon.dev/metropolis/proto/ext"
 )
 
-// ClusterServices is the interface containing all gRPC services that a
-// Metropolis Cluster implements on its public interface. With the current
-// implementaiton of Metropolis, this is all implemented by the Curator.
-type ClusterServices interface {
+var (
+	// nodePermissions are the set of metropolis.common.ext.authorization
+	// permissions automatically given to nodes when connecting to curator gRPC
+	// services, either locally or remotely.
+	nodePermissions = Permissions{
+		epb.Permission_PERMISSION_READ_CLUSTER_STATUS: true,
+	}
+)
+
+// ClusterExternalServices is the interface containing all gRPC services that a
+// Metropolis Cluster implements on its external interface. With the current
+// implementation of Metropolis, this is all implemented by the Curator.
+type ClusterExternalServices interface {
 	cpb.CuratorServer
 	apb.AAAServer
 	apb.ManagementServer
 }
 
-// ServerSecurity are the security options of a RPC server that will run
-// ClusterServices on a Metropolis node. It contains all the data for the
-// server implementation to authenticate itself to the clients and authenticate
-// and authorize clients connecting to it.
-type ServerSecurity struct {
-	// NodeCredentials is the TLS certificate/key of the node that the server
-	// implementation is running on. It should be signed by
-	// ClusterCACertificate.
-	NodeCredentials tls.Certificate
-	// ClusterCACertificate is the cluster's CA certificate. It will be used to
-	// authenticate the client certificates of incoming gRPC connections.
-	ClusterCACertificate *x509.Certificate
+// ClusterInternalServices is the interface containing all gRPC services that a
+// Metropolis Cluster implements on its internal interface. Currently this is
+// just the Curator service.
+type ClusterInternalServices interface {
+	cpb.CuratorServer
 }
 
-// SetupPublicGRPC returns a grpc.Server ready to listen and serve all public
-// gRPC APIs that the cluster server implementation should run, with all calls
-// being authenticated and authorized based on the data in ServerSecurity. The
-// argument 'impls' is the object implementing the gRPC APIs.
-//
-// This effectively configures gRPC interceptors that verify
-// metropolis.proto.ext.authorizaton options and authenticate/authorize
-// incoming connections. It also runs the gRPC server with the correct TLS
-// settings for authenticating itself to callers.
-func (l *ServerSecurity) SetupPublicGRPC(impls ClusterServices) *grpc.Server {
-	publicCreds := credentials.NewTLS(&tls.Config{
-		Certificates: []tls.Certificate{l.NodeCredentials},
-		ClientAuth:   tls.RequestClientCert,
-	})
-
-	s := grpc.NewServer(
-		grpc.Creds(publicCreds),
-		grpc.UnaryInterceptor(l.unaryInterceptor),
-		grpc.StreamInterceptor(l.streamInterceptor),
-	)
-	cpb.RegisterCuratorServer(s, impls)
-	apb.RegisterAAAServer(s, impls)
-	apb.RegisterManagementServer(s, impls)
-	return s
-}
-
-// authorize performs an authorization check for the given gRPC context
-// (containing peer information) and given RPC method name (as obtained from
-// FullMethodName in {Unary,Stream}ServerInfo). The actual authorization
-// requirements per method are retrieved from the Authorization protobuf
-// option applied to the RPC method.
-//
-// If the peer (as retrieved from the context) is authorized to run this method,
-// no error is returned. Otherwise, a gRPC status is returned outlining the
-// reason the authorization being rejected.
-func (l *ServerSecurity) authorize(ctx context.Context, methodName string) error {
-	if !strings.HasPrefix(methodName, "/") {
-		return status.Errorf(codes.InvalidArgument, "invalid method name %q", methodName)
-	}
-	methodName = strings.ReplaceAll(methodName[1:], "/", ".")
-	desc, err := protoregistry.GlobalFiles.FindDescriptorByName(protoreflect.FullName(methodName))
-	if err != nil {
-		return status.Errorf(codes.InvalidArgument, "could not retrieve descriptor for method: %v", err)
-	}
-	method, ok := desc.(protoreflect.MethodDescriptor)
-	if !ok {
-		return status.Error(codes.InvalidArgument, "querying method name did not yield a MethodDescriptor")
-	}
-
-	// Get authorization extension, defaults to no options set.
-	authz, ok := proto.GetExtension(method.Options(), epb.E_Authorization).(*epb.Authorization)
-	if !ok || authz == nil {
-		authz = &epb.Authorization{}
-	}
-
-	// If unauthenticated connections are allowed, let them through immediately.
-	if authz.AllowUnauthenticated && len(authz.Need) == 0 {
-		return nil
-	}
-
-	// Otherwise, we check that the other side of the connection is authenticated
-	// using a valid cluster CA client certificate.
-	p, ok := peer.FromContext(ctx)
-	if !ok {
-		return status.Error(codes.Unavailable, "could not retrive peer info")
-	}
-	tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo)
-	if !ok {
-		return status.Error(codes.Unauthenticated, "connection not secure")
-	}
-	count := len(tlsInfo.State.PeerCertificates)
-	if count == 0 {
-		return status.Errorf(codes.Unauthenticated, "no client certificate presented")
-	}
-	if count > 1 {
-		return status.Errorf(codes.Unauthenticated, "exactly one client certificate must be sent (got %d)", count)
-	}
-	pCert := tlsInfo.State.PeerCertificates[0]
-
-	// Ensure that the certificate is signed by the cluster CA.
-	if err := pCert.CheckSignatureFrom(l.ClusterCACertificate); err != nil {
-		return status.Errorf(codes.Unauthenticated, "invalid client certificate: %v", err)
-	}
-	// Ensure that the certificate is a client certificate.
-	// TODO(q3k): synchronize this with //metropolis/pkg/pki Client()/Server()/...
-	isClient := false
-	for _, ku := range pCert.ExtKeyUsage {
-		if ku == x509.ExtKeyUsageClientAuth {
-			isClient = true
-			break
-		}
-	}
-	if !isClient {
-		return status.Error(codes.PermissionDenied, "presented certificate is not a client certificate")
-	}
-
-	// MVP: all permissions are granted to all users.
-	// TODO(q3k): check authz.Need once we have a user/identity system implemented.
-	return nil
-}
-
-// streamInterceptor is a gRPC server stream interceptor that performs
-// authentication and authorization of incoming RPCs based on the Authorization
-// option set on each method.
-func (l *ServerSecurity) streamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
-	if err := l.authorize(ss.Context(), info.FullMethod); err != nil {
-		return err
-	}
-	return handler(srv, ss)
-}
-
-// unaryInterceptor is a gRPC server unary interceptor that performs
-// authentication and authorization of incoming RPCs based on the Authorization
-// option set on each method.
-func (l *ServerSecurity) unaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
-	if err := l.authorize(ctx, info.FullMethod); err != nil {
-		return nil, err
-	}
-	return handler(ctx, req)
+type ClusterServices interface {
+	ClusterExternalServices
+	ClusterInternalServices
 }