// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

// Program tl-longchain prints commands to re-sign Tailscale nodes that have
// long rotation signature chains.
//
// There is an implicit limit on the number of rotation signatures that can
// be chained before the signature becomes too long. This program helps
// tailnet admins to identify nodes that have signatures with long chains and
// prints commands to re-sign those node keys with a fresh direct signature.
// Commands are printed to stdout, while log messages are printed to stderr.
//
// Note that the Tailscale client this command is executed on must have
// ACL visibility to all other nodes to be able to see their signatures.
// https://tailscale.com/kb/1087/device-visibility
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"time"

	"tailscale.com/client/local"
	"tailscale.com/ipn/ipnstate"
	"tailscale.com/tka"
	"tailscale.com/types/key"
)

var (
	flagSocket   = flag.String("socket", "", "custom path to tailscaled socket")
	maxRotations = flag.Int("rotations", 10, "number of rotation signatures before re-signing (max 16)")
	showFiltered = flag.Bool("show-filtered", false, "include nodes with invalid signatures")
)

func main() {
	flag.Parse()

	lc := local.Client{Socket: *flagSocket}
	if lc.Socket != "" {
		lc.UseSocketOnly = true
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	st, err := lc.TailnetLockStatus(ctx)
	if err != nil {
		log.Fatalf("could not get Tailnet Lock status: %v", err)
	}
	if !st.Enabled {
		log.Print("Tailnet Lock is not enabled")
		return
	}
	print("Self", *st.NodeKey, *st.NodeKeySignature)
	if len(st.VisiblePeers) > 0 {
		log.Print("Visible peers with valid signatures:")
		for _, peer := range st.VisiblePeers {
			print(peerInfo(peer), peer.NodeKey, peer.NodeKeySignature)
		}
	}
	if *showFiltered && len(st.FilteredPeers) > 0 {
		log.Print("Visible peers with invalid signatures:")
		for _, peer := range st.FilteredPeers {
			print(peerInfo(peer), peer.NodeKey, peer.NodeKeySignature)
		}
	}
}

// peerInfo returns a string with information about a peer.
func peerInfo(peer *ipnstate.TKAPeer) string {
	return fmt.Sprintf("Peer %s (%s) nodeid=%s, current signature kind=%v", peer.Name, peer.TailscaleIPs[0], peer.StableID, peer.NodeKeySignature.SigKind)
}

// print prints a message about a node key signature and a re-signing command if needed.
func print(info string, nodeKey key.NodePublic, sig tka.NodeKeySignature) {
	if ln := chainLength(sig); ln > *maxRotations {
		log.Printf("%s: chain length %d, printing command to re-sign", info, ln)
		wrapping, _ := sig.UnverifiedWrappingPublic()
		fmt.Printf("tailscale lock sign %s %s\n", nodeKey, key.NLPublicFromEd25519Unsafe(wrapping).CLIString())
	} else {
		log.Printf("%s: does not need re-signing", info)
	}
}

// chainLength returns the length of the rotation signature chain.
func chainLength(sig tka.NodeKeySignature) int {
	if sig.SigKind != tka.SigRotation {
		return 1
	}
	return 1 + chainLength(*sig.Nested)
}
