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

// Package distsign implements signature and validation of arbitrary
// distributable files.
//
// There are 3 parties in this exchange:
//   - builder, which creates files, signs them with signing keys and publishes
//     to server
//   - server, which distributes public signing keys, files and signatures
//   - client, which downloads files and signatures from server, and validates
//     the signatures
//
// There are 2 types of keys:
//   - signing keys, that sign individual distributable files on the builder
//   - root keys, that sign signing keys and are kept offline
//
// root keys -(sign)-> signing keys -(sign)-> files
//
// All keys are asymmetric Ed25519 key pairs.
//
// The server serves static files under some known prefix. The kinds of files are:
//   - distsign.pub - bundle of PEM-encoded public signing keys
//   - distsign.pub.sig - signature of distsign.pub using one of the root keys
//   - $file - any distributable file
//   - $file.sig - signature of $file using any of the signing keys
//
// The root public keys are baked into the client software at compile time.
// These keys are long-lived and prove the validity of current signing keys
// from distsign.pub. To rotate root keys, a new client release must be
// published, they are not rotated dynamically. There are multiple root keys in
// different locations specifically to allow this rotation without using the
// discarded root key for any new signatures.
//
// The signing public keys are fetched by the client dynamically before every
// download and can be rotated more readily, assuming that most deployed
// clients trust the root keys used to issue fresh signing keys.
package distsign

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/binary"
	"encoding/pem"
	"errors"
	"fmt"
	"hash"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"time"

	"github.com/hdevalence/ed25519consensus"
	"golang.org/x/crypto/blake2s"
	"tailscale.com/feature"
	"tailscale.com/net/netutil"
	"tailscale.com/types/logger"
	"tailscale.com/util/httpm"
	"tailscale.com/util/must"
	"tailscale.com/util/progresstracking"
)

const (
	pemTypeRootPrivate    = "ROOT PRIVATE KEY"
	pemTypeRootPublic     = "ROOT PUBLIC KEY"
	pemTypeSigningPrivate = "SIGNING PRIVATE KEY"
	pemTypeSigningPublic  = "SIGNING PUBLIC KEY"

	downloadSizeLimit    = 1 << 29 // 512MB
	signingKeysSizeLimit = 1 << 20 // 1MB
	signatureSizeLimit   = ed25519.SignatureSize
)

// RootKey is a root key used to sign signing keys.
type RootKey struct {
	k ed25519.PrivateKey
}

// GenerateRootKey generates a new root key pair and encodes it as PEM.
func GenerateRootKey() (priv, pub []byte, err error) {
	pub, priv, err = ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return nil, nil, err
	}
	return pem.EncodeToMemory(&pem.Block{
			Type:  pemTypeRootPrivate,
			Bytes: []byte(priv),
		}), pem.EncodeToMemory(&pem.Block{
			Type:  pemTypeRootPublic,
			Bytes: []byte(pub),
		}), nil
}

// ParseRootKey parses the PEM-encoded private root key. The key must be in the
// same format as returned by GenerateRootKey.
func ParseRootKey(privKey []byte) (*RootKey, error) {
	k, err := parsePrivateKey(privKey, pemTypeRootPrivate)
	if err != nil {
		return nil, fmt.Errorf("failed to parse root key: %w", err)
	}
	return &RootKey{k: k}, nil
}

// SignSigningKeys signs the bundle of public signing keys. The bundle must be
// a sequence of PEM blocks joined with newlines.
func (r *RootKey) SignSigningKeys(pubBundle []byte) ([]byte, error) {
	if _, err := ParseSigningKeyBundle(pubBundle); err != nil {
		return nil, err
	}
	return ed25519.Sign(r.k, pubBundle), nil
}

// SigningKey is a signing key used to sign packages.
type SigningKey struct {
	k ed25519.PrivateKey
}

// GenerateSigningKey generates a new signing key pair and encodes it as PEM.
func GenerateSigningKey() (priv, pub []byte, err error) {
	pub, priv, err = ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return nil, nil, err
	}
	return pem.EncodeToMemory(&pem.Block{
			Type:  pemTypeSigningPrivate,
			Bytes: []byte(priv),
		}), pem.EncodeToMemory(&pem.Block{
			Type:  pemTypeSigningPublic,
			Bytes: []byte(pub),
		}), nil
}

// ParseSigningKey parses the PEM-encoded private signing key. The key must be
// in the same format as returned by GenerateSigningKey.
func ParseSigningKey(privKey []byte) (*SigningKey, error) {
	k, err := parsePrivateKey(privKey, pemTypeSigningPrivate)
	if err != nil {
		return nil, fmt.Errorf("failed to parse root key: %w", err)
	}
	return &SigningKey{k: k}, nil
}

// SignPackageHash signs the hash and the length of a package. Use PackageHash
// to compute the inputs.
func (s *SigningKey) SignPackageHash(hash []byte, len int64) ([]byte, error) {
	if len <= 0 {
		return nil, fmt.Errorf("package length must be positive, got %d", len)
	}
	msg := binary.LittleEndian.AppendUint64(hash, uint64(len))
	return ed25519.Sign(s.k, msg), nil
}

// PackageHash is a hash.Hash that counts the number of bytes written. Use it
// to get the hash and length inputs to SigningKey.SignPackageHash.
type PackageHash struct {
	hash.Hash
	len int64
}

// NewPackageHash returns an initialized PackageHash using BLAKE2s.
func NewPackageHash() *PackageHash {
	h, err := blake2s.New256(nil)
	if err != nil {
		// Should never happen with a nil key passed to blake2s.
		panic(err)
	}
	return &PackageHash{Hash: h}
}

func (ph *PackageHash) Write(b []byte) (int, error) {
	ph.len += int64(len(b))
	return ph.Hash.Write(b)
}

// Reset the PackageHash to its initial state.
func (ph *PackageHash) Reset() {
	ph.len = 0
	ph.Hash.Reset()
}

// Len returns the total number of bytes written.
func (ph *PackageHash) Len() int64 { return ph.len }

// Client downloads and validates files from a distribution server.
type Client struct {
	logf     logger.Logf
	roots    []ed25519.PublicKey
	pkgsAddr *url.URL
}

// NewClient returns a new client for distribution server located at pkgsAddr,
// and uses embedded root keys from the roots/ subdirectory of this package.
func NewClient(logf logger.Logf, pkgsAddr string) (*Client, error) {
	if logf == nil {
		logf = log.Printf
	}
	u, err := url.Parse(pkgsAddr)
	if err != nil {
		return nil, fmt.Errorf("invalid pkgsAddr %q: %w", pkgsAddr, err)
	}
	return &Client{logf: logf, roots: roots(), pkgsAddr: u}, nil
}

func (c *Client) url(path string) string {
	return c.pkgsAddr.JoinPath(path).String()
}

// Download fetches a file at path srcPath from pkgsAddr passed in NewClient.
// The file is downloaded to dstPath and its signature is validated using the
// embedded root keys. Download returns an error if anything goes wrong with
// the actual file download or with signature validation.
func (c *Client) Download(ctx context.Context, srcPath, dstPath string) error {
	// Always fetch a fresh signing key.
	sigPub, err := c.signingKeys()
	if err != nil {
		return err
	}

	srcURL := c.url(srcPath)
	sigURL := srcURL + ".sig"

	c.logf("Downloading %q", srcURL)
	dstPathUnverified := dstPath + ".unverified"
	hash, len, err := c.download(ctx, srcURL, dstPathUnverified, downloadSizeLimit)
	if err != nil {
		return err
	}
	c.logf("Downloading %q", sigURL)
	sig, err := fetch(sigURL, signatureSizeLimit)
	if err != nil {
		// Best-effort clean up of downloaded package.
		os.Remove(dstPathUnverified)
		return err
	}
	msg := binary.LittleEndian.AppendUint64(hash, uint64(len))
	if !VerifyAny(sigPub, msg, sig) {
		// Best-effort clean up of downloaded package.
		os.Remove(dstPathUnverified)
		return fmt.Errorf("signature %q for file %q does not validate with the current release signing key; either you are under attack, or attempting to download an old version of Tailscale which was signed with an older signing key", sigURL, srcURL)
	}
	c.logf("Signature OK")

	if err := os.Rename(dstPathUnverified, dstPath); err != nil {
		return fmt.Errorf("failed to move %q to %q after signature validation", dstPathUnverified, dstPath)
	}

	return nil
}

// ValidateLocalBinary fetches the latest signature associated with the binary
// at srcURLPath and uses it to validate the file located on disk via
// localFilePath. ValidateLocalBinary returns an error if anything goes wrong
// with the signature download or with signature validation.
func (c *Client) ValidateLocalBinary(srcURLPath, localFilePath string) error {
	// Always fetch a fresh signing key.
	sigPub, err := c.signingKeys()
	if err != nil {
		return err
	}

	srcURL := c.url(srcURLPath)
	sigURL := srcURL + ".sig"

	localFile, err := os.Open(localFilePath)
	if err != nil {
		return err
	}
	defer localFile.Close()

	h := NewPackageHash()
	_, err = io.Copy(h, localFile)
	if err != nil {
		return err
	}
	hash, hashLen := h.Sum(nil), h.Len()

	c.logf("Downloading %q", sigURL)
	sig, err := fetch(sigURL, signatureSizeLimit)
	if err != nil {
		return err
	}

	msg := binary.LittleEndian.AppendUint64(hash, uint64(hashLen))
	if !VerifyAny(sigPub, msg, sig) {
		return fmt.Errorf("signature %q for file %q does not validate with the current release signing key; either you are under attack, or attempting to download an old version of Tailscale which was signed with an older signing key", sigURL, localFilePath)
	}
	c.logf("Signature OK")

	return nil
}

// signingKeys fetches current signing keys from the server and validates them
// against the roots. Should be called before validation of any downloaded file
// to get the fresh keys.
func (c *Client) signingKeys() ([]ed25519.PublicKey, error) {
	keyURL := c.url("distsign.pub")
	sigURL := keyURL + ".sig"
	raw, err := fetch(keyURL, signingKeysSizeLimit)
	if err != nil {
		return nil, err
	}
	sig, err := fetch(sigURL, signatureSizeLimit)
	if err != nil {
		return nil, err
	}
	if !VerifyAny(c.roots, raw, sig) {
		return nil, fmt.Errorf("signature %q for key %q does not validate with any known root key; either you are under attack, or running a very old version of Tailscale with outdated root keys", sigURL, keyURL)
	}

	keys, err := ParseSigningKeyBundle(raw)
	if err != nil {
		return nil, fmt.Errorf("cannot parse signing key bundle from %q: %w", keyURL, err)
	}
	return keys, nil
}

// fetch reads the response body from url into memory, up to limit bytes.
func fetch(url string, limit int64) ([]byte, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	return io.ReadAll(io.LimitReader(resp.Body, limit))
}

// download writes the response body of url into a local file at dst, up to
// limit bytes. On success, the returned value is a BLAKE2s hash of the file.
func (c *Client) download(ctx context.Context, url, dst string, limit int64) ([]byte, int64, error) {
	tr := netutil.NewDefaultTransport()
	tr.Proxy = feature.HookProxyFromEnvironment.GetOrNil()
	defer tr.CloseIdleConnections()
	hc := &http.Client{
		Transport: tr,
		CheckRedirect: func(r *http.Request, via []*http.Request) error {
			c.logf("Download redirected to %q", r.URL)
			return nil
		},
	}

	quickCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()
	headReq := must.Get(http.NewRequestWithContext(quickCtx, httpm.HEAD, url, nil))

	res, err := hc.Do(headReq)
	if err != nil {
		return nil, 0, err
	}
	if res.StatusCode != http.StatusOK {
		return nil, 0, fmt.Errorf("HEAD %q: %v", url, res.Status)
	}
	if res.ContentLength <= 0 {
		return nil, 0, fmt.Errorf("HEAD %q: unexpected Content-Length %v", url, res.ContentLength)
	}
	c.logf("Download size: %v", res.ContentLength)

	dlReq := must.Get(http.NewRequestWithContext(ctx, httpm.GET, url, nil))
	dlRes, err := hc.Do(dlReq)
	if err != nil {
		return nil, 0, err
	}
	defer dlRes.Body.Close()
	// TODO(bradfitz): resume from existing partial file on disk
	if dlRes.StatusCode != http.StatusOK {
		return nil, 0, fmt.Errorf("GET %q: %v", url, dlRes.Status)
	}

	of, err := os.Create(dst)
	if err != nil {
		return nil, 0, err
	}
	defer of.Close()
	total := res.ContentLength
	pw := progresstracking.NewWriter(io.Discard, total, 2*time.Second, func(done int64) {
		c.logf("Downloaded %v/%v (%.1f%%)", done, total, float64(done)/float64(total)*100)
	})
	h := NewPackageHash()
	n, err := io.Copy(io.MultiWriter(of, h, pw), io.LimitReader(dlRes.Body, limit))
	if err != nil {
		return nil, n, err
	}
	if n != res.ContentLength {
		return nil, n, fmt.Errorf("GET %q: downloaded %v, want %v", url, n, res.ContentLength)
	}
	if err := dlRes.Body.Close(); err != nil {
		return nil, n, err
	}
	if err := of.Close(); err != nil {
		return nil, n, err
	}

	return h.Sum(nil), h.Len(), nil
}

func parsePrivateKey(data []byte, typeTag string) (ed25519.PrivateKey, error) {
	b, rest := pem.Decode(data)
	if b == nil {
		return nil, errors.New("failed to decode PEM data")
	}
	if len(rest) > 0 {
		return nil, errors.New("trailing PEM data")
	}
	if b.Type != typeTag {
		return nil, fmt.Errorf("PEM type is %q, want %q", b.Type, typeTag)
	}
	if len(b.Bytes) != ed25519.PrivateKeySize {
		return nil, errors.New("private key has incorrect length for an Ed25519 private key")
	}
	return ed25519.PrivateKey(b.Bytes), nil
}

// ParseSigningKeyBundle parses the bundle of PEM-encoded public signing keys.
func ParseSigningKeyBundle(bundle []byte) ([]ed25519.PublicKey, error) {
	return parsePublicKeyBundle(bundle, pemTypeSigningPublic)
}

// ParseRootKeyBundle parses the bundle of PEM-encoded public root keys.
func ParseRootKeyBundle(bundle []byte) ([]ed25519.PublicKey, error) {
	return parsePublicKeyBundle(bundle, pemTypeRootPublic)
}

func parsePublicKeyBundle(bundle []byte, typeTag string) ([]ed25519.PublicKey, error) {
	var keys []ed25519.PublicKey
	for len(bundle) > 0 {
		pub, rest, err := parsePublicKey(bundle, typeTag)
		if err != nil {
			return nil, err
		}
		keys = append(keys, pub)
		bundle = rest
	}
	if len(keys) == 0 {
		return nil, errors.New("no signing keys found in the bundle")
	}
	return keys, nil
}

func parseSinglePublicKey(data []byte, typeTag string) (ed25519.PublicKey, error) {
	pub, rest, err := parsePublicKey(data, typeTag)
	if err != nil {
		return nil, err
	}
	if len(rest) > 0 {
		return nil, errors.New("trailing PEM data")
	}
	return pub, err
}

func parsePublicKey(data []byte, typeTag string) (pub ed25519.PublicKey, rest []byte, retErr error) {
	b, rest := pem.Decode(data)
	if b == nil {
		return nil, nil, errors.New("failed to decode PEM data")
	}
	if b.Type != typeTag {
		return nil, nil, fmt.Errorf("PEM type is %q, want %q", b.Type, typeTag)
	}
	if len(b.Bytes) != ed25519.PublicKeySize {
		return nil, nil, errors.New("public key has incorrect length for an Ed25519 public key")
	}
	return ed25519.PublicKey(b.Bytes), rest, nil
}

// VerifyAny verifies whether sig is valid for msg using any of the keys.
// VerifyAny will panic if any of the keys have the wrong size for Ed25519.
func VerifyAny(keys []ed25519.PublicKey, msg, sig []byte) bool {
	for _, k := range keys {
		if ed25519consensus.Verify(k, msg, sig) {
			return true
		}
	}
	return false
}
