From 418027453d8aec9a877bd9818ed34e9394a54c8b Mon Sep 17 00:00:00 2001 From: Itxaka Date: Tue, 28 May 2024 09:48:37 +0000 Subject: [PATCH] Rework TPM workflow (#318) Co-authored-by: Dimitris Karakasilis --- main.go | 31 +++++--- pkg/lib/lock.go | 185 +++++++++++++++++++++++++++++++---------------- pkg/lib/utils.go | 2 +- 3 files changed, 147 insertions(+), 71 deletions(-) diff --git a/main.go b/main.go index f2bdec1..fbf83af 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/rs/zerolog" "os" "github.com/kairos-io/kcrypt/pkg/lib" @@ -25,27 +26,39 @@ func main() { Name: "encrypt", Description: "Encrypts a partition", Usage: "Encrypts a partition", - ArgsUsage: "kcrypt [--version VERSION] [--tpm] LABEL", + ArgsUsage: "kcrypt [--tpm] [--tpm-pcrs] [--public-key-pcrs] LABEL", Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "version", - Value: "luks1", - Usage: "luks version to use", - }, &cli.BoolFlag{ Name: "tpm", - Usage: "Use TPM to lock the partition", + Usage: "Use TPM measurements to lock the partition", + }, + &cli.StringSliceFlag{ + Name: "tpm-pcrs", + Usage: "tpm pcrs to bind to (single measurement) . Only applies when --tpm is also set.", + }, + &cli.StringSliceFlag{ + Name: "public-key-pcrs", + Usage: "public key pcrs to bind to (policy). Only applies when --tpm is also set.", + Value: &cli.StringSlice{"11"}, }, }, Action: func(c *cli.Context) error { + var err error + var out string if c.NArg() != 1 { return fmt.Errorf("requires 1 arg, the partition label") } - out, err := lib.Luksify(c.Args().First(), c.String("version"), c.Bool("tpm")) + log := zerolog.New(os.Stdout).With().Timestamp().Logger() + if c.Bool("tpm") { + err = lib.LuksifyMeasurements(c.Args().First(), c.StringSlice("tpm-pcrs"), c.StringSlice("public-key-pcrs"), log) + } else { + out, err = lib.Luksify(c.Args().First(), log) + fmt.Println(out) + } if err != nil { return err } - fmt.Println(out) + return nil }, }, diff --git a/pkg/lib/lock.go b/pkg/lib/lock.go index 3c99f39..8f366db 100644 --- a/pkg/lib/lock.go +++ b/pkg/lib/lock.go @@ -1,24 +1,24 @@ package lib import ( + "bytes" "fmt" "math/rand" "os" "os/exec" "strings" + "syscall" "time" "github.com/gofrs/uuid" "github.com/jaypipes/ghw" "github.com/jaypipes/ghw/pkg/block" configpkg "github.com/kairos-io/kcrypt/pkg/config" + "github.com/rs/zerolog" ) -func CreateLuks(dev, password, version string, cryptsetupArgs ...string) error { - if version == "" { - version = "luks2" - } - args := []string{"luksFormat", "--type", version, "--iter-time", "5", "-q", dev} +func CreateLuks(dev, password string, cryptsetupArgs ...string) error { + args := []string{"luksFormat", "--type", "luks2", "--iter-time", "5", "-q", dev} args = append(args, cryptsetupArgs...) cmd := exec.Command("cryptsetup", args...) cmd.Stdin = strings.NewReader(password) @@ -49,14 +49,8 @@ func getRandomString(length int) string { // This is because the label of the encrypted partition is not accessible unless // the partition is decrypted first and the uuid changed after encryption so // any stored information needs to be updated (by the caller). -func Luksify(label, version string, tpm bool) (string, error) { +func Luksify(label string, logger zerolog.Logger) (string, error) { var pass string - if version == "" { - version = "luks1" - } - if version != "luks1" && version != "luks2" { - return "", fmt.Errorf("version must be luks1 or luks2") - } // Make sure ghw will see all partitions correctly. // Some versions of udevadm don't support --settle (e.g. alpine) @@ -69,76 +63,145 @@ func Luksify(label, version string, tpm bool) (string, error) { part, b, err := FindPartition(label) if err != nil { + logger.Err(err).Msg("find partition") return "", err } - if tpm { - // On TPM locking we generate a random password that will only be used here then discarded. - // only unlocking method will be PCR values - pass = getRandomString(32) - } else { - pass, err = GetPassword(b) - if err != nil { - return "", err - } + pass, err = GetPassword(b) + if err != nil { + logger.Err(err).Msg("get password") + return "", err } - part = fmt.Sprintf("/dev/%s", part) - devMapper := fmt.Sprintf("/dev/mapper/%s", b.Name) + mapper := fmt.Sprintf("/dev/mapper/%s", b.Name) + device := fmt.Sprintf("/dev/%s", part) + partUUID := uuid.NewV5(uuid.NamespaceURL, label) + extraArgs := []string{"--uuid", partUUID.String()} + + if err := CreateLuks(device, pass, extraArgs...); err != nil { + logger.Err(err).Msg("create luks") + return "", err + } + + err = formatLuks(device, b.Name, mapper, label, pass, logger) + if err != nil { + logger.Err(err).Msg("format luks") + return "", err + } + + return configpkg.PartitionToString(b), nil +} + +// LuksifyMeasurements takes a label and a list if public-keys and pcrs to bind and uses the measurements +// in the current node to encrypt the partition with those and bind those to the given pcrs +// this expects systemd 255 as it needs the SRK public key that systemd extracts +// Sets a random password, enrolls the policy, unlocks and formats the partition, closes it and tfinally removes the random password from it +// Note that there is a diff between the publicKeyPcrs and normal Pcrs +// The former links to a policy type that allows anything signed by that policy to unlcok the partitions so its +// really useful for binding to PCR11 which is the UKI measurements in order to be able to upgrade the system and still be able +// to unlock the partitions. +// The later binds to a SINGLE measurement, so if that changes, it will not unlock anything. +// This is useful for things like PCR7 which measures the secureboot state and certificates if you dont expect those to change during +// the whole lifetime of a machine +// It can also be used to bind to things like the firmware code or efi drivers that we dont expect to change +// default for publicKeyPcrs is 11 +// default for pcrs is nothing, so it doesn't bind as we want to expand things like DBX and be able to blacklist certs and such +func LuksifyMeasurements(label string, publicKeyPcrs []string, pcrs []string, logger zerolog.Logger) error { + part, b, err := FindPartition(label) + if err != nil { + return err + } + + // On TPM locking we generate a random password that will only be used here then discarded. + // only unlocking method will be PCR values + pass := getRandomString(32) + mapper := fmt.Sprintf("/dev/mapper/%s", b.Name) + device := fmt.Sprintf("/dev/%s", part) partUUID := uuid.NewV5(uuid.NamespaceURL, label) extraArgs := []string{"--uuid", partUUID.String()} - if err := CreateLuks(part, pass, version, extraArgs...); err != nil { - return "", err - } - if tpm { - // Enroll PCR policy as a keyslot - // We pass the current signature of the booted system to confirm that we would be able to unlock with the current booted system - // That checks the policy against the signatures and fails if a UKI with those signatures wont be able to unlock the device - // Files are generated by systemd automatically and are extracted from the UKI binary directly - // public pem cert -> .pcrpkey section fo the elf file - // signatures -> .pcrsig section of the elf file - // leave --tpm2-pcrs= to an empty value so it doesnt bind to a single measure - args := []string{"--tpm2-public-key=/run/systemd/tpm2-pcr-public-key.pem", "--tpm2-public-key-pcrs=11", "--tpm2-pcrs=", "--tpm2-signature=/run/systemd/tpm2-pcr-signature.json", "--tpm2-device-key=/run/systemd/tpm2-srk-public-key.tpm2b_public", part} - cmd := exec.Command("systemd-cryptenroll", args...) - cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", pass)) // cannot pass it via stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return "", err - } + if err := CreateLuks(device, pass, extraArgs...); err != nil { + return err } - if err := LuksUnlock(part, b.Name, pass); err != nil { - return "", fmt.Errorf("unlock err: %w", err) + if len(publicKeyPcrs) == 0 { + publicKeyPcrs = []string{"11"} } - if err := Waitdevice(devMapper, 10); err != nil { - return "", fmt.Errorf("waitdevice err: %w", err) - } + syscall.Sync() - cmd := fmt.Sprintf("mkfs.ext4 -L %s %s", label, devMapper) - out, err = SH(cmd) + // Enroll PCR policy as a keyslot + // We pass the current signature of the booted system to confirm that we would be able to unlock with the current booted system + // That checks the policy against the signatures and fails if a UKI with those signatures wont be able to unlock the device + // Files are generated by systemd automatically and are extracted from the UKI binary directly + // public pem cert -> .pcrpkey section fo the elf file + // signatures -> .pcrsig section of the elf file + args := []string{ + "--tpm2-public-key=/run/systemd/tpm2-pcr-public-key.pem", + fmt.Sprintf("--tpm2-public-key-pcrs=%s", strings.Join(publicKeyPcrs, "+")), + fmt.Sprintf("--tpm2-pcrs=%s", strings.Join(pcrs, "+")), + "--tpm2-signature=/run/systemd/tpm2-pcr-signature.json", + "--tpm2-device-key=/run/systemd/tpm2-srk-public-key.tpm2b_public", + part} + logger.Debug().Str("args", strings.Join(args, " ")).Msg("running command") + cmd := exec.Command("systemd-cryptenroll", args...) + cmd.Env = append(cmd.Env, fmt.Sprintf("PASSWORD=%s", pass), "SYSTEMD_LOG_LEVEL=debug") // cannot pass it via stdin + // Store the output into a buffer to log it in case we need it + // debug output goes to stderr for some reason? + stdOut := bytes.Buffer{} + cmd.Stdout = &stdOut + cmd.Stderr = &stdOut + err = cmd.Run() if err != nil { - return "", fmt.Errorf("mkfs err: %w, out: %s", err, out) + logger.Debug().Str("output", stdOut.String()).Msg("debug from cryptenroll") + logger.Err(err).Msg("Enrolling measurements") + return err } - out, err = SH(fmt.Sprintf("cryptsetup close %s", b.Name)) + err = formatLuks(device, b.Name, mapper, label, pass, logger) if err != nil { - return "", fmt.Errorf("lock err: %w, out: %s", err, out) + logger.Err(err).Msg("format luks") + return err } - if tpm { - // Delete password slot from luks device - out, err := SH(fmt.Sprintf("systemd-cryptenroll --wipe-slot=password %s", part)) - if err != nil { - return "", fmt.Errorf("err: %w, out: %s", err, out) - } + // Delete password slot from luks device + out, err := SH(fmt.Sprintf("systemd-cryptenroll --wipe-slot=password %s", device)) + if err != nil { + logger.Err(err).Str("out", out).Msg("Removing password") + return err + } + return nil +} + +// format luks will unlock the device, wait for it and then format it +// device is the actual /dev/X luks device +// label is the label we will set to the formatted partition +// password is the pass to unlock the device to be able to format the underlying mapper +func formatLuks(device, name, mapper, label, pass string, logger zerolog.Logger) error { + l := logger.With().Str("device", device).Str("name", name).Str("mapper", mapper).Logger() + l.Debug().Msg("unlock") + if err := LuksUnlock(device, name, pass); err != nil { + return fmt.Errorf("unlock err: %w", err) } - return configpkg.PartitionToString(b), nil + l.Debug().Msg("wait device") + if err := Waitdevice(mapper, 10); err != nil { + return fmt.Errorf("waitdevice err: %w", err) + } + + l.Debug().Msg("format") + cmdFormat := fmt.Sprintf("mkfs.ext4 -L %s %s", label, mapper) + out, err := SH(cmdFormat) + if err != nil { + return fmt.Errorf("mkfs err: %w, out: %s", err, out) + } + l.Debug().Msg("close") + out, err = SH(fmt.Sprintf("cryptsetup close %s", mapper)) + if err != nil { + return fmt.Errorf("lock err: %w, out: %s", err, out) + } + return nil } func FindPartition(label string) (string, *block.Partition, error) { @@ -156,5 +219,5 @@ func FindPartition(label string) (string, *block.Partition, error) { return "", nil, err } - return "", nil, fmt.Errorf("not found") + return "", nil, fmt.Errorf("not found label %s", label) } diff --git a/pkg/lib/utils.go b/pkg/lib/utils.go index df00187..317c4d5 100644 --- a/pkg/lib/utils.go +++ b/pkg/lib/utils.go @@ -24,5 +24,5 @@ func Waitdevice(device string, attempts int) error { } time.Sleep(1 * time.Second) } - return fmt.Errorf("no device found") + return fmt.Errorf("no device found %s", device) }