mirror of
https://github.com/kairos-io/kcrypt-challenger.git
synced 2025-09-25 06:12:12 +00:00
479 lines
17 KiB
Go
479 lines
17 KiB
Go
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/jaypipes/ghw/pkg/block"
|
||
"github.com/kairos-io/kairos-challenger/cmd/discovery/client"
|
||
"github.com/kairos-io/kairos-challenger/pkg/constants"
|
||
"github.com/kairos-io/kairos-sdk/kcrypt/bus"
|
||
"github.com/kairos-io/kairos-sdk/types"
|
||
"github.com/kairos-io/tpm-helpers"
|
||
"github.com/spf13/cobra"
|
||
)
|
||
|
||
// GetFlags holds all flags specific to the get command
|
||
type GetFlags struct {
|
||
PartitionName string
|
||
PartitionUUID string
|
||
PartitionLabel string
|
||
Attempts int
|
||
ChallengerServer string
|
||
EnableMDNS bool
|
||
ServerCertificate string
|
||
}
|
||
|
||
var (
|
||
// Global/persistent flags
|
||
debug bool
|
||
)
|
||
|
||
// rootCmd represents the base command (TPM hash generation)
|
||
var rootCmd = &cobra.Command{
|
||
Use: "kcrypt-discovery-challenger",
|
||
Short: "kcrypt-challenger discovery client",
|
||
Long: `kcrypt-challenger discovery client
|
||
|
||
This tool provides TPM-based operations for encrypted partition management.
|
||
By default, it outputs the TPM hash for this device.
|
||
|
||
Configuration:
|
||
The client reads configuration from Kairos configuration files in the following directories:
|
||
- /oem (during installation from ISO)
|
||
- /sysroot/oem (on installed systems during initramfs)
|
||
- /tmp/oem (when running in hooks)
|
||
|
||
Configuration format (YAML):
|
||
kcrypt:
|
||
challenger:
|
||
challenger_server: "https://my-server.com:8082" # Server URL
|
||
mdns: true # Enable mDNS discovery
|
||
certificate: "/path/to/server-cert.pem" # Server certificate
|
||
nv_index: "0x1500000" # TPM NV index (offline mode)
|
||
c_index: "0x1500001" # TPM certificate index
|
||
tpm_device: "/dev/tpmrm0" # TPM device path`,
|
||
Example: ` # Get TPM hash for this device (default)
|
||
kcrypt-discovery-challenger
|
||
|
||
# Get passphrase for encrypted partition
|
||
kcrypt-discovery-challenger get --partition-name=/dev/sda2
|
||
|
||
# Clean up TPM NV memory (useful for development)
|
||
kcrypt-discovery-challenger cleanup
|
||
|
||
# Run plugin event
|
||
kcrypt-discovery-challenger discovery.password`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
return runTPMHash()
|
||
},
|
||
}
|
||
|
||
// newCleanupCmd creates the cleanup command
|
||
func newCleanupCmd() *cobra.Command {
|
||
var nvIndex string
|
||
var tpmDevice string
|
||
var skipConfirmation bool
|
||
|
||
cmd := &cobra.Command{
|
||
Use: "cleanup",
|
||
Short: "Clean up TPM NV memory",
|
||
Long: `Clean up TPM NV memory by undefining specific NV indices.
|
||
|
||
⚠️ DANGER: This command removes encryption passphrases from TPM memory!
|
||
⚠️ If you delete the wrong index, your encrypted disk may become UNBOOTABLE!
|
||
|
||
This command helps clean up TPM NV memory used by the local pass flow,
|
||
which stores encrypted passphrases in TPM non-volatile memory. Without
|
||
cleanup, these passphrases persist indefinitely and take up space.
|
||
|
||
The command will prompt for confirmation before deletion unless you use
|
||
the --i-know-what-i-am-doing flag to skip the safety prompt.
|
||
|
||
Default behavior:
|
||
- Uses the same NV index as the local pass flow (from config or 0x1500000)
|
||
- Uses the same TPM device as configured (or system default if none specified)
|
||
- Prompts for confirmation with safety warnings`,
|
||
Example: ` # Clean up default NV index (with confirmation prompt)
|
||
kcrypt-discovery-challenger cleanup
|
||
|
||
# Clean up specific NV index
|
||
kcrypt-discovery-challenger cleanup --nv-index=0x1500001
|
||
|
||
# Clean up with specific TPM device
|
||
kcrypt-discovery-challenger cleanup --tpm-device=/dev/tpmrm0
|
||
|
||
# Skip confirmation prompt (DANGEROUS!)
|
||
kcrypt-discovery-challenger cleanup --i-know-what-i-am-doing`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
return runCleanup(nvIndex, tpmDevice, skipConfirmation)
|
||
},
|
||
}
|
||
|
||
cmd.Flags().StringVar(&nvIndex, "nv-index", "", fmt.Sprintf("NV index to clean up (defaults to configured index or %s)", client.DefaultNVIndex))
|
||
cmd.Flags().StringVar(&tpmDevice, "tpm-device", "", "TPM device path (defaults to configured device or system default)")
|
||
cmd.Flags().BoolVar(&skipConfirmation, "i-know-what-i-am-doing", false, "Skip confirmation prompt (DANGEROUS: may make encrypted disks unbootable)")
|
||
|
||
return cmd
|
||
}
|
||
|
||
// newGetCmd creates the get command with its flags
|
||
func newGetCmd() *cobra.Command {
|
||
flags := &GetFlags{}
|
||
|
||
cmd := &cobra.Command{
|
||
Use: "get",
|
||
Short: "Get passphrase for encrypted partition",
|
||
Long: `Get passphrase for encrypted partition using TPM attestation.
|
||
|
||
This command retrieves passphrases for encrypted partitions by communicating
|
||
with a challenger server using TPM-based attestation. At least one partition
|
||
identifier (name, UUID, or label) must be provided.
|
||
|
||
The command uses configuration from the root command's config files, but flags
|
||
can override specific settings:
|
||
--challenger-server Override kcrypt.challenger.challenger_server
|
||
--mdns Override kcrypt.challenger.mdns
|
||
--certificate Override kcrypt.challenger.certificate`,
|
||
Example: ` # Get passphrase using partition name
|
||
kcrypt-discovery-challenger get --partition-name=/dev/sda2
|
||
|
||
# Get passphrase using UUID
|
||
kcrypt-discovery-challenger get --partition-uuid=12345-abcde
|
||
|
||
# Get passphrase using filesystem label
|
||
kcrypt-discovery-challenger get --partition-label=encrypted-data
|
||
|
||
# Get passphrase with multiple identifiers
|
||
kcrypt-discovery-challenger get --partition-name=/dev/sda2 --partition-uuid=12345-abcde --partition-label=encrypted-data
|
||
|
||
# Get passphrase with custom server
|
||
kcrypt-discovery-challenger get --partition-label=encrypted-data --challenger-server=https://my-server.com:8082`,
|
||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||
// Validate that at least one partition identifier is provided
|
||
if flags.PartitionName == "" && flags.PartitionUUID == "" && flags.PartitionLabel == "" {
|
||
return fmt.Errorf("at least one of --partition-name, --partition-uuid, or --partition-label must be provided")
|
||
}
|
||
return nil
|
||
},
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
return runGetPassphrase(flags)
|
||
},
|
||
}
|
||
|
||
// Register flags
|
||
cmd.Flags().StringVar(&flags.PartitionName, "partition-name", "", "Name of the partition (at least one identifier required)")
|
||
cmd.Flags().StringVar(&flags.PartitionUUID, "partition-uuid", "", "UUID of the partition (at least one identifier required)")
|
||
cmd.Flags().StringVar(&flags.PartitionLabel, "partition-label", "", "Filesystem label of the partition (at least one identifier required)")
|
||
cmd.Flags().IntVar(&flags.Attempts, "attempts", 30, "Number of attempts to get the passphrase")
|
||
cmd.Flags().StringVar(&flags.ChallengerServer, "challenger-server", "", "URL of the challenger server (overrides config)")
|
||
cmd.Flags().BoolVar(&flags.EnableMDNS, "mdns", false, "Enable mDNS discovery (overrides config)")
|
||
cmd.Flags().StringVar(&flags.ServerCertificate, "certificate", "", "Server certificate for verification (overrides config)")
|
||
|
||
return cmd
|
||
}
|
||
|
||
// pluginCmd represents the plugin event commands
|
||
var pluginCmd = &cobra.Command{
|
||
Use: string(bus.EventDiscoveryPassword),
|
||
Short: fmt.Sprintf("Run %s plugin event", bus.EventDiscoveryPassword),
|
||
Long: fmt.Sprintf(`Run the %s plugin event.
|
||
|
||
This command runs in plugin mode, reading JSON partition data from stdin
|
||
and outputting the passphrase to stdout. This is used for integration
|
||
with kcrypt and other tools.`, bus.EventDiscoveryPassword),
|
||
Example: fmt.Sprintf(` # Plugin mode (for integration with kcrypt)
|
||
echo '{"data": "{\"name\": \"/dev/sda2\", \"uuid\": \"12345-abcde\", \"label\": \"encrypted-data\"}"}' | kcrypt-discovery-challenger %s`, bus.EventDiscoveryPassword),
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
return runPluginMode()
|
||
},
|
||
}
|
||
|
||
func init() {
|
||
// Global/persistent flags (available to all commands)
|
||
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "Enable debug logging")
|
||
|
||
// Add subcommands
|
||
rootCmd.AddCommand(newGetCmd())
|
||
rootCmd.AddCommand(newCleanupCmd())
|
||
rootCmd.AddCommand(pluginCmd)
|
||
}
|
||
|
||
func main() {
|
||
if err := rootCmd.Execute(); err != nil {
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
// ExecuteWithArgs executes the root command with the given arguments.
|
||
// This function is used by tests to simulate CLI execution.
|
||
func ExecuteWithArgs(args []string) error {
|
||
// Set command arguments (this overrides os.Args)
|
||
rootCmd.SetArgs(args)
|
||
|
||
return rootCmd.Execute()
|
||
}
|
||
|
||
// runTPMHash handles the root command - TPM hash generation
|
||
func runTPMHash() error {
|
||
// Create logger based on debug flag
|
||
var logger types.KairosLogger
|
||
if debug {
|
||
logger = types.NewKairosLogger("kcrypt-discovery-challenger", "debug", false)
|
||
logger.Debugf("Debug mode enabled for TPM hash generation")
|
||
} else {
|
||
logger = types.NewKairosLogger("kcrypt-discovery-challenger", "error", false)
|
||
}
|
||
|
||
// Initialize AK Manager with the standard handle file
|
||
logger.Debugf("Initializing AK Manager with handle file: %s", constants.AKBlobFile)
|
||
akManager, err := tpm.NewAKManager(tpm.WithAKHandleFile(constants.AKBlobFile))
|
||
if err != nil {
|
||
return fmt.Errorf("creating AK manager: %w", err)
|
||
}
|
||
logger.Debugf("AK Manager initialized successfully")
|
||
|
||
// Ensure AK exists (create if necessary)
|
||
logger.Debugf("Getting or creating AK")
|
||
_, err = akManager.GetOrCreateAK()
|
||
if err != nil {
|
||
return fmt.Errorf("getting/creating AK: %w", err)
|
||
}
|
||
logger.Debugf("AK obtained/created successfully")
|
||
|
||
// Get attestation data (includes EK)
|
||
logger.Debugf("Getting attestation data")
|
||
ek, _, err := akManager.GetAttestationData()
|
||
if err != nil {
|
||
return fmt.Errorf("getting attestation data: %w", err)
|
||
}
|
||
logger.Debugf("Attestation data retrieved successfully")
|
||
|
||
// Compute TPM hash from EK
|
||
logger.Debugf("Computing TPM hash from EK")
|
||
tpmHash, err := tpm.DecodePubHash(ek)
|
||
if err != nil {
|
||
return fmt.Errorf("computing TPM hash: %w", err)
|
||
}
|
||
logger.Debugf("TPM hash computed successfully: %s", tpmHash)
|
||
|
||
// Output the TPM hash to stdout
|
||
fmt.Print(tpmHash)
|
||
return nil
|
||
}
|
||
|
||
// runGetPassphrase handles the get subcommand - passphrase retrieval
|
||
func runGetPassphrase(flags *GetFlags) error {
|
||
// Create logger based on debug flag
|
||
var logger types.KairosLogger
|
||
if debug {
|
||
logger = types.NewKairosLogger("kcrypt-discovery-challenger", "debug", false)
|
||
} else {
|
||
logger = types.NewKairosLogger("kcrypt-discovery-challenger", "error", false)
|
||
}
|
||
|
||
// Create client with potential CLI overrides
|
||
c, err := createClientWithOverrides(flags.ChallengerServer, flags.EnableMDNS, flags.ServerCertificate, logger)
|
||
if err != nil {
|
||
return fmt.Errorf("creating client: %w", err)
|
||
}
|
||
|
||
// Create partition object
|
||
partition := &block.Partition{
|
||
Name: flags.PartitionName,
|
||
UUID: flags.PartitionUUID,
|
||
FilesystemLabel: flags.PartitionLabel,
|
||
}
|
||
|
||
// Log partition information
|
||
logger.Debugf("Partition details:")
|
||
logger.Debugf(" Name: %s", partition.Name)
|
||
logger.Debugf(" UUID: %s", partition.UUID)
|
||
logger.Debugf(" Label: %s", partition.FilesystemLabel)
|
||
logger.Debugf(" Attempts: %d", flags.Attempts)
|
||
|
||
// Get the passphrase using the same backend logic as the plugin
|
||
fmt.Fprintf(os.Stderr, "Requesting passphrase for partition %s (UUID: %s, Label: %s)...\n",
|
||
flags.PartitionName, flags.PartitionUUID, flags.PartitionLabel)
|
||
|
||
passphrase, err := c.GetPassphrase(partition, flags.Attempts)
|
||
if err != nil {
|
||
return fmt.Errorf("getting passphrase: %w", err)
|
||
}
|
||
|
||
// Output the passphrase to stdout (this is what tools expect)
|
||
fmt.Print(passphrase)
|
||
fmt.Fprintf(os.Stderr, "\nPassphrase retrieved successfully\n")
|
||
return nil
|
||
}
|
||
|
||
// runPluginMode handles plugin event commands
|
||
func runPluginMode() error {
|
||
// In plugin mode, use quiet=true to log to file instead of console
|
||
// Log level depends on debug flag, write logs to /var/log/kairos/kcrypt-discovery-challenger.log
|
||
var logLevel string
|
||
if debug {
|
||
logLevel = "debug"
|
||
} else {
|
||
logLevel = "error"
|
||
}
|
||
|
||
logger := types.NewKairosLogger("kcrypt-discovery-challenger", logLevel, true)
|
||
c, err := client.NewClientWithLogger(logger)
|
||
if err != nil {
|
||
return fmt.Errorf("creating client: %w", err)
|
||
}
|
||
|
||
err = c.Start()
|
||
if err != nil {
|
||
return fmt.Errorf("starting plugin: %w", err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// createClientWithOverrides creates a client and applies CLI flag overrides to the config
|
||
func createClientWithOverrides(serverURL string, enableMDNS bool, certificate string, logger types.KairosLogger) (*client.Client, error) {
|
||
// Start with the default config from files and pass the logger
|
||
c, err := client.NewClientWithLogger(logger)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Log the original configuration values
|
||
logger.Debugf("Original configuration:")
|
||
logger.Debugf(" Server: %s", c.Config.Kcrypt.Challenger.Server)
|
||
logger.Debugf(" MDNS: %t", c.Config.Kcrypt.Challenger.MDNS)
|
||
logger.Debugf(" Certificate: %s", maskSensitiveString(c.Config.Kcrypt.Challenger.Certificate))
|
||
|
||
// Apply CLI overrides if provided
|
||
if serverURL != "" {
|
||
logger.Debugf("Overriding server URL: %s -> %s", c.Config.Kcrypt.Challenger.Server, serverURL)
|
||
c.Config.Kcrypt.Challenger.Server = serverURL
|
||
}
|
||
|
||
// For boolean flags, we can directly use the value since Cobra handles it properly
|
||
if enableMDNS {
|
||
logger.Debugf("Overriding MDNS setting: %t -> %t", c.Config.Kcrypt.Challenger.MDNS, enableMDNS)
|
||
c.Config.Kcrypt.Challenger.MDNS = enableMDNS
|
||
}
|
||
|
||
if certificate != "" {
|
||
logger.Debugf("Overriding certificate: %s -> %s",
|
||
maskSensitiveString(c.Config.Kcrypt.Challenger.Certificate),
|
||
maskSensitiveString(certificate))
|
||
c.Config.Kcrypt.Challenger.Certificate = certificate
|
||
}
|
||
|
||
// Log the final configuration values
|
||
logger.Debugf("Final configuration:")
|
||
logger.Debugf(" Server: %s", c.Config.Kcrypt.Challenger.Server)
|
||
logger.Debugf(" MDNS: %t", c.Config.Kcrypt.Challenger.MDNS)
|
||
logger.Debugf(" Certificate: %s", maskSensitiveString(c.Config.Kcrypt.Challenger.Certificate))
|
||
|
||
return c, nil
|
||
}
|
||
|
||
// runCleanup handles the cleanup subcommand - TPM NV memory cleanup
|
||
func runCleanup(nvIndex, tpmDevice string, skipConfirmation bool) error {
|
||
// Create logger based on debug flag
|
||
var logger types.KairosLogger
|
||
if debug {
|
||
logger = types.NewKairosLogger("kcrypt-discovery-challenger", "debug", false)
|
||
logger.Debugf("Debug mode enabled for TPM NV cleanup")
|
||
} else {
|
||
logger = types.NewKairosLogger("kcrypt-discovery-challenger", "error", false)
|
||
}
|
||
|
||
// Load configuration to get defaults if flags not provided
|
||
var config client.Config
|
||
c, err := client.NewClientWithLogger(logger)
|
||
if err != nil {
|
||
logger.Debugf("Warning: Could not load configuration: %v", err)
|
||
// Continue with defaults - not a fatal error
|
||
} else {
|
||
config = c.Config
|
||
}
|
||
|
||
// Determine NV index to clean up (follow same pattern as localPass/genAndStore)
|
||
targetIndex := nvIndex
|
||
if targetIndex == "" {
|
||
// First check config, then fall back to the same default used by the local pass flow
|
||
if config.Kcrypt.Challenger.NVIndex != "" {
|
||
targetIndex = config.Kcrypt.Challenger.NVIndex
|
||
} else {
|
||
targetIndex = client.DefaultNVIndex
|
||
}
|
||
}
|
||
|
||
// Determine TPM device
|
||
targetDevice := tpmDevice
|
||
if targetDevice == "" && config.Kcrypt.Challenger.TPMDevice != "" {
|
||
targetDevice = config.Kcrypt.Challenger.TPMDevice
|
||
}
|
||
|
||
logger.Debugf("Cleaning up TPM NV index: %s", targetIndex)
|
||
if targetDevice != "" {
|
||
logger.Debugf("Using TPM device: %s", targetDevice)
|
||
}
|
||
|
||
// Check if the NV index exists first
|
||
opts := []tpm.TPMOption{tpm.WithIndex(targetIndex)}
|
||
if targetDevice != "" {
|
||
opts = append(opts, tpm.WithDevice(targetDevice))
|
||
}
|
||
|
||
// Try to read from the index to see if it exists
|
||
logger.Debugf("Checking if NV index %s exists", targetIndex)
|
||
_, err = tpm.ReadBlob(opts...)
|
||
if err != nil {
|
||
// If we can't read it, it might not exist or be empty
|
||
logger.Debugf("NV index %s appears to be empty or non-existent: %v", targetIndex, err)
|
||
fmt.Printf("NV index %s appears to be empty or does not exist\n", targetIndex)
|
||
return nil
|
||
}
|
||
|
||
// Confirmation prompt with warning
|
||
if !skipConfirmation {
|
||
fmt.Printf("\n⚠️ WARNING: You are about to delete TPM NV index %s\n", targetIndex)
|
||
fmt.Printf("⚠️ If this index contains your disk encryption passphrase, your encrypted disk will become UNBOOTABLE!\n")
|
||
fmt.Printf("⚠️ This action CANNOT be undone.\n\n")
|
||
fmt.Printf("Are you sure you want to continue? (type 'yes' to confirm): ")
|
||
|
||
scanner := bufio.NewScanner(os.Stdin)
|
||
scanner.Scan()
|
||
response := strings.TrimSpace(strings.ToLower(scanner.Text()))
|
||
|
||
if response != "yes" {
|
||
fmt.Printf("Cleanup cancelled.\n")
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// Use native Go TPM library to undefine the NV space
|
||
logger.Debugf("Using native TPM library to undefine NV index")
|
||
fmt.Printf("Cleaning up TPM NV index %s...\n", targetIndex)
|
||
|
||
err = tpm.UndefineBlob(opts...)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to undefine NV index %s: %w", targetIndex, err)
|
||
}
|
||
|
||
fmt.Printf("Successfully cleaned up NV index %s\n", targetIndex)
|
||
logger.Debugf("Successfully undefined NV index %s", targetIndex)
|
||
return nil
|
||
}
|
||
|
||
// maskSensitiveString masks certificate paths/content for logging
|
||
func maskSensitiveString(s string) string {
|
||
if s == "" {
|
||
return "<empty>"
|
||
}
|
||
if len(s) <= 10 {
|
||
return strings.Repeat("*", len(s))
|
||
}
|
||
// Show first 3 and last 3 characters with * in between
|
||
return s[:3] + strings.Repeat("*", len(s)-6) + s[len(s)-3:]
|
||
}
|