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/test/e2e/BUILD.bazel b/metropolis/test/e2e/BUILD.bazel
index 117bbe9..53c0e38 100644
--- a/metropolis/test/e2e/BUILD.bazel
+++ b/metropolis/test/e2e/BUILD.bazel
@@ -33,6 +33,7 @@
     rundir = ".",
     deps = [
         "//metropolis/node:go_default_library",
+        "//metropolis/node/core/rpc:go_default_library",
         "//metropolis/proto/api:go_default_library",
         "//metropolis/test/launch:go_default_library",
         "@io_k8s_api//core/v1:go_default_library",
diff --git a/metropolis/test/e2e/main_test.go b/metropolis/test/e2e/main_test.go
index 0fe1378..d1102b1 100644
--- a/metropolis/test/e2e/main_test.go
+++ b/metropolis/test/e2e/main_test.go
@@ -39,6 +39,7 @@
 	podv1 "k8s.io/kubernetes/pkg/api/v1/pod"
 
 	common "source.monogon.dev/metropolis/node"
+	"source.monogon.dev/metropolis/node/core/rpc"
 	apb "source.monogon.dev/metropolis/proto/api"
 	"source.monogon.dev/metropolis/test/launch"
 )
@@ -115,15 +116,14 @@
 	t.Run("RunGroup", func(t *testing.T) {
 		t.Run("Connect to Curator", func(t *testing.T) {
 			testEventual(t, "Retrieving owner credentials succesful", ctx, 60*time.Second, func(ctx context.Context) error {
-				initClient, err := launch.NewInitialClient(&launch.InitialClientOptions{
-					Remote:  fmt.Sprintf("localhost:%v", portMap[common.CuratorServicePort]),
-					Private: launch.InsecurePrivateKey,
-				})
+				remote := fmt.Sprintf("localhost:%v", portMap[common.CuratorServicePort])
+				initClient, err := rpc.NewEphemeralClient(remote, launch.InsecurePrivateKey, nil)
 				if err != nil {
 					return fmt.Errorf("NewInitialClient: %w", err)
 				}
 
-				cert, err := initClient.RetrieveOwnerCertificate(ctx)
+				aaa := apb.NewAAAClient(initClient)
+				cert, err := rpc.RetrieveOwnerCertificate(ctx, aaa, launch.InsecurePrivateKey)
 				if err != nil {
 					return fmt.Errorf("RetrieveOwnerCertificate: %w", err)
 				}
diff --git a/metropolis/test/launch/BUILD.bazel b/metropolis/test/launch/BUILD.bazel
index eeb78b5..3f9d8fd 100644
--- a/metropolis/test/launch/BUILD.bazel
+++ b/metropolis/test/launch/BUILD.bazel
@@ -3,7 +3,6 @@
 go_library(
     name = "go_default_library",
     srcs = [
-        "client.go",
         "insecure_key.go",
         "launch.go",
     ],
@@ -16,7 +15,6 @@
         "@com_github_golang_protobuf//proto:go_default_library",
         "@com_github_grpc_ecosystem_go_grpc_middleware//retry:go_default_library",
         "@org_golang_google_grpc//:go_default_library",
-        "@org_golang_google_grpc//credentials:go_default_library",
         "@org_golang_x_sys//unix:go_default_library",
     ],
 )
diff --git a/metropolis/test/launch/client.go b/metropolis/test/launch/client.go
deleted file mode 100644
index 5913f14..0000000
--- a/metropolis/test/launch/client.go
+++ /dev/null
@@ -1,149 +0,0 @@
-package launch
-
-import (
-	"context"
-	"crypto/ed25519"
-	"crypto/rand"
-	"crypto/tls"
-	"crypto/x509"
-	"encoding/pem"
-	"fmt"
-	"math/big"
-	"time"
-
-	"google.golang.org/grpc"
-	"google.golang.org/grpc/credentials"
-
-	apb "source.monogon.dev/metropolis/proto/api"
-)
-
-// InitialClient implements a gRPC wrapper for dialing a Metropolis cluster
-// while not (yet) authenticated, i.e. using only a self-signed public
-// certificate to prove ownership of an ed25519 public key.
-//
-// This is used to dial a cluster's AAA.Escrow service after cluster bootstrap
-// using the owner key configured in NodeParams.
-type InitialClient struct {
-	// conn is the underlying dialed gRPC connection to the cluster.
-	conn *grpc.ClientConn
-	// aaa is a stub to the AAA service running on conn.
-	aaa apb.AAAClient
-	// options are the options this client has been opened with.
-	options *InitialClientOptions
-}
-
-type InitialClientOptions struct {
-	// Remote is an address:port to connect to. This should be a cluster node's
-	// curator port.
-	Remote string
-	// Private is the cluster owner private key, which should correspond to the
-	// owner public key defined in NodeParametrs.ClusterBootstrap when a cluster
-	// is bootstrapped.
-	Private ed25519.PrivateKey
-}
-
-// NewInitialClient dials a cluster's curator service using just a self-signed
-// certificate and can be used to then escrow real cluster credentials for the
-// owner.
-//
-// MVP SECURITY: this does not verify the identity of the cluster/node. However,
-// because any intercepting party cannot forward the presented client
-// certificate to any real cluster, no danger of intercepting administrative
-// access to the expected cluster is possible. Instead, the interceptor can just
-// pretend to be the cluster which was expected.
-func NewInitialClient(o *InitialClientOptions) (*InitialClient, error) {
-	template := x509.Certificate{
-		SerialNumber: big.NewInt(1),
-		NotBefore:    time.Now(),
-		NotAfter:     time.Now().Add(time.Hour),
-
-		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
-		BasicConstraintsValid: true,
-	}
-	certificateBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, o.Private.Public(), o.Private)
-	if err != nil {
-		return nil, fmt.Errorf("when generating self-signed certificate: %w", err)
-	}
-	keyBytes, err := x509.MarshalPKCS8PrivateKey(o.Private)
-	if err != nil {
-		return nil, fmt.Errorf("when marshaling private key: %w", err)
-	}
-	key := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
-	certificate := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certificateBytes})
-	clientCert, err := tls.X509KeyPair(certificate, key)
-	if err != nil {
-		return nil, fmt.Errorf("when building self-signed TLS client certificate: %w", err)
-	}
-
-	creds := credentials.NewTLS(&tls.Config{
-		Certificates: []tls.Certificate{
-			clientCert,
-		},
-		InsecureSkipVerify:    true,
-		VerifyPeerCertificate: o.verify,
-	})
-
-	conn, err := grpc.Dial(o.Remote, grpc.WithTransportCredentials(creds))
-	if err != nil {
-		return nil, fmt.Errorf("when dialing: %w", err)
-	}
-
-	return &InitialClient{
-		conn:    conn,
-		aaa:     apb.NewAAAClient(conn),
-		options: o,
-	}, nil
-}
-
-// Close must be called when the InitialClient is not used anymore. This closes
-// the underlying gRPC connection(s).
-func (i *InitialClient) Close() error {
-	return i.conn.Close()
-}
-
-func (o *InitialClientOptions) verify(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
-	// SECURITY: Always permit all server certificates. See NewInitialClient godoc
-	// for more information.
-	return nil
-}
-
-// 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 (i *InitialClient) RetrieveOwnerCertificate(ctx context.Context) (*tls.Certificate, error) {
-	srv, err := i.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:             i.options.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)
-	}
-
-	certificateBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: resp.EmittedCertificate})
-	key, err := x509.MarshalPKCS8PrivateKey(i.options.Private)
-	if err != nil {
-		return nil, fmt.Errorf("while marshalling private key: %w", err)
-	}
-	keyBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key})
-	ownerCert, err := tls.X509KeyPair(certificateBytes, keyBytes)
-	if err != nil {
-		return nil, fmt.Errorf("could not build certificate from data received from cluster: %w", err)
-	}
-
-	return &ownerCert, nil
-}