m/n/core/rpc: provide lower-level gRPC dialing constructs

This replaces the 2x2 cartesian product of ready-made dialing functions
(New{Authenticated,Ephemeral}Client{Test,}) with plain gRPC Dial
Options.

This is partially to reduce the magical aspect of the RPC library (after
all, we are just using gRPC here, no need for these wrappers), but
mostly in preparation for having another dimension added: dynamic
cluster resolving, which will also be just provided as a Dial Option.

Change-Id: Id051ca5204e4b44afcc10164f376ccf08af46120
Reviewed-on: https://review.monogon.dev/c/monogon/+/640
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/node/core/cluster/BUILD.bazel b/metropolis/node/core/cluster/BUILD.bazel
index 2002571..93cc5de 100644
--- a/metropolis/node/core/cluster/BUILD.bazel
+++ b/metropolis/node/core/cluster/BUILD.bazel
@@ -24,6 +24,7 @@
         "//metropolis/proto/api:go_default_library",
         "//metropolis/proto/private:go_default_library",
         "@com_github_cenkalti_backoff_v4//:go_default_library",
+        "@org_golang_google_grpc//:go_default_library",
         "@org_golang_google_protobuf//proto:go_default_library",
     ],
 )
diff --git a/metropolis/node/core/cluster/cluster_register.go b/metropolis/node/core/cluster/cluster_register.go
index 7376b84..3acb7d7 100644
--- a/metropolis/node/core/cluster/cluster_register.go
+++ b/metropolis/node/core/cluster/cluster_register.go
@@ -12,6 +12,8 @@
 	"strings"
 	"time"
 
+	"google.golang.org/grpc"
+
 	"source.monogon.dev/metropolis/node"
 	ipb "source.monogon.dev/metropolis/node/core/curator/proto/api"
 	"source.monogon.dev/metropolis/node/core/identity"
@@ -99,7 +101,11 @@
 	// MVP: this should be properly client-side loadbalanced.
 	remote := register.ClusterDirectory.Nodes[0].Addresses[0].Host
 	remote = net.JoinHostPort(remote, strconv.Itoa(int(node.CuratorServicePort)))
-	eph, err := rpc.NewEphemeralClient(remote, priv, ca)
+	ephCreds, err := rpc.NewEphemeralCredentials(priv, ca)
+	if err != nil {
+		return fmt.Errorf("could not create ephemeral credentials: %w", err)
+	}
+	eph, err := grpc.Dial(remote, grpc.WithTransportCredentials(ephCreds))
 	if err != nil {
 		return fmt.Errorf("could not create ephemeral client to %q: %w", remote, err)
 	}
diff --git a/metropolis/node/core/curator/impl_leader_test.go b/metropolis/node/core/curator/impl_leader_test.go
index 60b1d70..b07b49f 100644
--- a/metropolis/node/core/curator/impl_leader_test.go
+++ b/metropolis/node/core/curator/impl_leader_test.go
@@ -8,6 +8,7 @@
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/hex"
+	"net"
 	"testing"
 
 	"go.etcd.io/etcd/integration"
@@ -141,19 +142,30 @@
 		externalSrv.Stop()
 	}()
 
+	withLocalDialer := grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) {
+		return externalLis.Dial()
+	})
+	ca := nodeCredentials.ClusterCA()
+
 	// Create an authenticated manager gRPC client.
-	mcl, err := rpc.NewAuthenticatedClientTest(externalLis, ownerCreds, nodeCredentials.ClusterCA())
+	mcl, err := grpc.Dial("local", withLocalDialer, grpc.WithTransportCredentials(rpc.NewAuthenticatedCredentials(ownerCreds, ca)))
 	if err != nil {
 		t.Fatalf("Dialing external GRPC failed: %v", err)
 	}
 
-	// Create an ephemeral node gRPC client for the local node.
-	lcl, err := rpc.NewAuthenticatedClientTest(externalLis, nodeCredentials.TLSCredentials(), nodeCredentials.ClusterCA())
+	// Create a node gRPC client for the local node.
+	lcl, err := grpc.Dial("local", withLocalDialer,
+		grpc.WithTransportCredentials(rpc.NewAuthenticatedCredentials(nodeCredentials.TLSCredentials(), ca)))
 	if err != nil {
 		t.Fatalf("Dialing external GRPC failed: %v", err)
 	}
+
 	// Create an ephemeral node gRPC client for the 'other node'.
-	ocl, err := rpc.NewEphemeralClientTest(externalLis, otherPriv, nodeCredentials.ClusterCA())
+	otherEphCreds, err := rpc.NewEphemeralCredentials(otherPriv, ca)
+	if err != nil {
+		t.Fatalf("NewEphemeralCredentials: %v", err)
+	}
+	ocl, err := grpc.Dial("local", withLocalDialer, grpc.WithTransportCredentials(otherEphCreds))
 	if err != nil {
 		t.Fatalf("Dialing external GRPC failed: %v", err)
 	}
diff --git a/metropolis/node/core/roleserve/value_clustermembership.go b/metropolis/node/core/roleserve/value_clustermembership.go
index d699473..1a9ebc5 100644
--- a/metropolis/node/core/roleserve/value_clustermembership.go
+++ b/metropolis/node/core/roleserve/value_clustermembership.go
@@ -136,7 +136,8 @@
 	}
 	host := m.remoteCurators.Nodes[0].Addresses[0].Host
 	addr := net.JoinHostPort(host, common.CuratorServicePort.PortString())
-	return rpc.NewAuthenticatedClient(addr, m.credentials.TLSCredentials(), m.credentials.ClusterCA())
+	creds := rpc.NewAuthenticatedCredentials(m.credentials.TLSCredentials(), m.credentials.ClusterCA())
+	return grpc.Dial(addr, grpc.WithTransportCredentials(creds))
 }
 
 func (m *ClusterMembership) NodePubkey() ed25519.PublicKey {
diff --git a/metropolis/node/core/rpc/BUILD.bazel b/metropolis/node/core/rpc/BUILD.bazel
index 4d83dfe..8ec88c0 100644
--- a/metropolis/node/core/rpc/BUILD.bazel
+++ b/metropolis/node/core/rpc/BUILD.bazel
@@ -25,7 +25,6 @@
         "@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_grpc//test/bufconn:go_default_library",
         "@org_golang_google_protobuf//encoding/prototext:go_default_library",
         "@org_golang_google_protobuf//proto:go_default_library",
         "@org_golang_google_protobuf//reflect/protoreflect:go_default_library",
@@ -45,6 +44,7 @@
         "//metropolis/pkg/logtree: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//status:go_default_library",
         "@org_golang_google_grpc//test/bufconn:go_default_library",
diff --git a/metropolis/node/core/rpc/client.go b/metropolis/node/core/rpc/client.go
index 5ff71be..70173d8 100644
--- a/metropolis/node/core/rpc/client.go
+++ b/metropolis/node/core/rpc/client.go
@@ -8,13 +8,10 @@
 	"crypto/x509"
 	"fmt"
 	"math/big"
-	"net"
 	"time"
 
-	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/status"
-	"google.golang.org/grpc/test/bufconn"
 
 	"source.monogon.dev/metropolis/node/core/identity"
 	"source.monogon.dev/metropolis/pkg/pki"
@@ -40,19 +37,23 @@
 	}
 }
 
-// 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.
+// NewEphemeralCredentials returns gRPC TransportCredentials that can be used to
+// dial a cluster without authenticating with a certificate, but instead
+// authenticating by proving the possession of a private key, via an ephemeral
+// self-signed certificate.
 //
-// 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.
+// Currently these credentials are used in two flows:
+//
+//   1. Registration of nodes into a cluster, after which a node receives a proper
+//      node certificate
+//
+//   2. Escrow of initial owner credentials into a proper manager
+//      certificate
 //
 // If 'ca' is given, the remote side will be cryptographically verified to be a
 // node that's part of the cluster represented by the ca. Otherwise, no
 // verification is performed and this function is unsafe.
-func NewEphemeralClient(remote string, private ed25519.PrivateKey, ca *x509.Certificate, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
+func NewEphemeralCredentials(private ed25519.PrivateKey, ca *x509.Certificate) (credentials.TransportCredentials, error) {
 	template := x509.Certificate{
 		SerialNumber: big.NewInt(1),
 		NotBefore:    time.Now(),
@@ -70,13 +71,25 @@
 		Certificate: [][]byte{certificateBytes},
 		PrivateKey:  private,
 	}
-	return NewAuthenticatedClient(remote, certificate, ca, opts...)
+	return NewAuthenticatedCredentials(certificate, ca), nil
 }
 
-func NewEphemeralClientTest(listener *bufconn.Listener, private ed25519.PrivateKey, ca *x509.Certificate) (*grpc.ClientConn, error) {
-	return NewEphemeralClient("local", private, ca, grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) {
-		return listener.Dial()
-	}))
+// NewAuthenticatedCredentials returns gRPC TransportCredentials that can be
+// used to dial a cluster with a given TLS certificate (from node or manager
+// credentials).
+//
+// If 'ca' is given, the remote side will be cryptographically verified to be a
+// node that's part of the cluster represented by the ca. Otherwise, no
+// verification is performed and this function is unsafe.
+func NewAuthenticatedCredentials(cert tls.Certificate, ca *x509.Certificate) credentials.TransportCredentials {
+	config := &tls.Config{
+		Certificates:       []tls.Certificate{cert},
+		InsecureSkipVerify: true,
+	}
+	if ca != nil {
+		config.VerifyPeerCertificate = verifyClusterCertificate(ca)
+	}
+	return credentials.NewTLS(config)
 }
 
 // RetrieveOwnerCertificates uses AAA.Escrow to retrieve a cluster manager
@@ -113,27 +126,3 @@
 		PrivateKey:  private,
 	}, nil
 }
-
-// NewAuthenticatedClient dials a cluster's services using the given TLS
-// credentials (either user or node credentials).
-//
-// If 'ca' is given, the remote side will be cryptographically verified to be a
-// node that's part of the cluster represented by the ca. Otherwise, no
-// verification is performed and this function is unsafe.
-func NewAuthenticatedClient(remote string, cert tls.Certificate, ca *x509.Certificate, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
-	config := &tls.Config{
-		Certificates:       []tls.Certificate{cert},
-		InsecureSkipVerify: true,
-	}
-	if ca != nil {
-		config.VerifyPeerCertificate = verifyClusterCertificate(ca)
-	}
-	opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(config)))
-	return grpc.Dial(remote, opts...)
-}
-
-func NewAuthenticatedClientTest(listener *bufconn.Listener, cert tls.Certificate, ca *x509.Certificate) (*grpc.ClientConn, error) {
-	return NewAuthenticatedClient("local", cert, ca, grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) {
-		return listener.Dial()
-	}))
-}
diff --git a/metropolis/node/core/rpc/server_authentication_test.go b/metropolis/node/core/rpc/server_authentication_test.go
index 559d4fa..247479e 100644
--- a/metropolis/node/core/rpc/server_authentication_test.go
+++ b/metropolis/node/core/rpc/server_authentication_test.go
@@ -4,8 +4,10 @@
 	"context"
 	"crypto/ed25519"
 	"crypto/rand"
+	"net"
 	"testing"
 
+	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 	"google.golang.org/grpc/test/bufconn"
@@ -51,10 +53,16 @@
 	defer lis.Close()
 	defer srv.Stop()
 
+	withLocalDialer := grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) {
+		return lis.Dial()
+	})
+
 	// Authenticate as manager externally, ensure that GetRegisterTicket runs.
-	cl, err := NewAuthenticatedClientTest(lis, eph.Manager, eph.CA)
+	cl, err := grpc.Dial("local",
+		grpc.WithTransportCredentials(NewAuthenticatedCredentials(eph.Manager, eph.CA)),
+		withLocalDialer)
 	if err != nil {
-		t.Fatalf("NewAuthenticatedClient: %v", err)
+		t.Fatalf("Dial: %v", err)
 	}
 	defer cl.Close()
 	mgmt := apb.NewManagementClient(cl)
@@ -65,9 +73,11 @@
 
 	// Authenticate as node externally, ensure that GetRegisterTicket is refused
 	// (this is because nodes miss the GET_REGISTER_TICKET permissions).
-	cl, err = NewAuthenticatedClientTest(lis, eph.Nodes[0].TLSCredentials(), eph.CA)
+	cl, err = grpc.Dial("local",
+		grpc.WithTransportCredentials(NewAuthenticatedCredentials(eph.Nodes[0].TLSCredentials(), eph.CA)),
+		withLocalDialer)
 	if err != nil {
-		t.Fatalf("NewAuthenticatedClient: %v", err)
+		t.Fatalf("Dial: %v", err)
 	}
 	defer cl.Close()
 	mgmt = apb.NewManagementClient(cl)
@@ -91,9 +101,14 @@
 	if err != nil {
 		t.Fatalf("GenerateKey: %v", err)
 	}
-	cl, err = NewEphemeralClientTest(lis, sk, eph.CA)
+
+	ephCreds, err := NewEphemeralCredentials(sk, eph.CA)
 	if err != nil {
-		t.Fatalf("NewEphemeralClient: %v", err)
+		t.Fatalf("NewEphemeralCredentials: %v", err)
+	}
+	cl, err = grpc.Dial("local", grpc.WithTransportCredentials(ephCreds), withLocalDialer)
+	if err != nil {
+		t.Fatalf("Dial: %v", err)
 	}
 	defer cl.Close()
 	mgmt = apb.NewManagementClient(cl)