~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
257 lines · 6.4 KB Raw History
package statusstate

import (
	"context"
	"crypto/sha256"
	"crypto/x509"
	"encoding/hex"
	"encoding/pem"
	"errors"
	"fmt"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/vault/openbao"
)

const (
	DefaultRotationWindow = 30 * 24 * time.Hour

	maxCertificateScan = 256
)

type VaultCredentialObservation struct {
	Exists bool

	ExpiresAt string

	RotationDue bool
}

type VaultCertificateObservation struct {
	Exists bool

	SerialNumber string

	Fingerprint string

	ExpiresAt string

	RotationDue bool
}

type VaultSource interface {
	CredentialMetadata(ctx context.Context, server, mount, path string) (VaultCredentialObservation, error)

	CertificateMetadata(ctx context.Context, server, mount, role, commonName string) (VaultCertificateObservation, error)
}

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

	List(ctx context.Context, path string) ([]string, error)
}

type OpenBaoMetadataSource struct {
	Open func(ctx context.Context, server, mount string) (MetadataReader, error)

	RotationWindow time.Duration
}

var _ VaultSource = (*OpenBaoMetadataSource)(nil)

func (s *OpenBaoMetadataSource) CredentialMetadata(ctx context.Context, server, mount, path string) (VaultCredentialObservation, error) {
	reader, err := s.reader(ctx, server, mount)
	if err != nil {
		return VaultCredentialObservation{}, err
	}
	metadata, err := reader.Read(ctx, metadataPath(mount, path))
	if errors.Is(err, openbao.ErrNotFound) {
		return VaultCredentialObservation{}, nil
	}
	if err != nil {
		return VaultCredentialObservation{}, err
	}
	metadata = unwrapMetadata(metadata)
	if len(metadata) == 0 {
		return VaultCredentialObservation{}, nil
	}
	if versionDeleted(metadata, int64Of(metadata["current_version"])) {
		return VaultCredentialObservation{}, nil
	}
	observation := VaultCredentialObservation{Exists: true}
	custom := mapOf(metadata["custom_metadata"])
	observation.ExpiresAt = firstString(custom, "expires_at", "expiresAt", "expiry")
	observation.RotationDue = boolOf(custom["rotation_due"]) || boolOf(custom["rotationDue"])
	if observation.ExpiresAt != "" {
		if expiry, err := time.Parse(time.RFC3339, observation.ExpiresAt); err == nil && time.Now().Add(s.rotationWindow()).After(expiry) {
			observation.RotationDue = true
		}
	}
	return observation, nil
}

func (s *OpenBaoMetadataSource) CertificateMetadata(ctx context.Context, server, mount, role, commonName string) (VaultCertificateObservation, error) {
	_ = role
	reader, err := s.reader(ctx, server, mount)
	if err != nil {
		return VaultCertificateObservation{}, err
	}
	serials, err := reader.List(ctx, strings.Trim(mount, "/")+"/certs")
	if errors.Is(err, openbao.ErrNotFound) {
		return VaultCertificateObservation{}, nil
	}
	if err != nil {
		return VaultCertificateObservation{}, err
	}
	scanned := 0
	for _, serial := range serials {
		if scanned >= maxCertificateScan {
			break
		}
		scanned++
		entry, err := reader.Read(ctx, strings.Trim(mount, "/")+"/cert/"+url.PathEscape(serial))
		if errors.Is(err, openbao.ErrNotFound) {
			continue
		}
		if err != nil {
			return VaultCertificateObservation{}, err
		}
		entry = unwrapMetadata(entry)
		if int64Of(entry["revocation_time"]) != 0 || firstString(entry, "revocation_time_rfc3339") != "" {
			continue
		}
		certificate, err := parseCertificate(firstString(entry, "certificate"))
		if err != nil {
			continue
		}
		if certificate.Subject.CommonName != commonName {
			continue
		}
		serialNumber := firstString(entry, "serial_number")
		if serialNumber == "" {
			serialNumber = certificate.SerialNumber.Text(16)
		}
		observation := VaultCertificateObservation{
			Exists:       true,
			SerialNumber: serialNumber,
			Fingerprint:  certificateFingerprint(certificate),
			ExpiresAt:    certificate.NotAfter.UTC().Format(time.RFC3339),
			RotationDue:  time.Now().Add(s.rotationWindow()).After(certificate.NotAfter),
		}
		return observation, nil
	}
	return VaultCertificateObservation{}, nil
}

func (s *OpenBaoMetadataSource) reader(ctx context.Context, server, mount string) (MetadataReader, error) {
	if s == nil || s.Open == nil {
		return nil, fmt.Errorf("statusstate: the OpenBao metadata source has no Open function")
	}
	return s.Open(ctx, server, strings.Trim(mount, "/"))
}

func (s *OpenBaoMetadataSource) rotationWindow() time.Duration {
	if s == nil || s.RotationWindow <= 0 {
		return DefaultRotationWindow
	}
	return s.RotationWindow
}

func metadataPath(mount, path string) string {
	mount = strings.Trim(mount, "/")
	path = strings.Trim(path, "/")
	if strings.HasPrefix(path, mount+"/metadata/") {
		return path
	}
	return mount + "/metadata/" + path
}

func unwrapMetadata(metadata map[string]any) map[string]any {
	if inner, ok := metadata["data"].(map[string]any); ok {
		return inner
	}
	return metadata
}

func versionDeleted(metadata map[string]any, current int64) bool {
	versions := mapOf(metadata["versions"])
	if len(versions) == 0 || current <= 0 {
		return false
	}
	entry := mapOf(versions[strconv.FormatInt(current, 10)])
	return firstString(entry, "deletion_time") != ""
}

func parseCertificate(value string) (*x509.Certificate, error) {
	block, _ := pem.Decode([]byte(value))
	if block == nil {
		return nil, fmt.Errorf("statusstate: the certificate is not PEM")
	}
	return x509.ParseCertificate(block.Bytes)
}

func certificateFingerprint(certificate *x509.Certificate) string {
	sum := sha256.Sum256(certificate.Raw)
	return hex.EncodeToString(sum[:])
}

func mapOf(value any) map[string]any {
	if out, ok := value.(map[string]any); ok {
		return out
	}
	return nil
}

func firstString(values map[string]any, keys ...string) string {
	for _, key := range keys {
		if out := stringOf(values[key]); out != "" {
			return out
		}
	}
	return ""
}

func stringOf(value any) string {
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed)
	case fmt.Stringer:
		return strings.TrimSpace(typed.String())
	default:
		return ""
	}
}

func boolOf(value any) bool {
	switch typed := value.(type) {
	case bool:
		return typed
	case string:
		out, err := strconv.ParseBool(strings.TrimSpace(typed))
		return err == nil && out
	default:
		return false
	}
}

func int64Of(value any) int64 {
	switch typed := value.(type) {
	case int:
		return int64(typed)
	case int32:
		return int64(typed)
	case int64:
		return typed
	case float64:
		return int64(typed)
	case string:
		out, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
		if err == nil {
			return out
		}
	}
	return 0
}