m/n/core/rpc: create library for common gRPC functions

This is the beginning of consolidating all gRPC-related code into a
single package.

We also run the Curator service publicly and place it behind a new
authorization permission bit. This is in preparation for Curator
followers needing access to this Service.

Some of the service split and authorization options are likely to be
changed in the future (I'm considering renaming Curator to something
else, or at least clearly stating that it's a node-to-node service).

Change-Id: I0a4a57da15b35688aefe7bf669ba6342d46aa3f5
Reviewed-on: https://review.monogon.dev/c/monogon/+/316
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/node/core/rpc/BUILD.bazel b/metropolis/node/core/rpc/BUILD.bazel
new file mode 100644
index 0000000..df03356
--- /dev/null
+++ b/metropolis/node/core/rpc/BUILD.bazel
@@ -0,0 +1,25 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "client.go",
+        "server.go",
+    ],
+    importpath = "source.monogon.dev/metropolis/node/core/rpc",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//metropolis/node/core/curator/proto/api:go_default_library",
+        "//metropolis/pkg/pki:go_default_library",
+        "//metropolis/proto/api:go_default_library",
+        "//metropolis/proto/ext:go_default_library",
+        "@org_golang_google_grpc//:go_default_library",
+        "@org_golang_google_grpc//codes:go_default_library",
+        "@org_golang_google_grpc//credentials:go_default_library",
+        "@org_golang_google_grpc//peer:go_default_library",
+        "@org_golang_google_grpc//status:go_default_library",
+        "@org_golang_google_protobuf//proto:go_default_library",
+        "@org_golang_google_protobuf//reflect/protoreflect:go_default_library",
+        "@org_golang_google_protobuf//reflect/protoregistry:go_default_library",
+    ],
+)
diff --git a/metropolis/node/core/rpc/client.go b/metropolis/node/core/rpc/client.go
new file mode 100644
index 0000000..cc48f95
--- /dev/null
+++ b/metropolis/node/core/rpc/client.go
@@ -0,0 +1,131 @@
+package rpc
+
+import (
+	"context"
+	"crypto/ed25519"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"math/big"
+	"time"
+
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+
+	"source.monogon.dev/metropolis/pkg/pki"
+	apb "source.monogon.dev/metropolis/proto/api"
+)
+
+// NewEphemeralClient dials a cluster's services using just a self-signed
+// certificate and can be used to then escrow real cluster credentials for the
+// owner.
+//
+// These self-signed certificates are used by clients connecting to the cluster
+// which want to prove ownership of an ED25519 keypair but don't have any
+// 'real' client certificate (yet). Current users include users of AAA.Escrow
+// and new nodes Registering into the Cluster.
+//
+// If ca is given, the other side of the connection is verified to be served by
+// a node presenting a certificate signed by that CA. Otherwise, no
+// verification of the other side is performed (however, any attacker
+// impersonating the cluster cannot use the escrowed credentials as the private
+// key is never passed to the server).
+func NewEphemeralClient(remote string, private ed25519.PrivateKey, ca *x509.Certificate) (*grpc.ClientConn, error) {
+	template := x509.Certificate{
+		SerialNumber: big.NewInt(1),
+		NotBefore:    time.Now(),
+		NotAfter:     pki.UnknownNotAfter,
+
+		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
+		BasicConstraintsValid: true,
+	}
+	certificateBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, private.Public(), private)
+	if err != nil {
+		return nil, fmt.Errorf("when generating self-signed certificate: %w", err)
+	}
+	certificate := tls.Certificate{
+		Certificate: [][]byte{certificateBytes},
+		PrivateKey:  private,
+	}
+	creds := credentials.NewTLS(&tls.Config{
+		Certificates: []tls.Certificate{
+			certificate,
+		},
+		InsecureSkipVerify: true,
+		VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+			if len(rawCerts) < 1 {
+				return fmt.Errorf("server presented no certificate")
+			}
+			certs := make([]*x509.Certificate, len(rawCerts))
+			for i, rawCert := range rawCerts {
+				cert, err := x509.ParseCertificate(rawCert)
+				if err != nil {
+					return fmt.Errorf("could not parse server certificate %d: %v", i, err)
+				}
+				certs[i] = cert
+			}
+
+			if ca != nil {
+				// CA given, perform full chain verification.
+				roots := x509.NewCertPool()
+				roots.AddCert(ca)
+				opts := x509.VerifyOptions{
+					Roots:         roots,
+					Intermediates: x509.NewCertPool(),
+				}
+				for _, cert := range certs[1:] {
+					opts.Intermediates.AddCert(cert)
+				}
+				_, err := certs[0].Verify(opts)
+				if err != nil {
+					return err
+				}
+			}
+
+			// Regardless of CA given, ensure that the leaf certificate has the
+			// right ExtKeyUsage.
+			for _, ku := range certs[0].ExtKeyUsage {
+				if ku == x509.ExtKeyUsageServerAuth {
+					return nil
+				}
+			}
+			return fmt.Errorf("server presented a certificate without server auth ext key usage")
+		},
+	})
+
+	return grpc.Dial(remote, grpc.WithTransportCredentials(creds))
+}
+
+// RetrieveOwnerCertificates uses AAA.Escrow to retrieve a cluster manager
+// certificate for the initial owner of the cluster, authenticated by the
+// public/private key set in the clusters NodeParameters.ClusterBoostrap.
+//
+// The retrieved certificate can be used to dial further cluster RPCs.
+func RetrieveOwnerCertificate(ctx context.Context, aaa apb.AAAClient, private ed25519.PrivateKey) (*tls.Certificate, error) {
+	srv, err := aaa.Escrow(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("when opening Escrow RPC: %w", err)
+	}
+	if err := srv.Send(&apb.EscrowFromClient{
+		Parameters: &apb.EscrowFromClient_Parameters{
+			RequestedIdentityName: "owner",
+			PublicKey:             private.Public().(ed25519.PublicKey),
+		},
+	}); err != nil {
+		return nil, fmt.Errorf("when sending client parameters: %w", err)
+	}
+	resp, err := srv.Recv()
+	if err != nil {
+		return nil, fmt.Errorf("when receiving server message: %w", err)
+	}
+	if len(resp.EmittedCertificate) == 0 {
+		return nil, fmt.Errorf("expected certificate, instead got needed proofs: %+v", resp.Needed)
+	}
+
+	return &tls.Certificate{
+		Certificate: [][]byte{resp.EmittedCertificate},
+		PrivateKey:  private,
+	}, nil
+}
diff --git a/metropolis/node/core/rpc/server.go b/metropolis/node/core/rpc/server.go
new file mode 100644
index 0000000..3d6917d
--- /dev/null
+++ b/metropolis/node/core/rpc/server.go
@@ -0,0 +1,165 @@
+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 {
+	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
+}
+
+// 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)
+}