2023-01-02 15:56:10 +02:00
|
|
|
|
package client
|
|
|
|
|
|
|
|
|
|
import (
|
2023-01-18 23:32:23 +01:00
|
|
|
|
"encoding/base64"
|
2023-01-02 15:56:10 +02:00
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/jaypipes/ghw/pkg/block"
|
2023-01-18 23:32:23 +01:00
|
|
|
|
"github.com/kairos-io/kairos-challenger/pkg/constants"
|
2023-01-24 12:03:08 +01:00
|
|
|
|
"github.com/kairos-io/kairos-challenger/pkg/payload"
|
2025-05-06 11:18:50 +02:00
|
|
|
|
"github.com/kairos-io/kairos-sdk/kcrypt/bus"
|
2025-09-18 14:29:48 +03:00
|
|
|
|
"github.com/kairos-io/kairos-sdk/types"
|
2023-01-18 16:02:17 +01:00
|
|
|
|
"github.com/kairos-io/tpm-helpers"
|
2023-01-02 15:56:10 +02:00
|
|
|
|
"github.com/mudler/go-pluggable"
|
2023-01-18 23:32:23 +01:00
|
|
|
|
"github.com/mudler/yip/pkg/utils"
|
2023-01-02 15:56:10 +02:00
|
|
|
|
)
|
|
|
|
|
|
2024-01-22 19:48:12 +02:00
|
|
|
|
// Because of how go-pluggable works, we can't just print to stdout
|
|
|
|
|
const LOGFILE = "/tmp/kcrypt-challenger-client.log"
|
|
|
|
|
|
2023-01-19 14:24:33 +01:00
|
|
|
|
var errPartNotFound error = fmt.Errorf("pass for partition not found")
|
2023-02-09 10:49:32 +02:00
|
|
|
|
var errBadCertificate error = fmt.Errorf("unknown certificate")
|
2023-01-02 15:56:10 +02:00
|
|
|
|
|
|
|
|
|
func NewClient() (*Client, error) {
|
2025-09-18 14:29:48 +03:00
|
|
|
|
return NewClientWithLogger(types.NewKairosLogger("kcrypt-challenger-client", "error", false))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewClientWithLogger(logger types.KairosLogger) (*Client, error) {
|
2023-01-18 23:32:23 +01:00
|
|
|
|
conf, err := unmarshalConfig()
|
2023-01-02 15:56:10 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 14:29:48 +03:00
|
|
|
|
return &Client{Config: conf, Logger: logger}, nil
|
2023-01-02 15:56:10 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ❯ echo '{ "data": "{ \\"label\\": \\"LABEL\\" }"}' | sudo -E WSS_SERVER="http://localhost:8082/challenge" ./challenger "discovery.password"
|
2025-09-18 13:47:10 +03:00
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-02 15:56:10 +02:00
|
|
|
|
func (c *Client) Start() error {
|
2024-01-22 19:48:12 +02:00
|
|
|
|
if err := os.RemoveAll(LOGFILE); err != nil { // Start fresh
|
|
|
|
|
return fmt.Errorf("removing the logfile: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-02 15:56:10 +02:00
|
|
|
|
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()),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 13:47:10 +03:00
|
|
|
|
// Use the extracted core logic
|
|
|
|
|
pass, err := c.GetPassphrase(b, 30)
|
2023-01-02 15:56:10 +02:00
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-22 19:48:12 +02:00
|
|
|
|
func (c *Client) generatePass(postEndpoint string, headers map[string]string, p *block.Partition) error {
|
2023-01-24 12:03:08 +01:00
|
|
|
|
|
|
|
|
|
rand := utils.RandomString(32)
|
|
|
|
|
pass, err := tpm.EncryptBlob([]byte(rand))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
bpass := base64.RawURLEncoding.EncodeToString(pass)
|
|
|
|
|
|
|
|
|
|
opts := []tpm.Option{
|
2023-02-09 09:52:51 +02:00
|
|
|
|
tpm.WithCAs([]byte(c.Config.Kcrypt.Challenger.Certificate)),
|
2023-02-09 11:24:10 +02:00
|
|
|
|
tpm.AppendCustomCAToSystemCA,
|
2023-05-10 00:24:58 +02:00
|
|
|
|
tpm.WithAdditionalHeader("label", p.FilesystemLabel),
|
2023-01-24 12:03:08 +01:00
|
|
|
|
tpm.WithAdditionalHeader("name", p.Name),
|
|
|
|
|
tpm.WithAdditionalHeader("uuid", p.UUID),
|
|
|
|
|
}
|
2024-01-22 19:48:12 +02:00
|
|
|
|
for k, v := range headers {
|
|
|
|
|
opts = append(opts, tpm.WithAdditionalHeader(k, v))
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-24 12:03:08 +01:00
|
|
|
|
conn, err := tpm.Connection(postEndpoint, opts...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return conn.WriteJSON(payload.Data{Passphrase: bpass, GeneratedBy: constants.TPMSecret})
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-24 17:40:00 +01:00
|
|
|
|
func (c *Client) waitPass(p *block.Partition, attempts int) (pass string, err error) {
|
2024-01-22 19:48:12 +02:00
|
|
|
|
serverURL := c.Config.Kcrypt.Challenger.Server
|
|
|
|
|
|
|
|
|
|
// If we don't have any server configured, just do local
|
|
|
|
|
if serverURL == "" {
|
2023-01-18 23:32:23 +01:00
|
|
|
|
return localPass(c.Config)
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-17 17:29:34 +03:00
|
|
|
|
additionalHeaders := map[string]string{}
|
2024-01-22 19:48:12 +02:00
|
|
|
|
if c.Config.Kcrypt.Challenger.MDNS {
|
2025-09-18 14:29:48 +03:00
|
|
|
|
serverURL, additionalHeaders, err = queryMDNS(serverURL, c.Logger)
|
2025-09-17 17:29:34 +03:00
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2024-01-22 19:48:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-09-17 17:29:34 +03:00
|
|
|
|
// Determine which flow to use based on TPM capabilities
|
|
|
|
|
if c.canUseTPMAttestation() {
|
2025-09-18 14:29:48 +03:00
|
|
|
|
c.Logger.Debugf("TPM attestation capabilities detected, using TPM flow")
|
2025-09-17 17:29:34 +03:00
|
|
|
|
return c.waitPassWithTPMAttestation(serverURL, additionalHeaders, p, attempts)
|
|
|
|
|
} else {
|
2025-09-18 14:29:48 +03:00
|
|
|
|
c.Logger.Debugf("No TPM attestation capabilities, using legacy flow")
|
2025-09-17 17:29:34 +03:00
|
|
|
|
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 {
|
2025-09-18 14:29:48 +03:00
|
|
|
|
c.Logger.Debugf("TPM device not available: %v", err)
|
2025-09-17 17:29:34 +03:00
|
|
|
|
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 {
|
2025-09-18 14:29:48 +03:00
|
|
|
|
c.Logger.Debugf("Failed to read PCR values: %v", err)
|
2025-09-17 17:29:34 +03:00
|
|
|
|
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 {
|
2025-09-18 14:29:48 +03:00
|
|
|
|
c.Logger.Debugf("PCRs 0, 7, 11 are all zero - measured boot did not occur")
|
2025-09-17 17:29:34 +03:00
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 14:29:48 +03:00
|
|
|
|
c.Logger.Debugf("TPM device available and PCRs populated")
|
2025-09-17 17:29:34 +03:00
|
|
|
|
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
|
|
|
|
|
|
2025-09-18 14:29:48 +03:00
|
|
|
|
c.Logger.Debugf("TPM attestation flow - not yet implemented")
|
2025-09-17 17:29:34 +03:00
|
|
|
|
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) {
|
2024-01-22 19:48:12 +02:00
|
|
|
|
getEndpoint := fmt.Sprintf("%s/getPass", serverURL)
|
|
|
|
|
postEndpoint := fmt.Sprintf("%s/postPass", serverURL)
|
2023-01-18 23:32:23 +01:00
|
|
|
|
|
2023-01-02 15:56:10 +02:00
|
|
|
|
for tries := 0; tries < attempts; tries++ {
|
2023-01-18 23:32:23 +01:00
|
|
|
|
var generated bool
|
2025-09-17 17:29:34 +03:00
|
|
|
|
pass, generated, err := getPass(getEndpoint, additionalHeaders, c.Config.Kcrypt.Challenger.Certificate, p)
|
2023-01-24 12:03:08 +01:00
|
|
|
|
if err == errPartNotFound {
|
|
|
|
|
// IF server doesn't have a pass for us, then we generate one and we set it
|
2024-01-22 19:48:12 +02:00
|
|
|
|
err = c.generatePass(postEndpoint, additionalHeaders, p)
|
2023-01-24 12:03:08 +01:00
|
|
|
|
if err != nil {
|
2025-09-17 17:29:34 +03:00
|
|
|
|
return "", err
|
2023-01-24 12:03:08 +01:00
|
|
|
|
}
|
|
|
|
|
// Attempt to fetch again - validate that the server has it now
|
|
|
|
|
tries = 0
|
|
|
|
|
continue
|
|
|
|
|
}
|
2023-02-09 10:49:32 +02:00
|
|
|
|
|
2023-01-19 15:46:35 +02:00
|
|
|
|
if generated { // passphrase is encrypted
|
|
|
|
|
return c.decryptPassphrase(pass)
|
2023-01-02 15:56:10 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-02-09 10:49:32 +02:00
|
|
|
|
if err == errBadCertificate { // No need to retry, won't succeed.
|
2025-09-17 17:29:34 +03:00
|
|
|
|
return "", err
|
2023-02-09 10:49:32 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-01-24 17:53:38 +01:00
|
|
|
|
if err == nil { // passphrase available, no errors
|
2025-09-17 17:29:34 +03:00
|
|
|
|
return pass, nil
|
2023-01-02 15:56:10 +02:00
|
|
|
|
}
|
2023-01-19 15:46:35 +02:00
|
|
|
|
|
2025-09-18 14:29:48 +03:00
|
|
|
|
c.Logger.Debugf("Failed with error: %s . Will retry.", err.Error())
|
2023-01-19 15:46:35 +02:00
|
|
|
|
time.Sleep(1 * time.Second) // network errors? retry
|
2023-01-02 15:56:10 +02:00
|
|
|
|
}
|
2023-01-19 15:46:35 +02:00
|
|
|
|
|
2025-09-17 17:29:34 +03:00
|
|
|
|
return "", fmt.Errorf("exhausted all attempts (%d) to get passphrase", attempts)
|
2023-01-02 15:56:10 +02:00
|
|
|
|
}
|
2023-01-19 15:46:35 +02:00
|
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
|
}
|
2023-01-19 16:06:53 +02:00
|
|
|
|
passBytes, err := tpm.DecryptBlob(blob, opts...)
|
2023-01-19 15:46:35 +02:00
|
|
|
|
|
|
|
|
|
return string(passBytes), err
|
|
|
|
|
}
|