mirror of
https://github.com/kairos-io/kcrypt-challenger.git
synced 2025-09-26 21:13:23 +00:00
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>
273 lines
8.2 KiB
Go
273 lines
8.2 KiB
Go
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)
|
||
}
|