Skip to content

Commit

Permalink
Rework TPM workflow (#318)
Browse files Browse the repository at this point in the history
Co-authored-by: Dimitris Karakasilis <[email protected]>
  • Loading branch information
Itxaka and jimmykarily authored May 28, 2024
1 parent efa923b commit 4180274
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 69 deletions.
31 changes: 22 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"github.com/rs/zerolog"
"os"

"github.com/kairos-io/kcrypt/pkg/lib"
Expand All @@ -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
},
},
Expand Down
181 changes: 122 additions & 59 deletions pkg/lib/lock.go
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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(part, pass, version, extraArgs...); err != nil {
if err := CreateLuks(device, pass, extraArgs...); err != nil {
logger.Err(err).Msg("create luks")
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
}

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
}

if err := LuksUnlock(part, b.Name, pass); err != nil {
return "", fmt.Errorf("unlock err: %w", 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(device, pass, extraArgs...); err != nil {
return err
}

if err := Waitdevice(devMapper, 10); err != nil {
return "", fmt.Errorf("waitdevice err: %w", err)
if len(publicKeyPcrs) == 0 {
publicKeyPcrs = []string{"11"}
}

syscall.Sync()

// 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 {
logger.Debug().Str("output", stdOut.String()).Msg("debug from cryptenroll")
logger.Err(err).Msg("Enrolling measurements")
return err
}

cmd := fmt.Sprintf("mkfs.ext4 -L %s %s", label, devMapper)
out, err = SH(cmd)
err = formatLuks(device, b.Name, mapper, label, pass, logger)
if err != nil {
return "", fmt.Errorf("mkfs err: %w, out: %s", err, out)
logger.Err(err).Msg("format luks")
return err
}

out, err = SH(fmt.Sprintf("cryptsetup close %s", b.Name))
// Delete password slot from luks device
out, err := SH(fmt.Sprintf("systemd-cryptenroll --wipe-slot=password %s", device))
if err != nil {
return "", fmt.Errorf("lock err: %w, out: %s", err, out)
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)
}

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)
}
l.Debug().Msg("wait device")
if err := Waitdevice(mapper, 10); err != nil {
return fmt.Errorf("waitdevice err: %w", err)
}

return configpkg.PartitionToString(b), nil
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) {
Expand All @@ -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)
}
2 changes: 1 addition & 1 deletion pkg/lib/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit 4180274

Please sign in to comment.