Files
kcrypt-challenger/cmd/discovery/client/client.go
Dimitris Karakasilis f943b01c90 Introduce a cli interface to interace with the challenger client
This will make debugging easier both while developing and in production.
No need to use it through the kcrypt binary anymore, because we might
not actually care about decrypting the disks but rather about getting
the passphrase from the KMS.

Signed-off-by: Dimitris Karakasilis <dimitris@karakasilis.me>
2025-09-18 13:47:10 +03:00

273 lines
8.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package client
import (
"encoding/base64"
"encoding/json"
"fmt"
"os"
"time"
"github.com/jaypipes/ghw/pkg/block"
"github.com/kairos-io/kairos-challenger/pkg/constants"
"github.com/kairos-io/kairos-challenger/pkg/payload"
"github.com/kairos-io/kairos-sdk/kcrypt/bus"
"github.com/kairos-io/tpm-helpers"
"github.com/mudler/go-pluggable"
"github.com/mudler/yip/pkg/utils"
)
// Because of how go-pluggable works, we can't just print to stdout
const LOGFILE = "/tmp/kcrypt-challenger-client.log"
var errPartNotFound error = fmt.Errorf("pass for partition not found")
var errBadCertificate error = fmt.Errorf("unknown certificate")
func NewClient() (*Client, error) {
conf, err := unmarshalConfig()
if err != nil {
return nil, err
}
return &Client{Config: conf}, nil
}
// echo '{ "data": "{ \\"label\\": \\"LABEL\\" }"}' | sudo -E WSS_SERVER="http://localhost:8082/challenge" ./challenger "discovery.password"
// GetPassphrase retrieves a passphrase for the given partition - core business logic
func (c *Client) GetPassphrase(partition *block.Partition, attempts int) (string, error) {
return c.waitPass(partition, attempts)
}
func (c *Client) Start() error {
if err := os.RemoveAll(LOGFILE); err != nil { // Start fresh
return fmt.Errorf("removing the logfile: %w", err)
}
factory := pluggable.NewPluginFactory()
// Input: bus.EventInstallPayload
// Expected output: map[string]string{}
factory.Add(bus.EventDiscoveryPassword, func(e *pluggable.Event) pluggable.EventResponse {
b := &block.Partition{}
err := json.Unmarshal([]byte(e.Data), b)
if err != nil {
return pluggable.EventResponse{
Error: fmt.Sprintf("failed reading partitions: %s", err.Error()),
}
}
// Use the extracted core logic
pass, err := c.GetPassphrase(b, 30)
if err != nil {
return pluggable.EventResponse{
Error: fmt.Sprintf("failed getting pass: %s", err.Error()),
}
}
return pluggable.EventResponse{
Data: pass,
}
})
return factory.Run(pluggable.EventType(os.Args[1]), os.Stdin, os.Stdout)
}
func (c *Client) generatePass(postEndpoint string, headers map[string]string, p *block.Partition) error {
rand := utils.RandomString(32)
pass, err := tpm.EncryptBlob([]byte(rand))
if err != nil {
return err
}
bpass := base64.RawURLEncoding.EncodeToString(pass)
opts := []tpm.Option{
tpm.WithCAs([]byte(c.Config.Kcrypt.Challenger.Certificate)),
tpm.AppendCustomCAToSystemCA,
tpm.WithAdditionalHeader("label", p.FilesystemLabel),
tpm.WithAdditionalHeader("name", p.Name),
tpm.WithAdditionalHeader("uuid", p.UUID),
}
for k, v := range headers {
opts = append(opts, tpm.WithAdditionalHeader(k, v))
}
conn, err := tpm.Connection(postEndpoint, opts...)
if err != nil {
return err
}
return conn.WriteJSON(payload.Data{Passphrase: bpass, GeneratedBy: constants.TPMSecret})
}
func (c *Client) waitPass(p *block.Partition, attempts int) (pass string, err error) {
serverURL := c.Config.Kcrypt.Challenger.Server
// If we don't have any server configured, just do local
if serverURL == "" {
return localPass(c.Config)
}
additionalHeaders := map[string]string{}
if c.Config.Kcrypt.Challenger.MDNS {
serverURL, additionalHeaders, err = queryMDNS(serverURL)
if err != nil {
return "", err
}
}
// Determine which flow to use based on TPM capabilities
if c.canUseTPMAttestation() {
logToFile("TPM attestation capabilities detected, using TPM flow\n")
return c.waitPassWithTPMAttestation(serverURL, additionalHeaders, p, attempts)
} else {
logToFile("No TPM attestation capabilities, using legacy flow\n")
return c.waitPassLegacy(serverURL, additionalHeaders, p, attempts)
}
}
// canUseTPMAttestation checks if TPM device exists and PCRs 0, 7, 11 are populated
func (c *Client) canUseTPMAttestation() bool {
// Check if TPM device is available by trying to get EK
_, err := tpm.GetPubHash()
if err != nil {
logToFile("TPM device not available: %v\n", err)
return false
}
// Check if the critical PCRs (0, 7, 11) have values (measured boot occurred)
pcrValues, err := c.readPCRValues([]int{0, 7, 11})
if err != nil {
logToFile("Failed to read PCR values: %v\n", err)
return false
}
// Check if any of the critical PCRs are populated (not all zeros)
allZero := true
for _, pcr := range pcrValues {
for _, b := range pcr {
if b != 0 {
allZero = false
break
}
}
if !allZero {
break
}
}
if allZero {
logToFile("PCRs 0, 7, 11 are all zero - measured boot did not occur\n")
return false
}
logToFile("TPM device available and PCRs populated\n")
return true
}
// readPCRValues reads the specified PCR values from the TPM using simple command execution
func (c *Client) readPCRValues(pcrIndices []int) ([][]byte, error) {
// For now, we'll use a simplified approach to check if TPM has meaningful PCR values
// This can be enhanced later with proper TPM library integration
// We'll just check if TPM is accessible and assume PCRs are valid if TPM responds
// Try to access TPM by getting EK pub hash - if this works, TPM is functional
_, err := tpm.GetPubHash()
if err != nil {
return nil, fmt.Errorf("TPM not accessible: %w", err)
}
// For the MVP, we'll assume if TPM is accessible, PCRs are likely populated
// Return dummy non-zero values to indicate PCRs are "populated"
// TODO: Implement proper PCR reading using go-attestation or tpm2-tools
result := make([][]byte, len(pcrIndices))
for i := range result {
// Create dummy PCR value (non-zero to indicate "populated")
result[i] = []byte{0x01, 0x02, 0x03} // Placeholder - indicates PCR has values
}
return result, nil
}
// waitPassWithTPMAttestation implements the new TPM remote attestation flow
func (c *Client) waitPassWithTPMAttestation(serverURL string, additionalHeaders map[string]string, p *block.Partition, attempts int) (string, error) {
// TODO: Implement TPM attestation flow
// 1. Initialize AKManager
// 2. Request challenge from server
// 3. Generate proof with TPM quote
// 4. Send proof and get passphrase
logToFile("TPM attestation flow - not yet implemented\n")
return "", fmt.Errorf("TPM attestation flow not implemented yet")
}
// waitPassLegacy implements the current/legacy flow without TPM attestation
func (c *Client) waitPassLegacy(serverURL string, additionalHeaders map[string]string, p *block.Partition, attempts int) (string, error) {
getEndpoint := fmt.Sprintf("%s/getPass", serverURL)
postEndpoint := fmt.Sprintf("%s/postPass", serverURL)
for tries := 0; tries < attempts; tries++ {
var generated bool
pass, generated, err := getPass(getEndpoint, additionalHeaders, c.Config.Kcrypt.Challenger.Certificate, p)
if err == errPartNotFound {
// IF server doesn't have a pass for us, then we generate one and we set it
err = c.generatePass(postEndpoint, additionalHeaders, p)
if err != nil {
return "", err
}
// Attempt to fetch again - validate that the server has it now
tries = 0
continue
}
if generated { // passphrase is encrypted
return c.decryptPassphrase(pass)
}
if err == errBadCertificate { // No need to retry, won't succeed.
return "", err
}
if err == nil { // passphrase available, no errors
return pass, nil
}
logToFile("Failed with error: %s . Will retry.\n", err.Error())
time.Sleep(1 * time.Second) // network errors? retry
}
return "", fmt.Errorf("exhausted all attempts (%d) to get passphrase", attempts)
}
// decryptPassphrase decodes (base64) and decrypts the passphrase returned
// by the challenger server.
func (c *Client) decryptPassphrase(pass string) (string, error) {
blob, err := base64.RawURLEncoding.DecodeString(pass)
if err != nil {
return "", err
}
// Decrypt and return it to unseal the LUKS volume
opts := []tpm.TPMOption{}
if c.Config.Kcrypt.Challenger.CIndex != "" {
opts = append(opts, tpm.WithIndex(c.Config.Kcrypt.Challenger.CIndex))
}
if c.Config.Kcrypt.Challenger.TPMDevice != "" {
opts = append(opts, tpm.WithDevice(c.Config.Kcrypt.Challenger.TPMDevice))
}
passBytes, err := tpm.DecryptBlob(blob, opts...)
return string(passBytes), err
}
func logToFile(format string, a ...any) {
s := fmt.Sprintf(format, a...)
file, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
defer file.Close()
file.WriteString(s)
}