m/n/c/curator: implement Join Flow

This implements Join Flow in Curator, as described in Cluster Lifecycle
and Integrity design document.

Change-Id: Idabb471575e1d22a7eb7cce2ad29d18f1f94760a
Reviewed-on: https://review.monogon.dev/c/monogon/+/667
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/core/curator/impl_leader_curator.go b/metropolis/node/core/curator/impl_leader_curator.go
index 26f3004..1c7221f 100644
--- a/metropolis/node/core/curator/impl_leader_curator.go
+++ b/metropolis/node/core/curator/impl_leader_curator.go
@@ -5,6 +5,7 @@
 	"crypto/subtle"
 	"fmt"
 	"net"
+	"time"
 
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
@@ -245,6 +246,7 @@
 	}
 	// ... update its' status ...
 	node.status = req.Status
+	node.status.Timestamp = time.Now().UnixNano()
 	// ... and save it to etcd.
 	if err := nodeSave(ctx, l.leadership, node); err != nil {
 		return nil, err
@@ -263,6 +265,9 @@
 	}
 	pubkey := pi.Unauthenticated.SelfSignedPublicKey
 
+	// TODO(mateusz@monogon.tech): check req.JoinKey length once Join Flow is
+	// implemented on the client side.
+
 	// Verify that call contains a RegisterTicket and that this RegisterTicket is
 	// valid.
 	wantTicket, err := l.ensureRegisterTicket(ctx)
@@ -307,6 +312,7 @@
 	// No node exists, create one.
 	node = &Node{
 		pubkey: pubkey,
+		jkey:   req.JoinKey,
 		state:  cpb.NodeState_NODE_STATE_NEW,
 	}
 	if err := nodeSave(ctx, l.leadership, node); err != nil {
@@ -414,3 +420,39 @@
 		NodeCertificate: nodeCertBytes,
 	}, nil
 }
+
+func (l *leaderCurator) JoinNode(ctx context.Context, req *ipb.JoinNodeRequest) (*ipb.JoinNodeResponse, error) {
+	// Gather peer information.
+	pi := rpc.GetPeerInfo(ctx)
+	if pi == nil || pi.Unauthenticated == nil {
+		return nil, status.Error(codes.PermissionDenied, "connection must be established with a self-signed ephemeral certificate")
+	}
+	// The node will attempt to connect using its Join Key. jkey will contain
+	// its public part.
+	jkey := pi.Unauthenticated.SelfSignedPublicKey
+
+	// Take the lock to prevent data races during the next step.
+	l.muNodes.Lock()
+	defer l.muNodes.Unlock()
+
+	// Resolve the Node ID using Join Key, then use the ID to load node
+	// information from etcd.
+	id, err := nodeIdByJoinKey(ctx, l.leadership, jkey)
+	if err != nil {
+		return nil, err
+	}
+	node, err := nodeLoad(ctx, l.leadership, id)
+	if err != nil {
+		return nil, err
+	}
+
+	// Don't progress further unless the node is already UP.
+	if node.state != cpb.NodeState_NODE_STATE_UP {
+		return nil, status.Errorf(codes.FailedPrecondition, "node isn't UP, cannot join")
+	}
+
+	// Return the Node's CUK, completing the Join Flow.
+	return &ipb.JoinNodeResponse{
+		ClusterUnlockKey: node.clusterUnlockKey,
+	}, nil
+}