~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
150 lines · 3.7 KB Raw History
package identitybundle

import (
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"
)

var Paths = []string{
	"tls/server-ca.crt",
	"tls/server-ca.key",
	"tls/client-ca.crt",
	"tls/client-ca.key",
	"tls/request-header-ca.crt",
	"tls/request-header-ca.key",
	"tls/etcd/server-ca.crt",
	"tls/etcd/server-ca.key",
	"tls/etcd/peer-ca.crt",
	"tls/etcd/peer-ca.key",
	"tls/service.key",
	"cred/passwd",
	"cred/ipsec.psk",
}

type Entry struct {
	Path    string
	Bytes   []byte
	ModTime time.Time
}

type Bundle struct {
	Entries []Entry
}

var ErrIncomplete = errors.New("identitybundle: incomplete")

var ErrMismatch = errors.New("identitybundle: mismatch")

func Harvest(dataDir string) (*Bundle, error) {
	b := &Bundle{Entries: make([]Entry, 0, len(Paths))}
	for _, rel := range Paths {
		full := filepath.Join(dataDir, rel)
		info, err := os.Stat(full)
		if err != nil {
			return nil, fmt.Errorf("%w: %s: %w", ErrIncomplete, rel, err)
		}
		data, err := os.ReadFile(full)
		if err != nil {
			return nil, fmt.Errorf("%w: %s: %w", ErrIncomplete, rel, err)
		}
		b.Entries = append(b.Entries, Entry{Path: rel, Bytes: data, ModTime: info.ModTime()})
	}
	return b, nil
}

func (b *Bundle) Verify(dataDir string) error {
	if len(b.Entries) != len(Paths) {
		return fmt.Errorf("%w: bundle has %d entries, want %d", ErrIncomplete, len(b.Entries), len(Paths))
	}
	for _, e := range b.Entries {
		got, err := os.ReadFile(filepath.Join(dataDir, e.Path))
		if err != nil {
			return fmt.Errorf("%w: %s: %w", ErrMismatch, e.Path, err)
		}
		if !equal(got, e.Bytes) {
			return fmt.Errorf("%w: %s differs (on disk %s, expected %s)",
				ErrMismatch, e.Path, Sum(got), Sum(e.Bytes))
		}
	}
	return nil
}

func (b *Bundle) Seed(dataDir string) error {
	if len(b.Entries) != len(Paths) {
		return fmt.Errorf("%w: bundle has %d entries, want %d", ErrIncomplete, len(b.Entries), len(Paths))
	}
	for _, e := range b.Entries {
		full := filepath.Join(dataDir, e.Path)
		if err := os.MkdirAll(filepath.Dir(full), 0o700); err != nil {
			return fmt.Errorf("identitybundle: mkdir for %s: %w", e.Path, err)
		}
		if err := os.WriteFile(full, e.Bytes, 0o600); err != nil {
			return fmt.Errorf("identitybundle: write %s: %w", e.Path, err)
		}
		if err := os.Chtimes(full, e.ModTime, e.ModTime); err != nil {
			return fmt.Errorf("identitybundle: chtimes %s: %w", e.Path, err)
		}
	}
	return b.Verify(dataDir)
}

func Sum(b []byte) string {
	h := sha256.Sum256(b)
	return hex.EncodeToString(h[:])
}

const ClusterUIDFile = "cluster-uid"

var ErrEarlierCreation = errors.New("identitybundle: identity bundle belongs to an earlier creation of this cluster")

func ReadClusterUID(dir string) (string, error) {
	raw, err := os.ReadFile(filepath.Join(dir, ClusterUIDFile))
	if errors.Is(err, os.ErrNotExist) {
		return "", nil
	}
	if err != nil {
		return "", fmt.Errorf("identitybundle: reading %s: %w", ClusterUIDFile, err)
	}
	return string(raw), nil
}

func WriteClusterUID(dir, uid string) error {
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return fmt.Errorf("identitybundle: mkdir for %s: %w", ClusterUIDFile, err)
	}
	if err := os.WriteFile(filepath.Join(dir, ClusterUIDFile), []byte(uid), 0o600); err != nil {
		return fmt.Errorf("identitybundle: write %s: %w", ClusterUIDFile, err)
	}
	return nil
}

func BindClusterUID(dir, uid string) (adopted bool, err error) {
	have, err := ReadClusterUID(dir)
	if err != nil {
		return false, err
	}
	switch have {
	case uid:
		return false, nil
	case "":
		return true, WriteClusterUID(dir, uid)
	}
	return false, fmt.Errorf("%w: it is bound to cluster uid %s and this creation is %s", ErrEarlierCreation, have, uid)
}

func equal(a, b []byte) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}