~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
239 lines · 6.9 KB Raw History
package clusterlink

import (
	"context"
	"crypto/ed25519"
	"encoding/base32"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"strconv"
	"strings"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/didkey"

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
)

const Mount = "socialweb-links"

const EnvSecret = "IROH_SECRET"

const (
	FieldSecret = "secret"

	FieldEndpointID = "endpointId"

	FieldDid = "did"
)

const TicketPrefix = "endpoint"

const connectLine = "dumbpipe connect-tcp "

const ticketVariant = 0x00

var Services = []v1alpha1.ClusterLinkService{v1alpha1.ClusterLinkApiserver, v1alpha1.ClusterLinkOpenBao}

var ticketEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)

var (
	ErrStoredKeyUnreadable = errors.New(
		"clusterlink: the link key stored in the cluster vault cannot be read back. It is never " +
			"regenerated over: a new key is a new endpoint id, and every node holding a ticket for " +
			"the old one would dial an endpoint nobody listens as")

	ErrBadTicket = errors.New("clusterlink: not a dumbpipe endpoint ticket")
)

type KeyStore interface {
	Read(ctx context.Context, path string) (map[string]any, bool, error)

	Create(ctx context.Context, path string, data map[string]any) (bool, error)
}

type Key struct {
	Service v1alpha1.ClusterLinkService

	Private ed25519.PrivateKey

	EndpointID string

	Did string
}

func (k Key) Env() string { return EnvSecret + "=" + hex.EncodeToString(k.Private.Seed()) }

func (k Key) record() map[string]any {
	return map[string]any{
		FieldSecret:     hex.EncodeToString(k.Private.Seed()),
		FieldEndpointID: k.EndpointID,
		FieldDid:        k.Did,
	}
}

func KeyFromSeed(service v1alpha1.ClusterLinkService, seed []byte) (Key, error) {
	if len(seed) != ed25519.SeedSize {
		return Key{}, fmt.Errorf("clusterlink: an iroh secret key is a %d byte ed25519 seed, this one is %d",
			ed25519.SeedSize, len(seed))
	}
	priv := ed25519.NewKeyFromSeed(seed)
	pub := priv.Public().(ed25519.PublicKey)
	id, err := didkey.EndpointID(pub)
	if err != nil {
		return Key{}, err
	}
	did, err := didkey.FromEd25519(pub)
	if err != nil {
		return Key{}, err
	}
	return Key{Service: service, Private: priv, EndpointID: id, Did: did}, nil
}

func keyFromRecord(service v1alpha1.ClusterLinkService, data map[string]any) (Key, error) {
	raw, _ := data[FieldSecret].(string)
	seed, err := hex.DecodeString(raw)
	if err != nil || len(seed) != ed25519.SeedSize {
		return Key{}, fmt.Errorf("%w: %s/%s holds no %d byte hex %q", ErrStoredKeyUnreadable, Mount, service,
			ed25519.SeedSize, FieldSecret)
	}
	k, err := KeyFromSeed(service, seed)
	if err != nil {
		return Key{}, err
	}
	if id, _ := data[FieldEndpointID].(string); id != "" && id != k.EndpointID {
		return Key{}, fmt.Errorf("%w: %s/%s names endpoint %s and its secret derives %s",
			ErrStoredKeyUnreadable, Mount, service, id, k.EndpointID)
	}
	return k, nil
}

func EnsureKey(ctx context.Context, store KeyStore, service v1alpha1.ClusterLinkService, random io.Reader) (Key, error) {
	path := string(service)
	data, found, err := store.Read(ctx, path)
	if err != nil {
		return Key{}, fmt.Errorf("clusterlink: reading %s/%s: %w", Mount, path, err)
	}
	if found {
		return keyFromRecord(service, data)
	}
	seed := make([]byte, ed25519.SeedSize)
	if _, err := io.ReadFull(random, seed); err != nil {
		return Key{}, fmt.Errorf("clusterlink: generating the %s link key: %w", service, err)
	}
	k, err := KeyFromSeed(service, seed)
	if err != nil {
		return Key{}, err
	}
	created, err := store.Create(ctx, path, k.record())
	if err != nil {
		return Key{}, fmt.Errorf("clusterlink: storing the %s link key at %s/%s: %w", service, Mount, path, err)
	}
	if created {
		return k, nil
	}
	data, found, err = store.Read(ctx, path)
	if err != nil {
		return Key{}, fmt.Errorf("clusterlink: reading %s/%s after another writer stored it first: %w", Mount, path, err)
	}
	if !found {
		return Key{}, fmt.Errorf("clusterlink: %s/%s refused a first write as already present and then read as absent",
			Mount, path)
	}
	return keyFromRecord(service, data)
}

func ParseTicket(ticket string) (string, error) {
	body, ok := strings.CutPrefix(ticket, TicketPrefix)
	if !ok {
		return "", fmt.Errorf("%w: %q has no %q prefix", ErrBadTicket, ticket, TicketPrefix)
	}
	raw, err := ticketEncoding.DecodeString(strings.ToUpper(body))
	if err != nil {
		return "", fmt.Errorf("%w: %q is not base32: %v", ErrBadTicket, ticket, err)
	}
	if len(raw) < 1+ed25519.PublicKeySize || raw[0] != ticketVariant {
		return "", fmt.Errorf("%w: %q carries no version-0 endpoint address", ErrBadTicket, ticket)
	}
	return didkey.EndpointID(ed25519.PublicKey(raw[1 : 1+ed25519.PublicKeySize]))
}

func StableTicket(k Key) string {
	pub := k.Private.Public().(ed25519.PublicKey)
	body := append(append([]byte{ticketVariant}, pub...), 0x00)
	return TicketPrefix + strings.ToLower(ticketEncoding.EncodeToString(body))
}

func TicketIn(output string) (string, bool) {
	found := ""
	for line := range strings.SplitSeq(output, "\n") {
		_, rest, ok := strings.Cut(line, connectLine)
		if !ok {
			continue
		}
		fields := strings.Fields(rest)
		if len(fields) == 0 || !strings.HasPrefix(fields[len(fields)-1], TicketPrefix) {
			continue
		}
		found = fields[len(fields)-1]
	}
	return found, found != ""
}

func Argv(binary string, targetPort int) []string {
	return []string{
		binary, "listen-tcp",
		"--host", "127.0.0.1:" + strconv.Itoa(targetPort),
		"--ipv4-addr", "0.0.0.0:0",
	}
}

func Name(clusterKey string, service v1alpha1.ClusterLinkService) string {
	return clusterKey + "-" + string(service)
}

func Object(clusterKey string, service v1alpha1.ClusterLinkService, targetPort int, status v1alpha1.ClusterLinkStatus) v1alpha1.ClusterLink {
	return v1alpha1.ClusterLink{
		TypeMeta: metav1.TypeMeta{APIVersion: v1alpha1.GroupVersion.String(), Kind: "ClusterLink"},
		ObjectMeta: metav1.ObjectMeta{
			Name: Name(clusterKey, service),
		},
		Spec: v1alpha1.ClusterLinkSpec{
			ClusterRef: commonv1.LocalObjectRef{Name: clusterKey},
			Service:    service,
			TargetPort: targetPort,
		},
		Status: status,
	}
}

func Listening(k Key, printed string) (v1alpha1.ClusterLinkStatus, error) {
	id, err := ParseTicket(printed)
	if err != nil {
		return v1alpha1.ClusterLinkStatus{}, err
	}
	if id != k.EndpointID {
		return v1alpha1.ClusterLinkStatus{}, fmt.Errorf(
			"clusterlink: the %s link printed a ticket for endpoint %s and its key is %s: the "+
				"listener is not running as the key the vault holds", k.Service, id, k.EndpointID)
	}
	return v1alpha1.ClusterLinkStatus{
		Phase:      v1alpha1.ClusterLinkListening,
		EndpointID: k.EndpointID,
		Did:        k.Did,
		Ticket:     StableTicket(k),
	}, nil
}

func Failed(k Key, message string) v1alpha1.ClusterLinkStatus {
	return v1alpha1.ClusterLinkStatus{
		Phase:      v1alpha1.ClusterLinkFailed,
		EndpointID: k.EndpointID,
		Did:        k.Did,
		Message:    message,
	}
}