Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework TPM workflow #318

Merged
merged 9 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
out, err := SH("udevadm trigger --settle -v --type=all")
Expand All @@ -67,76 +61,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 @@ -154,5 +217,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)
}