Skip to content
Open
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
106 changes: 106 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,112 @@ nullboot is a boot manager for environments that do not need a boot manager.
Instead of running a boot manager at boot, it directly manages the UEFI boot
entries for you.

Use cases
---------

Usage:
1. nullbootctl
2. nullbootctl -no-tpm -output-json FILE
3. nullbootctl -no-boot-next
4. nullbootctl recovery-key [OPTIONS]

### nullbootctl

Finalize shim and UKI installation.

Without any argument, nullbootctl shall:
- Install shim and UKI (Unified Kernel Image) to the EFI System Partition (ESP,
ie: /boot/efi)
- Update the sealing policy of the LUKS passphrase in the TPM
- Set EFI variable BootNext to point to the newly installed shim and kernel

It is intended to be called upon shim and UKI update.


### nullbootctl -no-tpm -output-json FILE

- Export EFI variables Boot#### and BootOrder.
- Install shim & UKI
- Update the sealing policy of the LUKS passphrase in the TPM

This shall create a JSON file with the EFI variables Boot#### and BootOrder, as
expected on the first boot.


### nullbootctl -no-boot-next

Commit EFI variable BootOrder after shim and UKI update.

This shall:
- Install shim and UKI (possibly a no-operation)
- Update the sealing policy of the LUKS passphrase in the TPM (possibly a
no-operation)
- Commit shim and UKI by updating EFI variable BootOrder (when the system
boots on EFI variable BootNext)
- Remove old kernel


### nullbootctl recovery-key [OPTIONS]

Manage recovery keys (LUKS passphrases) of a LUKS container.

This shall setup, list or delete a recovery key for the LUKS container.

Options:
--create [--device DEVICE] [--name NAME]
--delete [--device DEVICE] [--name NAME]
--list [--device DEVICE]

Example:
```
# nullbootctl recovery-key --create
Creating recovery key 'recovery-0001' in '/dev/disk/by-label/cloudimg-rootfs-enc'
18466-30786-51485-64513-29543-52270-35959-33619

# nullbootctl recovery-key --list
Listing recovery keys in '/dev/disk/by-label/cloudimg-rootfs-enc'
recovery-0001

# nullbootctl recovery-key --delete
Deleting recovery key 'recovery-0001' in '/dev/disk/by-label/cloudimg-rootfs-enc'
Cannot delete recovery key: cannot kill last remaining slot
```

Build
-----
```
$ go build -o . ./...
```

Execute
-------
```
$ ./nullbootctl -h
2026/05/19 09:40:54 usage:

1. nullbootctl
2. nullbootctl -no-tpm -output-json FILE
3. nullbootctl -no-boot-next
4. nullbootctl recovery-key [OPTIONS]

Commands:

recovery-key
Manage FDE recovery keys (LUKS passphrases).
Options:
--create [--device DEVICE] [--name NAME]
--delete [--device DEVICE] [--name NAME]
--list [--device DEVICE]
```

Unit test
---------
```
$ go test -v -coverprofile=profile.cov ./...
$ echo $?
0
```

Licensing
---------
This program is free software: you can redistribute it and/or modify it under
Expand Down
105 changes: 97 additions & 8 deletions cmd/nullbootctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,108 @@

package main

import "github.com/canonical/nullboot/efibootmgr"
import "flag"
import "log"
import "os"
import (
"flag"
"github.com/canonical/nullboot/efibootmgr"
"github.com/canonical/nullboot/luks2"
"log"
"os"
)

var noTPM = flag.Bool("no-tpm", false, "Do not do any resealing with the TPM")
var noEfivars = flag.Bool("no-efivars", false, "Do not use or update the EFI variables. Disables kernel fallback mechanism")
var outputJSON = flag.String("output-json", "", "JSON file to write. Disables writing real EFI variables and enablement of the kernel fallback mechanism")
var noBootNext = flag.Bool("no-boot-next", false, "Disables use of BootNext. This flag must be disabled in order to upgrade to a new kernel version.")
const Usage = `usage:

1. nullbootctl
2. nullbootctl -no-tpm -output-json FILE
3. nullbootctl -no-boot-next
4. nullbootctl recovery-key [OPTIONS]

Commands:

recovery-key
Manage FDE recovery keys (LUKS passphrases).
Options:
--create [--device DEVICE] [--name NAME]
--delete [--device DEVICE] [--name NAME]
--list [--device DEVICE]
`

func usage() {
log.Print(Usage)
os.Exit(1)
}

func main() {

if len(os.Args) > 1 {
if os.Args[1] == "recovery-key" {
os.Args = os.Args[1:] // Strip the first item
cmd_recovery_key()
return
}
if os.Args[1] == "-h" || os.Args[1] == "--help" {
usage()
}
}

cmd_default()
}

const (
default_cloudimg_encrypted_device = "/dev/disk/by-label/" + efibootmgr.RootfsLabel
default_recovery_name = "recovery-0001"
)

func cmd_recovery_key() {

var devicePath string
var recoveryName string
doCreate := flag.Bool("create", false, "Create and set a recovery key")
doList := flag.Bool("list", false, "List recovery keys")
doDelete := flag.Bool("delete", false, "Delete a recovery key")
flag.StringVar(&devicePath, "device", default_cloudimg_encrypted_device, "Device of the encrypted volume")
flag.StringVar(&recoveryName, "name", default_recovery_name, "Name of the recovery key")

flag.Parse()

if *doCreate && *doList {
log.Println("Options --create and --list cannot be used together")
os.Exit(1)
}
if *doCreate && *doDelete {
log.Println("Options --create and --delete cannot be used together")
os.Exit(1)
}
if *doDelete && *doList {
log.Println("Options --delete and --list cannot be used together")
os.Exit(1)
}

var err error
if *doList {
err = luks2.ListRecoveryKeys(devicePath)
} else if *doDelete {
err = luks2.DeleteRecoveryKey(devicePath, recoveryName)
} else if *doCreate {
err = luks2.CreateRecoveryKey(devicePath, recoveryName)
} else {
log.Println("Please select at least one action: --create, --list, --delete")
os.Exit(1)
}

if err != nil {
os.Exit(1)
}
}

func cmd_default() {
var assets *efibootmgr.TrustedAssets
var err error

noTPM := flag.Bool("no-tpm", false, "Do not do any resealing with the TPM")
noEfivars := flag.Bool("no-efivars", false, "Do not use or update the EFI variables. Disables kernel fallback mechanism")
outputJSON := flag.String("output-json", "", "JSON file to write. Disables writing real EFI variables and enablement of the kernel fallback mechanism")
noBootNext := flag.Bool("no-boot-next", false, "Disables use of BootNext. This flag must be disabled in order to upgrade to a new kernel version.")

flag.Parse()

const (
Expand Down
22 changes: 11 additions & 11 deletions efibootmgr/efivars.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"strings"

//"errors"
"github.com/canonical/go-efilib"
efi "github.com/canonical/go-efilib"
efi_linux "github.com/canonical/go-efilib/linux"
)

Expand All @@ -20,30 +20,30 @@ type EFIVariables interface {
ListVariables() ([]efi.VariableDescriptor, error)
GetVariable(guid efi.GUID, name string) (data []byte, attrs efi.VariableAttributes, err error)
SetVariable(guid efi.GUID, name string, data []byte, attrs efi.VariableAttributes) error
NewFileDevicePath(filepath string, mode efi_linux.FileDevicePathMode) (efi.DevicePath, error)
NewFileDevicePath(filepath string, mode efi_linux.FilePathToDevicePathMode) (efi.DevicePath, error)
}

// RealEFIVariables provides the real implementation of efivars
type RealEFIVariables struct{}

// ListVariables proxy
func (RealEFIVariables) ListVariables() ([]efi.VariableDescriptor, error) {
return efi.ListVariables()
func (vars RealEFIVariables) ListVariables() ([]efi.VariableDescriptor, error) {
return efi.ListVariables(efi.DefaultVarContext)
}

// GetVariable proxy
func (RealEFIVariables) GetVariable(guid efi.GUID, name string) (data []byte, attrs efi.VariableAttributes, err error) {
return efi.ReadVariable(name, guid)
func (vars RealEFIVariables) GetVariable(guid efi.GUID, name string) (data []byte, attrs efi.VariableAttributes, err error) {
return efi.ReadVariable(efi.DefaultVarContext, name, guid)
}

// SetVariable proxy
func (RealEFIVariables) SetVariable(guid efi.GUID, name string, data []byte, attrs efi.VariableAttributes) error {
return efi.WriteVariable(name, guid, attrs, data)
func (vars RealEFIVariables) SetVariable(guid efi.GUID, name string, data []byte, attrs efi.VariableAttributes) error {
return efi.WriteVariable(efi.DefaultVarContext, name, guid, attrs, data)
}

// NewFileDevicePath proxy
func (RealEFIVariables) NewFileDevicePath(filepath string, mode efi_linux.FileDevicePathMode) (efi.DevicePath, error) {
return efi_linux.NewFileDevicePath(filepath, mode)
func (vars RealEFIVariables) NewFileDevicePath(filepath string, mode efi_linux.FilePathToDevicePathMode) (efi.DevicePath, error) {
return efi_linux.FilePathToDevicePath(filepath, mode)
}

type mockEFIVariable struct {
Expand Down Expand Up @@ -87,7 +87,7 @@ func (m *MockEFIVariables) SetVariable(guid efi.GUID, name string, data []byte,
}

// NewFileDevicePath implements EFIVariables
func (m MockEFIVariables) NewFileDevicePath(filepath string, mode efi_linux.FileDevicePathMode) (efi.DevicePath, error) {
func (m MockEFIVariables) NewFileDevicePath(filepath string, mode efi_linux.FilePathToDevicePathMode) (efi.DevicePath, error) {
file, err := appFs.Open(filepath)
if err != nil {
return nil, err
Expand Down
4 changes: 2 additions & 2 deletions efibootmgr/efivars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package efibootmgr
import (
"errors"

"github.com/canonical/go-efilib"
efi "github.com/canonical/go-efilib"
efi_linux "github.com/canonical/go-efilib/linux"
)

Expand All @@ -27,7 +27,7 @@ func (NoEFIVariables) SetVariable(guid efi.GUID, name string, data []byte, attrs
return efi.ErrVarsUnavailable
}

func (NoEFIVariables) NewFileDevicePath(filepath string, mode efi_linux.FileDevicePathMode) (efi.DevicePath, error) {
func (NoEFIVariables) NewFileDevicePath(filepath string, mode efi_linux.FilePathToDevicePathMode) (efi.DevicePath, error) {
return nil, errors.New("Cannot access")
}

Expand Down
Loading
Loading