UC20 FDE boot flow

Hi,

Got below query while analyzing UC20 boot flow with FDE feature enabled.

If fde-reveal-key unable to unseal the key used for disk encryption, UC20 prompts user to enter the recovery key manually. But Recovery key is also a random key - generated by snapd snap. From anywhere user can get plain recovery key - which user can input to proceed the boot?

To get better understanding of boot flow, I tried to modify snapd disk unlock flow (fde-reveal-key flow) - Updated secboot_sb.go and secboot_hooks.go for this purpose. But changes are not reflecting. Whether UC20 image/base snap includes default snapd which runs before re-mounting of snapd snap in Run mode?

Hi, Can you re-phrase your question? It sounds like you are trying to change behavior in snapd, but it’s not clear what you are changing and what the overall purpose is of those changes.

Thanks,

Ian

Hi,

For our custom requirement, I am customizing FDE hook. Instead of OPTEE based encryption, planning to use custom encryption for sealing the key used for disk encryption. fde-setup hook will seal (encrypt) the key and fde-reveal-key hook will unseal (decrypt) the key. If any error during decryption, fde-reveal-key hook will fail to return unsealed key to snapd.

In this case, UC20 boot flow will halt with below message for input from user.

Please enter the recovery key for disk /dev/disk/by-partuuid/6310f368-f16b-494b-8584-15a0e2471a5d: (press TAB for no echo)

How to make use of this feature to recover and continue the boot? Do we have any method to get plain recovery key? (As my my understanding, recovery key will be generated by snapd and saved as encrypted payload with the help of FDE hook).

For understanding recovery and disk unlock flow, I added debug prints in snapd and generated custom snapd snap . But none of these debug prints are reflecting during UC20 boot. So I got doubt like UC20 image includes default snapd which runs before re-mounting of snapd snap in Run mode. Please correct me if my understanding is wrong.

If you want to see the recovery keys that are generated when the system is installed, you can do so with:

snap recovery --show-keys

while the system is running after the disk has been unlocked. This can also be accessed via the snapd REST API.

Thanks for suggesting the option to get recovery key. Why UC20 boot flow not trying to fetch recover key (ubuntu-data.recovery.sealed-key) with the help of FDE hook? instead it prompts user to input it.

Any comments on this query? Whether UC20 image includes default snapd which runs before re-mounting of snapd snap?

There are two possibilities, one if you are in recover mode, then the fallback key should be attempted to be used, and if that still fails, then we proceed to as a last attempt to unlock the encrypted partitions ask for the passphrase.

If you are not in recover mode, we do not attempt to use the recovery key, and instead immediately fallback to asking for the passphrase. I’m not sure if we have considered using the recovery key if the primary key fails in run mode, but I think the main reason why is that we don’t expect to not be able to use the primary key in run mode. Do you know why your primary key is not usable to unlock?

Well there are two places where “snapd code” runs, there is the snapd snap included in the image which runs in userspace, and there is the snap-bootstrap executable, built from snapd codebase, but included in the kernel’s initramfs image. If your boot has not progressed to userspace, then the snapd code you are seeing execute is snap-bootstrap from the initramfs. If you want to add debug prints etc to snap-bootstrap, it’s not sufficient to just rebuild the snapd snap, you also need to extract snap-bootstrap from that snapd snap, and then inject the snap-bootstrap binary into the kernel snap’s initramfs.

@ijohnson,

Thanks for your detailed reply.

Problem was in customized FDE hook.

Hi,

Trying to understand real use case of reinstall key.
reinstall-key can be retrieved along with recovery key using sudo snap recovery --show-keys.

reinstall-key is a recovery key of ubuntu-save partition. This key can be used for unlocking ubuntu-save partition when encryption key fails.

I did not find any document or snapd code flow for other use case of reinstall-key.

Whether reinstall-key has any other role in recovery/re-install mode?

The key can also be used if unlocking the data partition fails in run mode as well

@ijohnson,

Thanks for your reply.

I understand that reinstall key will used in run mode to unlock ubuntu-save partition if unlocking of data partition or save partition fails.

But why key name is termed as reinstall key (not recovery key)?

I’m not sure why it’s called “reinstall key” in our API actually but it has been that way since UC20 was released

Got a quick question, can I use the key to access the hard disk by re-mounting it to the other machine?

I had to do exactly this to debug a device that failed to boot but I still had the recovery key as a result of previously running snap recovery --show-keys.

The code below will unlock and mount encrypted Ubuntu Core partitions. ParseRecoveryKeys came from here - https://github.com/snapcore/secboot/blob/master/crypt.go which is responsible for deriving the LUKS encryption key that gets used by cryptsetup to unlock partitions.

package main

import (
	"bufio"
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strconv"

	"golang.org/x/xerrors"
)

// Parse[16]byte interprets the supplied string and returns the corresponding [16]byte. The recovery key is a
// 16-byte number, and the formatted version of this is represented as 8 5-digit zero-extended base-10 numbers (each
// with a range of 00000-65535) which may be separated by an optional '-', eg:
//
// "61665-00531-54469-09783-47273-19035-40077-28287"
//
// The formatted version of the recovery key is designed to be able to be inputted on a numeric keypad.
func ParseRecoveryKey(s string) (out [16]byte, err error) {
	for i := 0; i < 8; i++ {
		if len(s) < 5 {
			return [16]byte{}, errors.New("incorrectly formatted: insufficient characters")
		}
		x, err := strconv.ParseUint(s[0:5], 10, 16) // Base 10 16 bit int
		if err != nil {
			return [16]byte{}, xerrors.Errorf("incorrectly formatted: %w", err)
		}
		binary.LittleEndian.PutUint16(out[i*2:], uint16(x))

		// Move to the next 5 digits
		s = s[5:]
		// Permit each set of 5 digits to be separated by an optional '-', but don't allow the formatted key to end or begin with one.
		if len(s) > 1 && s[0] == '-' {
			s = s[1:]
		}
	}

	if len(s) > 0 {
		return [16]byte{}, errors.New("incorrectly formatted: too many characters")
	}

	return
}

func mount(key []byte, path string, name string) (string, error) {
	cmd := exec.Command("sudo", "cryptsetup", "luksOpen", path, name)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, "SYSTEMD_LOG_TARGET=console")
	cmd.Stdin = bytes.NewReader(key)

	output, err := cmd.CombinedOutput()
	return string(output), err
}

func unmount(key []byte, path string, name string) (string, error) {
	exec.Command("sudo", "unmount", path)
	cmd := exec.Command("sudo", "cryptsetup", "luksClose", name)
	cmd.Env = os.Environ()
	cmd.Env = append(cmd.Env, "SYSTEMD_LOG_TARGET=console")
	cmd.Stdin = bytes.NewReader(key)

	output, err := cmd.CombinedOutput()
	return string(output), err
}

func main() {
	var devicePath string
	var deviceName string
	var recoveryKey string

	fmt.Print("Enter device path (i.e. /dev/sdc4): ")
	fmt.Scanln(&devicePath)

	fmt.Print("Enter device name (choose a unique name to map this volume): ")
	fmt.Scanln(&deviceName)

	fmt.Print("Enter recovery key: ")
	fmt.Scanln(&recoveryKey)

	key, err := ParseRecoveryKey(recoveryKey)
	if err != nil {
		fmt.Println(err)
		return
	}

	errorMessage, mountError := mount(key[:], devicePath, deviceName)
	if mountError != nil { // Failed to mount the volume
		fmt.Println(errorMessage)
		return
	}

	fmt.Print("Press 'Enter' to unmount the volume...")
	bufio.NewReader(os.Stdin).ReadBytes('\n')

	errorMessage, unMountError := unmount(key[:], devicePath, deviceName)
	if unMountError != nil { // Failed to unmount the volume
		fmt.Println(errorMessage)
		return
	}
}