Merge branch 'morten/cli'

This commit is contained in:
Morten Linderud 2021-06-05 14:08:11 +02:00
commit 5d528ff82a
No known key found for this signature in database
GPG Key ID: E742683BA08CB2FF
25 changed files with 1212 additions and 742 deletions

View File

@ -4,10 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"github.com/foxboron/sbctl/logging"
)
type Bundle struct {
@ -37,15 +38,29 @@ func ReadBundleDatabase(dbpath string) (Bundles, error) {
return bundles, nil
}
func WriteBundleDatabase(dbpath string, bundles Bundles) {
func WriteBundleDatabase(dbpath string, bundles Bundles) error {
data, err := json.MarshalIndent(bundles, "", " ")
if err != nil {
log.Fatal(err)
return err
}
err = os.WriteFile(dbpath, data, 0644)
if err != nil {
log.Fatal(err)
return err
}
return nil
}
func BundleIter(fn func(s *Bundle) error) error {
files, err := ReadBundleDatabase(BundleDBPath)
if err != nil {
return err
}
for _, s := range files {
if err := fn(s); err != nil {
return err
}
}
return nil
}
func GetEfistub() string {
@ -83,7 +98,7 @@ func NewBundle() *Bundle {
}
}
func GenerateBundle(bundle *Bundle) bool {
func GenerateBundle(bundle *Bundle) (bool, error) {
args := []string{
"--add-section", fmt.Sprintf(".osrel=%s", bundle.OSRelease), "--change-section-vma", ".osrel=0x20000",
"--add-section", fmt.Sprintf(".cmdline=%s", bundle.Cmdline), "--change-section-vma", ".cmdline=0x30000",
@ -101,33 +116,12 @@ func GenerateBundle(bundle *Bundle) bool {
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
if errors.Is(err, exec.ErrNotFound) {
err2.Printf(err.Error())
return false
return false, err
}
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.ExitCode() == 0
return exitError.ExitCode() == 0, nil
}
}
msg.Printf("Wrote EFI bundle %s", bundle.Output)
return true
}
func FormatBundle(name string, bundle *Bundle) {
msg.Printf("Bundle: %s", name)
if bundle.AMDMicrocode != "" {
msg2.Printf("AMD Microcode: %s", bundle.AMDMicrocode)
}
if bundle.IntelMicrocode != "" {
msg2.Printf("Intel Microcode: %s", bundle.IntelMicrocode)
}
msg2.Printf("Kernel Image: %s", bundle.KernelImage)
msg2.Printf("Initramfs Image: %s", bundle.Initramfs)
msg2.Printf("Cmdline: %s", bundle.Cmdline)
msg2.Printf("OS Release: %s", bundle.OSRelease)
msg2.Printf("EFI Stub Image: %s", bundle.EFIStub)
msg2.Printf("ESP Location: %s", bundle.ESP)
if bundle.Splash != "" {
msg2.Printf("Splash Image: %s", bundle.Splash)
}
msg2.Printf("Output: %s", bundle.Output)
logging.Print("Wrote EFI bundle %s\n", bundle.Output)
return true, nil
}

103
cmd/sbctl/bundle.go Normal file
View File

@ -0,0 +1,103 @@
package main
import (
"os"
"path/filepath"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var (
amducode string
intelucode string
splashImg string
osRelease string
efiStub string
kernelImg string
cmdline string
initramfs string
espPath string
saveBundle bool
)
var bundleCmd = &cobra.Command{
Use: "bundle",
Short: "Bundle the needed files for an EFI stub image",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
logging.Print("Requires a file to sign...\n")
os.Exit(1)
}
checkFiles := []string{amducode, intelucode, splashImg, osRelease, efiStub, kernelImg, cmdline, initramfs}
for _, path := range checkFiles {
if path == "" {
continue
}
if _, err := os.Stat(path); os.IsNotExist(err) {
logging.Print("%s does not exist!\n", path)
os.Exit(1)
}
}
bundle := sbctl.NewBundle()
output, err := filepath.Abs(args[0])
if err != nil {
return err
}
// Fail early if user wants to save bundle but doesn't have permissions
var bundles sbctl.Bundles
if saveBundle {
// "err" needs to have been declared before this, otherwise it's necessary
// to use ":=", which shadows the "bundles" variable
bundles, err = sbctl.ReadBundleDatabase(sbctl.BundleDBPath)
if err != nil {
return err
}
}
bundle.Output = output
bundle.IntelMicrocode = intelucode
bundle.AMDMicrocode = amducode
bundle.KernelImage = kernelImg
bundle.Initramfs = initramfs
bundle.Cmdline = cmdline
bundle.Splash = splashImg
bundle.OSRelease = osRelease
bundle.EFIStub = efiStub
bundle.ESP = espPath
if err = sbctl.CreateBundle(*bundle); err != nil {
return err
}
logging.Print("Wrote EFI bundle %s\n", bundle.Output)
if saveBundle {
bundles[bundle.Output] = bundle
err := sbctl.WriteBundleDatabase(sbctl.BundleDBPath, bundles)
if err != nil {
return err
}
}
return nil
},
}
func bundleCmdFlags(cmd *cobra.Command) {
esp := sbctl.GetESP()
f := cmd.Flags()
f.StringVarP(&amducode, "amducode", "a", "", "AMD microcode location")
f.StringVarP(&intelucode, "intelucode", "i", "", "Intel microcode location")
f.StringVarP(&splashImg, "splash-img", "l", "", "Boot splash image location")
f.StringVarP(&osRelease, "os-release", "o", "/usr/lib/os-release", "OS Release file location")
f.StringVarP(&efiStub, "efi-stub", "e", "/usr/lib/systemd/boot/efi/linuxx64.efi.stub", "EFI Stub location")
f.StringVarP(&kernelImg, "kernel-img", "k", "/boot/vmlinuz-linux", "Kernel image location")
f.StringVarP(&cmdline, "cmdline", "c", "/etc/kernel/cmdline", "Cmdline location")
f.StringVarP(&initramfs, "initramfs", "f", "/boot/initramfs-linux.img", "Initramfs location")
f.StringVarP(&espPath, "esp", "p", esp, "ESP location")
f.BoolVarP(&saveBundle, "save", "s", false, "save bundle to the database")
}
func init() {
bundleCmdFlags(bundleCmd)
CliCommands = append(CliCommands, cliCommand{
Cmd: bundleCmd,
})
}

51
cmd/sbctl/completions.go Normal file
View File

@ -0,0 +1,51 @@
package main
import (
"os"
"github.com/spf13/cobra"
)
var completionCmd = &cobra.Command{Use: "completion"}
func completionBashCmd() *cobra.Command {
var completionCmd = &cobra.Command{
Use: "bash",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenBashCompletion(os.Stdout)
},
}
return completionCmd
}
func completionZshCmd() *cobra.Command {
var completionCmd = &cobra.Command{
Use: "zsh",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenZshCompletion(os.Stdout)
},
}
return completionCmd
}
func completionFishCmd() *cobra.Command {
var completionCmd = &cobra.Command{
Use: "fish",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenFishCompletion(os.Stdout, true)
},
}
return completionCmd
}
func init() {
completionCmd.AddCommand(completionBashCmd())
completionCmd.AddCommand(completionZshCmd())
completionCmd.AddCommand(completionFishCmd())
CliCommands = append(CliCommands, cliCommand{
Cmd: completionCmd,
})
}

32
cmd/sbctl/create-keys.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"fmt"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var createKeysCmd = &cobra.Command{
Use: "create-keys",
Short: "Create a set of secure boot signing keys",
RunE: func(cmd *cobra.Command, args []string) error {
if !sbctl.CheckIfKeysInitialized(sbctl.KeysPath) {
logging.Print("Creating secure boot keys...")
err := sbctl.InitializeSecureBootKeys(sbctl.DatabasePath)
if err != nil {
return fmt.Errorf("couldn't initialize secure boot: %w", err)
}
} else {
logging.Ok("Secure boot keys has already been created!")
}
return nil
},
}
func init() {
CliCommands = append(CliCommands, cliCommand{
Cmd: createKeysCmd,
})
}

46
cmd/sbctl/enroll-keys.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"errors"
"fmt"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var enrollKeysCmd = &cobra.Command{
Use: "enroll-keys",
Short: "Enroll the current keys to EFI",
RunE: func(cmd *cobra.Command, args []string) error {
var isImmutable bool
for _, file := range sbctl.EfivarFSFiles {
err := sbctl.IsImmutable(file)
if errors.Is(err, sbctl.ErrImmutable) {
isImmutable = true
logging.Warn("File is immutable: %s", file)
} else if errors.Is(err, sbctl.ErrNotImmutable) {
continue
} else if err != nil {
return fmt.Errorf("couldn't read file: %s", file)
}
}
if isImmutable {
return sbctl.ErrImmutable
}
logging.Print("Syncing keys to EFI variables...")
synced := sbctl.SBKeySync(sbctl.KeysPath)
if !synced {
return errors.New("couldn't sync keys")
}
logging.Println("")
logging.Ok("Synced keys!")
return nil
},
}
func init() {
CliCommands = append(CliCommands, cliCommand{
Cmd: enrollKeysCmd,
})
}

View File

@ -0,0 +1,69 @@
package main
import (
"errors"
"fmt"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var (
sign bool
)
var generateBundlesCmd = &cobra.Command{
Use: "generate-bundles",
Short: "Generate all EFI stub bundles",
RunE: func(cmd *cobra.Command, args []string) error {
logging.Println("Generating EFI bundles....")
out_create := true
out_sign := true
err := sbctl.BundleIter(func(bundle *sbctl.Bundle) error {
err := sbctl.CreateBundle(*bundle)
if err != nil {
fmt.Println(err)
out_create = false
return nil
}
logging.Print("Wrote EFI bundle %s\n", bundle.Output)
if sign {
file := bundle.Output
err = sbctl.SignFile(sbctl.DBKey, sbctl.DBCert, file, file, "")
if errors.Is(err, sbctl.ErrAlreadySigned) {
logging.Unknown("Bundle has already been signed")
} else if err != nil {
out_sign = false
} else {
logging.Ok("Signed %s", file)
}
}
return nil
})
if !out_create {
return errors.New("error generating EFI bundles")
}
if !out_sign {
return errors.New("error signing EFI bundles")
}
if err != nil {
return err
}
return nil
},
}
func generateBundlesCmdFlags(cmd *cobra.Command) {
f := cmd.Flags()
f.BoolVarP(&sign, "sign", "s", false, "Sign all the generated bundles")
}
func init() {
generateBundlesCmdFlags(generateBundlesCmd)
CliCommands = append(CliCommands, cliCommand{
Cmd: generateBundlesCmd,
})
}

73
cmd/sbctl/list-bundles.go Normal file
View File

@ -0,0 +1,73 @@
package main
import (
"strings"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
type JsonBundle struct {
sbctl.Bundle
IsSigned bool `json:"is_signed"`
}
var listBundlesCmd = &cobra.Command{
Use: "list-bundles",
Short: "List stored bundles",
RunE: func(cmd *cobra.Command, args []string) error {
bundles := []JsonBundle{}
var isSigned bool
err := sbctl.BundleIter(
func(s *sbctl.Bundle) error {
ok, err := sbctl.VerifyFile(sbctl.DBCert, s.Output)
if err != nil {
return err
}
logging.Println("Enrolled bundles:\n")
logging.Println(s.Output)
logging.Print("\tSigned:\t\t")
if ok {
isSigned = true
logging.Ok("Signed")
} else {
isSigned = false
logging.NotOk("Not Signed")
}
esp := sbctl.GetESP()
logging.Print("\tESP Location:\t%s\n", esp)
logging.Print("\tOutput:\t\t└─%s\n", strings.TrimPrefix(s.Output, esp))
logging.Print("\tEFI Stub Image:\t └─%s\n", s.EFIStub)
if s.Splash != "" {
logging.Print("\tSplash Image:\t ├─%s\n", s.Splash)
}
logging.Print("\tCmdline:\t ├─%s\n", s.Cmdline)
logging.Print("\tOS Release:\t ├─%s\n", s.OSRelease)
logging.Print("\tKernel Image:\t ├─%s\n", s.KernelImage)
logging.Print("\tInitramfs Image: └─%s\n", s.Initramfs)
if s.AMDMicrocode != "" {
logging.Print("\tAMD Microcode: └─%s\n", s.AMDMicrocode)
}
if s.IntelMicrocode != "" {
logging.Print("\tIntel Microcode: └─%s\n", s.IntelMicrocode)
}
bundles = append(bundles, JsonBundle{*s, isSigned})
logging.Println("")
return nil
})
if err != nil {
return err
}
if cmdOptions.JsonOutput {
JsonOut(bundles)
}
return nil
},
}
func init() {
CliCommands = append(CliCommands, cliCommand{
Cmd: listBundlesCmd,
})
}

61
cmd/sbctl/list-files.go Normal file
View File

@ -0,0 +1,61 @@
package main
import (
"fmt"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var listFilesCmd = &cobra.Command{
Use: "list-files",
Short: "List enrolled files",
RunE: RunList,
}
type JsonFile struct {
sbctl.SigningEntry
IsSigned bool `json:"is_signed"`
}
func RunList(_ *cobra.Command, args []string) error {
files := []JsonFile{}
var isSigned bool
err := sbctl.SigningEntryIter(
func(s *sbctl.SigningEntry) error {
ok, err := sbctl.VerifyFile(sbctl.DBCert, s.OutputFile)
if err != nil {
return err
}
logging.Println(s.File)
logging.Print("Signed:\t\t")
if ok {
isSigned = true
logging.Ok("Signed")
} else if !ok {
isSigned = false
logging.NotOk("Not Signed")
}
if s.File != s.OutputFile {
logging.Print("Output File:\t%s\n", s.OutputFile)
}
fmt.Println("")
files = append(files, JsonFile{*s, isSigned})
return nil
},
)
if err != nil {
return err
}
if cmdOptions.JsonOutput {
JsonOut(files)
}
return nil
}
func init() {
CliCommands = append(CliCommands, cliCommand{
Cmd: listFilesCmd,
})
}

388
cmd/sbctl/main.go Executable file → Normal file
View File

@ -1,353 +1,81 @@
package main
import (
"log"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "sbctl",
Short: "Secure Boot key manager",
type CmdOptions struct {
JsonOutput bool
}
func createKeysCmd() *cobra.Command {
return &cobra.Command{
Use: "create-keys",
Short: "Create a set of secure boot signing keys",
Run: func(cmd *cobra.Command, args []string) {
sbctl.CreateKeys()
},
type cliCommand struct {
Cmd *cobra.Command
}
var (
cmdOptions = CmdOptions{}
CliCommands = []cliCommand{}
ErrSilent = errors.New("SilentErr")
rootCmd = &cobra.Command{
Use: "sbctl",
Short: "Secure Boot Key Manager",
SilenceUsage: true,
SilenceErrors: true,
}
)
func baseFlags(cmd *cobra.Command) {
flags := cmd.PersistentFlags()
flags.BoolVar(&cmdOptions.JsonOutput, "json", false, "Output as json")
cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
if cmdOptions.JsonOutput {
logging.PrintOff()
}
}
}
func enrollKeysCmd() *cobra.Command {
return &cobra.Command{
Use: "enroll-keys",
Short: "Enroll the current keys to EFI",
Run: func(cmd *cobra.Command, args []string) {
sbctl.SyncKeys()
},
func JsonOut(v interface{}) error {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
return fmt.Errorf("could not marshal json: %w", err)
}
}
func signCmd() *cobra.Command {
var save bool
var output string
cmd := &cobra.Command{
Use: "sign",
Short: "Sign a file with secure boot keys",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
log.Fatalf("Requires a file to sign...\n")
}
// Ensure we have absolute paths
file, err := filepath.Abs(args[0])
if err != nil {
log.Fatal(err)
}
if output == "" {
output = file
} else {
output, err = filepath.Abs(output)
if err != nil {
log.Fatal(err)
}
}
if err := sbctl.Sign(file, output, save); err != nil {
log.Fatalln(err)
}
},
}
f := cmd.Flags()
f.BoolVarP(&save, "save", "s", false, "save file to the database")
f.StringVarP(&output, "output", "o", "", "output filename. Default replaces the file")
return cmd
}
func signAllCmd() *cobra.Command {
var generate bool
cmd := &cobra.Command{
Use: "sign-all",
Short: "Sign all enrolled files with secure boot keys",
Run: func(cmd *cobra.Command, args []string) {
var outBundle error
outSign := false
if generate {
outBundle = sbctl.GenerateAllBundles(true)
}
files, err := sbctl.ReadFileDatabase(sbctl.DBPath)
if err != nil {
log.Fatalln(err)
}
for _, entry := range files {
if sbctl.SignFile(sbctl.DBKey, sbctl.DBCert, entry.File, entry.OutputFile, entry.Checksum) != nil {
outSign = true
continue
}
// Update checksum after we signed it
checksum := sbctl.ChecksumFile(entry.File)
entry.Checksum = checksum
files[entry.File] = entry
sbctl.WriteFileDatabase(sbctl.DBPath, files)
}
if outBundle != nil || outSign {
log.Fatalln("Errors were encountered, see above")
}
},
}
f := cmd.Flags()
f.BoolVarP(&generate, "generate", "g", false, "run all generate-* sub-commands before signing")
return cmd
}
func removeFileCmd() *cobra.Command {
return &cobra.Command{
Use: "remove-file",
Short: "Remove file from database",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
log.Fatal("Need to specify file")
}
files, err := sbctl.ReadFileDatabase(sbctl.DBPath)
if err != nil {
log.Fatalln(err)
}
if _, ok := files[args[0]]; !ok {
log.Printf("File %s doesn't exist in database!\n", args[0])
os.Exit(1)
}
delete(files, args[0])
sbctl.WriteFileDatabase(sbctl.DBPath, files)
},
}
}
func statusCmd() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "Show current boot status",
Run: func(cmd *cobra.Command, args []string) {
sbctl.CheckStatus()
},
}
}
func verifyCmd() *cobra.Command {
return &cobra.Command{
Use: "verify",
Short: "Find and check if files in the ESP are signed or not",
Run: func(cmd *cobra.Command, args []string) {
if err := sbctl.VerifyESP(); err != nil {
// Really need to sort out the low level error handling
os.Exit(1)
}
},
}
}
func listFilesCmd() *cobra.Command {
return &cobra.Command{
Use: "list-files",
Short: "List enrolled files",
Run: func(cmd *cobra.Command, args []string) {
sbctl.ListFiles()
},
}
}
func bundleCmd() *cobra.Command {
var amducode string
var intelucode string
var splashImg string
var osRelease string
var efiStub string
var kernelImg string
var cmdline string
var initramfs string
var espPath string
var save bool
cmd := &cobra.Command{
Use: "bundle",
Short: "Bundle the needed files for an EFI stub image",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
log.Fatalf("Requires a file to sign...\n")
}
checkFiles := []string{amducode, intelucode, splashImg, osRelease, efiStub, kernelImg, cmdline, initramfs}
for _, path := range checkFiles {
if path == "" {
continue
}
if _, err := os.Stat(path); os.IsNotExist(err) {
log.Fatalf("%s does not exist!", path)
os.Exit(1)
}
}
bundle := sbctl.NewBundle()
output, err := filepath.Abs(args[0])
if err != nil {
log.Fatal(err)
}
// Fail early if user wants to save bundle but doesn't have permissions
var bundles sbctl.Bundles
if save {
// "err" needs to have been declared before this, otherwise it's necessary
// to use ":=", which shadows the "bundles" variable
bundles, err = sbctl.ReadBundleDatabase(sbctl.BundleDBPath)
if err != nil {
log.Fatalln(err)
}
}
bundle.Output = output
bundle.IntelMicrocode = intelucode
bundle.AMDMicrocode = amducode
bundle.KernelImage = kernelImg
bundle.Initramfs = initramfs
bundle.Cmdline = cmdline
bundle.Splash = splashImg
bundle.OSRelease = osRelease
bundle.EFIStub = efiStub
bundle.ESP = espPath
if err = sbctl.CreateBundle(*bundle); err != nil {
log.Fatalln(err)
os.Exit(1)
}
if save {
bundles[bundle.Output] = bundle
sbctl.WriteBundleDatabase(sbctl.BundleDBPath, bundles)
sbctl.FormatBundle(bundle.Output, bundle)
}
},
}
esp := sbctl.GetESP()
f := cmd.Flags()
f.StringVarP(&amducode, "amducode", "a", "", "AMD microcode location")
f.StringVarP(&intelucode, "intelucode", "i", "", "Intel microcode location")
f.StringVarP(&splashImg, "splash-img", "l", "", "Boot splash image location")
f.StringVarP(&osRelease, "os-release", "o", "/usr/lib/os-release", "OS Release file location")
f.StringVarP(&efiStub, "efi-stub", "e", "/usr/lib/systemd/boot/efi/linuxx64.efi.stub", "EFI Stub location")
f.StringVarP(&kernelImg, "kernel-img", "k", "/boot/vmlinuz-linux", "Kernel image location")
f.StringVarP(&cmdline, "cmdline", "c", "/etc/kernel/cmdline", "Cmdline location")
f.StringVarP(&initramfs, "initramfs", "f", "/boot/initramfs-linux.img", "Initramfs location")
f.StringVarP(&espPath, "esp", "p", esp, "ESP location")
f.BoolVarP(&save, "save", "s", false, "save bundle to the database")
return cmd
}
func generateBundlesCmd() *cobra.Command {
var sign bool
cmd := &cobra.Command{
Use: "generate-bundles",
Short: "Generate all EFI stub bundles",
Run: func(cmd *cobra.Command, args []string) {
sbctl.GenerateAllBundles(sign)
},
}
f := cmd.Flags()
f.BoolVarP(&sign, "sign", "s", false, "Sign all the generated bundles")
return cmd
}
func listBundlesCmd() *cobra.Command {
return &cobra.Command{
Use: "list-bundles",
Short: "List stored bundles",
Run: func(cmd *cobra.Command, args []string) {
sbctl.ListBundles()
},
}
}
func removeBundleCmd() *cobra.Command {
return &cobra.Command{
Use: "remove-bundle",
Short: "Remove bundle from database",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
log.Fatal("Need to specify file")
}
bundles, err := sbctl.ReadBundleDatabase(sbctl.BundleDBPath)
if err != nil {
log.Fatalln(err)
}
if _, ok := bundles[args[0]]; !ok {
log.Printf("Bundle %s doesn't exist in database!\n", args[0])
os.Exit(1)
}
delete(bundles, args[0])
sbctl.WriteBundleDatabase(sbctl.BundleDBPath, bundles)
},
}
}
func completionBashCmd() *cobra.Command {
var completionCmd = &cobra.Command{
Use: "bash",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenBashCompletion(os.Stdout)
},
}
return completionCmd
}
func completionZshCmd() *cobra.Command {
var completionCmd = &cobra.Command{
Use: "zsh",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenZshCompletion(os.Stdout)
},
}
return completionCmd
}
func completionFishCmd() *cobra.Command {
var completionCmd = &cobra.Command{
Use: "fish",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenFishCompletion(os.Stdout, true)
},
}
return completionCmd
fmt.Fprint(os.Stdout, string(b))
return nil
}
func main() {
rootCmd.AddCommand(createKeysCmd())
rootCmd.AddCommand(enrollKeysCmd())
rootCmd.AddCommand(signCmd())
rootCmd.AddCommand(signAllCmd())
rootCmd.AddCommand(statusCmd())
rootCmd.AddCommand(verifyCmd())
rootCmd.AddCommand(listFilesCmd())
rootCmd.AddCommand(bundleCmd())
rootCmd.AddCommand(generateBundlesCmd())
rootCmd.AddCommand(removeBundleCmd())
rootCmd.AddCommand(listBundlesCmd())
rootCmd.AddCommand(removeFileCmd())
for _, cmd := range CliCommands {
rootCmd.AddCommand(cmd.Cmd)
}
baseFlags(rootCmd)
// This returns i the flag is not found with a specific error
rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
cmd.Println(err)
cmd.Println(cmd.UsageString())
return ErrSilent
})
completionCmd := &cobra.Command{Use: "completion"}
completionCmd.AddCommand(completionBashCmd())
completionCmd.AddCommand(completionZshCmd())
completionCmd.AddCommand(completionFishCmd())
rootCmd.AddCommand(completionCmd)
if err := rootCmd.Execute(); err != nil {
if strings.HasPrefix(err.Error(), "unknown comman") {
logging.Println(err.Error())
} else if errors.Is(err, os.ErrPermission) {
logging.Error(fmt.Errorf("sbtl requires root to run: %w", err))
} else if errors.Is(err, sbctl.ErrImmutable) {
logging.Println("You need to chattr -i files in efivarfs")
} else if !errors.Is(err, ErrSilent) {
logging.Fatal(err)
}
os.Exit(1)
}
sbctl.ColorsOff()
}

View File

@ -0,0 +1,41 @@
package main
import (
"os"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var removeBundleCmd = &cobra.Command{
Use: "remove-bundle",
Short: "Remove bundle from database",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
logging.Print("Need to specify file\n")
os.Exit(1)
}
bundles, err := sbctl.ReadBundleDatabase(sbctl.BundleDBPath)
if err != nil {
return err
}
if _, ok := bundles[args[0]]; !ok {
logging.Print("Bundle %s doesn't exist in database!\n", args[0])
os.Exit(1)
}
delete(bundles, args[0])
err = sbctl.WriteBundleDatabase(sbctl.BundleDBPath, bundles)
if err != nil {
return err
}
return nil
},
}
func init() {
CliCommands = append(CliCommands, cliCommand{
Cmd: removeBundleCmd,
})
}

39
cmd/sbctl/remove-files.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"os"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var removeFileCmd = &cobra.Command{
Use: "remove-file",
Short: "Remove file from database",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
logging.Println("Need to specify file")
os.Exit(1)
}
files, err := sbctl.ReadFileDatabase(sbctl.DBPath)
if err != nil {
return err
}
if _, ok := files[args[0]]; !ok {
logging.Print("File %s doesn't exist in database!\n", args[0])
os.Exit(1)
}
delete(files, args[0])
if err := sbctl.WriteFileDatabase(sbctl.DBPath, files); err != nil {
return err
}
return nil
},
}
func init() {
CliCommands = append(CliCommands, cliCommand{
Cmd: removeFileCmd,
})
}

67
cmd/sbctl/sign-all.go Normal file
View File

@ -0,0 +1,67 @@
package main
import (
"errors"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var (
generate bool
)
var signAllCmd = &cobra.Command{
Use: "sign-all",
Short: "Sign all enrolled files with secure boot keys",
RunE: func(cmd *cobra.Command, args []string) error {
if generate {
sign = true
if err := generateBundlesCmd.RunE(cmd, args); err != nil {
return err
}
}
files, err := sbctl.ReadFileDatabase(sbctl.DBPath)
if err != nil {
return err
}
for _, entry := range files {
err := sbctl.SignFile(sbctl.DBKey, sbctl.DBCert, entry.File, entry.OutputFile, entry.Checksum)
if errors.Is(err, sbctl.ErrAlreadySigned) {
logging.Print("File have already been signed %s\n", entry.OutputFile)
} else if err != nil {
return err
} else {
logging.Ok("Signed %s", entry.OutputFile)
}
// Update checksum after we signed it
checksum, err := sbctl.ChecksumFile(entry.File)
if err != nil {
return err
}
entry.Checksum = checksum
files[entry.File] = entry
if err := sbctl.WriteFileDatabase(sbctl.DBPath, files); err != nil {
return err
}
}
return nil
},
}
func signAllCmdFlags(cmd *cobra.Command) {
f := cmd.Flags()
f.BoolVarP(&generate, "generate", "g", false, "run all generate-* sub-commands before signing")
}
func init() {
signAllCmdFlags(signAllCmd)
CliCommands = append(CliCommands, cliCommand{
Cmd: signAllCmd,
})
}

58
cmd/sbctl/sign.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"os"
"path/filepath"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var (
save bool
output string
)
var signCmd = &cobra.Command{
Use: "sign",
Short: "Sign a file with secure boot keys",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
logging.Print("Requires a file to sign\n")
os.Exit(1)
}
// Ensure we have absolute paths
file, err := filepath.Abs(args[0])
if err != nil {
return err
}
if output == "" {
output = file
} else {
output, err = filepath.Abs(output)
if err != nil {
return err
}
}
if err := sbctl.Sign(file, output, save); err != nil {
return err
}
return nil
},
}
func signCmdFlags(cmd *cobra.Command) {
f := cmd.Flags()
f.BoolVarP(&save, "save", "s", false, "save file to the database")
f.StringVarP(&output, "output", "o", "", "output filename. Default replaces the file")
}
func init() {
signCmdFlags(signCmd)
CliCommands = append(CliCommands, cliCommand{
Cmd: signCmd,
})
}

57
cmd/sbctl/status.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
"fmt"
"os"
"github.com/foxboron/go-uefi/efi"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var statusCmd = &cobra.Command{
Use: "status",
Short: "Show current boot status",
RunE: RunStatus,
}
func RunStatus(cmd *cobra.Command, args []string) error {
ret := map[string]interface{}{}
if _, err := os.Stat("/sys/firmware/efi/efivars"); os.IsNotExist(err) {
return fmt.Errorf("system is not booted with UEFI")
}
u, err := sbctl.GetGUID()
if err != nil {
return err
}
logging.Print("Owner GUID:\t")
logging.Println(u.String())
ret["Owner GUID"] = u.String()
logging.Print("Setup Mode:\t")
if efi.GetSetupMode() {
logging.NotOk("Enabled")
ret["Setup Mode"] = true
} else {
logging.Ok("Disabled")
ret["Setup Mode"] = false
}
logging.Print("Secure Boot:\t")
if efi.GetSecureBoot() {
logging.Ok("Enabled")
ret["Secure Boot"] = true
} else {
logging.NotOk("Disabled")
ret["Secure Boot"] = false
}
if cmdOptions.JsonOutput {
JsonOut(ret)
}
return nil
}
func init() {
CliCommands = append(CliCommands, cliCommand{
Cmd: statusCmd,
})
}

87
cmd/sbctl/verify.go Normal file
View File

@ -0,0 +1,87 @@
package main
import (
"errors"
"os"
"path/filepath"
"github.com/foxboron/sbctl"
"github.com/foxboron/sbctl/logging"
"github.com/spf13/cobra"
)
var verifyCmd = &cobra.Command{
Use: "verify",
Short: "Find and check if files in the ESP are signed or not",
RunE: func(cmd *cobra.Command, args []string) error {
// Exit early if we can't verify files
if err := sbctl.CanVerifyFiles(); err != nil {
return err
}
espPath := sbctl.GetESP()
logging.Print("Verifying file database and EFI images in %s...\n", espPath)
if err := sbctl.SigningEntryIter(func(file *sbctl.SigningEntry) error {
sbctl.AddChecked(file.OutputFile)
// Check output file exists before checking if it's signed
if _, err := os.Open(file.OutputFile); errors.Is(err, os.ErrNotExist) {
logging.Warn("%s does not exist", file.OutputFile)
return nil
} else if errors.Is(err, os.ErrPermission) {
logging.Warn("%s permission denied. Can't read file\n", file.OutputFile)
return nil
}
ok, err := sbctl.VerifyFile(sbctl.DBCert, file.OutputFile)
if err != nil {
return err
}
if ok {
logging.Ok("%s is signed", file.OutputFile)
} else {
logging.NotOk("%s is not signed", file.OutputFile)
}
return nil
}); err != nil {
return err
}
if err := filepath.Walk(espPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if fi, _ := os.Stat(path); fi.IsDir() {
return nil
}
if sbctl.InChecked(path) {
return nil
}
ok, err := sbctl.CheckMSDos(path)
if err != nil {
return err
}
if !ok {
return nil
}
ok, err = sbctl.VerifyFile(sbctl.DBCert, path)
if err != nil {
return err
}
if ok {
logging.Ok("%s is signed\n", path)
} else {
logging.NotOk("%s is not signed\n", path)
}
return nil
}); err != nil {
return err
}
return nil
},
}
func init() {
CliCommands = append(CliCommands, cliCommand{
Cmd: verifyCmd,
})
}

View File

@ -1 +0,0 @@
package sbctl

View File

@ -2,7 +2,7 @@ package sbctl
import (
"encoding/json"
"log"
"fmt"
"os"
)
@ -26,13 +26,27 @@ func ReadFileDatabase(dbpath string) (SigningEntries, error) {
return files, nil
}
func WriteFileDatabase(dbpath string, files SigningEntries) {
func WriteFileDatabase(dbpath string, files SigningEntries) error {
data, err := json.MarshalIndent(files, "", " ")
if err != nil {
log.Fatal(err)
return err
}
err = os.WriteFile(dbpath, data, 0644)
if err != nil {
log.Fatal(err)
return err
}
return nil
}
func SigningEntryIter(fn func(s *SigningEntry) error) error {
files, err := ReadFileDatabase(DBPath)
if err != nil {
return fmt.Errorf("couldn't open database %v: %w", DBPath, err)
}
for _, s := range files {
if err := fn(s); err != nil {
return err
}
}
return nil
}

2
go.mod
View File

@ -3,8 +3,10 @@ module github.com/foxboron/sbctl
go 1.15
require (
github.com/fatih/color v1.11.0 // indirect
github.com/foxboron/go-uefi v0.0.0-20210105211851-5faf8e43ee9b
github.com/google/uuid v1.1.1
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/cobra v1.0.0
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c
)

10
go.sum
View File

@ -18,6 +18,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.11.0 h1:l4iX0RqNnx/pU7rY2DB/I+znuYY0K3x6Ywac6EIr0PA=
github.com/fatih/color v1.11.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/foxboron/go-uefi v0.0.0-20210105211851-5faf8e43ee9b h1:Wsc63VYJUbbGF/YKUK9+TjguRUIKN/a5SvhB/mG94oc=
github.com/foxboron/go-uefi v0.0.0-20210105211851-5faf8e43ee9b/go.mod h1:lP2qQFTFX3752ZHhqwp0U+A0d6oRHZEBn06+mMssM/g=
github.com/foxboron/pkcs7 v0.0.0-20200515184129-2907ba0539a4/go.mod h1:px0/6X5Ap1wlLCWQ1DmiBULqyLBpoiXpUm0Vce+ufSk=
@ -55,6 +57,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
@ -62,6 +68,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
@ -118,6 +126,8 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c h1:+B+zPA6081G5cEb2triOIJpcvSW4AYzmIyWAqMn2JAc=
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=

44
guid.go Normal file
View File

@ -0,0 +1,44 @@
package sbctl
import (
"io/ioutil"
"os"
"path/filepath"
"github.com/google/uuid"
)
func CreateUUID() []byte {
id, _ := uuid.NewRandom()
return []byte(id.String())
}
func CreateGUID(output string) ([]byte, error) {
var uuid []byte
guidPath := filepath.Join(output, "GUID")
if _, err := os.Stat(guidPath); os.IsNotExist(err) {
uuid = CreateUUID()
err := ioutil.WriteFile(guidPath, uuid, 0600)
if err != nil {
return nil, err
}
} else {
uuid, err = ioutil.ReadFile(guidPath)
if err != nil {
return nil, err
}
}
return uuid, nil
}
func GetGUID() (uuid.UUID, error) {
b, err := os.ReadFile(GUIDPath)
if err != nil {
return [16]byte{}, err
}
u, err := uuid.ParseBytes(b)
if err != nil {
return [16]byte{}, err
}
return u, err
}

156
keys.go
View File

@ -7,8 +7,8 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"log"
"math/big"
"os"
"os/exec"
@ -16,7 +16,7 @@ import (
"strings"
"time"
"github.com/google/uuid"
"github.com/foxboron/sbctl/logging"
"golang.org/x/sys/unix"
)
@ -33,14 +33,21 @@ var (
DBCert = filepath.Join(KeysPath, "db", "db.pem")
DBPath = filepath.Join(DatabasePath, "files.db")
GUIDPath = filepath.Join(DatabasePath, "GUID")
)
func CreateKey(path, name string) []byte {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Fatalf("Failed to generate serial number: %v", err)
// Check if we can access the db certificate to verify files
func CanVerifyFiles() error {
if err := unix.Access(DBCert, unix.R_OK); err != nil {
return fmt.Errorf("couldn't access %s: %w", DBCert, err)
}
return nil
}
func CreateKey(path, name string) ([]byte, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit)
c := x509.Certificate{
SerialNumber: serialNumber,
PublicKeyAlgorithm: x509.RSA,
@ -55,72 +62,69 @@ func CreateKey(path, name string) []byte {
}
priv, err := rsa.GenerateKey(rand.Reader, RSAKeySize)
if err != nil {
log.Fatal(err)
return nil, err
}
derBytes, err := x509.CreateCertificate(rand.Reader, &c, &c, &priv.PublicKey, priv)
if err != nil {
log.Fatalf("Failed to create certificate: %v", err)
return nil, err
}
keyOut, err := os.OpenFile(fmt.Sprintf("%s.key", path), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Failed to open key.pem for writing: %v", err)
return nil, fmt.Errorf("failed to open key.pem for writing: %v", err)
}
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
log.Fatalf("Unable to marshal private key: %v", err)
return nil, fmt.Errorf("unable to marshal private key: %v", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
log.Fatalf("Failed to write data to key.pem: %v", err)
return nil, fmt.Errorf("failed to write data to key.pem: %v", err)
}
if err := keyOut.Close(); err != nil {
log.Fatalf("Error closing key.pem: %v", err)
return nil, fmt.Errorf("error closing key.pem: %v", err)
}
return derBytes
return derBytes, nil
}
func SaveKey(k []byte, path string) {
func SaveKey(k []byte, path string) error {
err := os.WriteFile(fmt.Sprintf("%s.der", path), k, 0644)
if err != nil {
log.Fatal(err)
return err
}
certOut, err := os.Create(fmt.Sprintf("%s.pem", path))
if err != nil {
log.Fatalf("Failed to open cert.pem for writing: %v", err)
return err
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: k}); err != nil {
log.Fatalf("Failed to write data to cert.pem: %v", err)
return err
}
if err := certOut.Close(); err != nil {
log.Fatalf("Error closing cert.pem: %v", err)
return err
}
return nil
}
func KeyToSiglist(UUID []byte, input string) []byte {
msg.Printf("Create EFI signature list %s.esl...", input)
out, err := exec.Command(
func KeyToSiglist(UUID []byte, input string) error {
_, err := exec.Command(
"sbsiglist",
"--owner", string(UUID),
"--type", "x509",
"--output", fmt.Sprintf("%s.esl", input), input,
).Output()
if err != nil {
log.Fatalf("Failed creating signature list: %s", err)
return err
}
return out
return nil
}
func SignEFIVariable(key, cert, varname, vardatafile, output string) []byte {
msg.Printf("Signing %s with %s...", vardatafile, key)
func SignEFIVariable(key, cert, varname, vardatafile, output string) ([]byte, error) {
out, err := exec.Command("sbvarsign", "--key", key, "--cert", cert, "--output", output, varname, vardatafile).Output()
if err != nil {
log.Fatalf("Failed signing EFI variable: %s", err)
return nil, fmt.Errorf("failed signing EFI variable: %v", err)
}
return out
return out, nil
}
func SBKeySync(dir string) bool {
msg.Printf("Syncing %s to EFI variables...", dir)
cmd := exec.Command("sbkeysync", "--pk", "--verbose", "--keystore", dir)
var out bytes.Buffer
cmd.Stdout = &out
@ -144,41 +148,51 @@ func SBKeySync(dir string) bool {
return true
}
func VerifyFile(cert, file string) bool {
func VerifyFile(cert, file string) (bool, error) {
if err := unix.Access(cert, unix.R_OK); err != nil {
return false, fmt.Errorf("couldn't access %s: %w", cert, err)
}
cmd := exec.Command("sbverify", "--cert", cert, file)
if err := cmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.ExitCode() == 0
return exitError.ExitCode() == 0, nil
}
}
return true
return true, nil
}
var ErrAlreadySigned = errors.New("already signed file")
func SignFile(key, cert, file, output, checksum string) error {
// Check file exists before we do anything
if _, err := os.Stat(file); os.IsNotExist(err) {
return PrintGenerateError(err2, "%s does not exist!", file)
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("%s does not exist", file)
}
// Let's check if we have signed it already AND the original file hasn't changed
if VerifyFile(cert, output) && ChecksumFile(file) == checksum {
msg.Printf("%s has been signed...", file)
return nil
ok, err := VerifyFile(cert, output)
if err != nil {
return err
}
chk, err := ChecksumFile(file)
if err != nil {
return err
}
if ok && chk == checksum {
return ErrAlreadySigned
}
// Let's also check if we can access the key
if err := unix.Access(key, unix.R_OK); err != nil {
err2.Printf("Couldn't access %s", key)
return err
return fmt.Errorf("couldn't access %s: %w", key, err)
}
msg2.Printf("Signing %s...", file)
_, err := exec.Command("sbsign", "--key", key, "--cert", cert, "--output", output, file).Output()
_, err = exec.Command("sbsign", "--key", key, "--cert", cert, "--output", output, file).Output()
if err != nil {
return PrintGenerateError(err2, "Failed signing file: %s", err)
return fmt.Errorf("failed signing file: %w", err)
}
return nil
}
@ -221,50 +235,34 @@ func CheckIfKeysInitialized(output string) bool {
return true
}
func CreateUUID() []byte {
id, err := uuid.NewRandom()
if err != nil {
log.Fatal(err)
}
return []byte(id.String())
}
func CreateGUID(output string) []byte {
var uuid []byte
guidPath := filepath.Join(output, "GUID")
if _, err := os.Stat(guidPath); os.IsNotExist(err) {
uuid = CreateUUID()
msg2.Printf("Created UUID %s...", uuid)
err := os.WriteFile(guidPath, uuid, 0600)
if err != nil {
log.Fatal(err)
}
} else {
uuid, err = os.ReadFile(guidPath)
if err != nil {
log.Fatal(err)
}
msg2.Printf("Using UUID %s...", uuid)
}
return uuid
}
func InitializeSecureBootKeys(output string) {
func InitializeSecureBootKeys(output string) error {
os.MkdirAll(output, os.ModePerm)
uuid := CreateGUID(output)
uuid, err := CreateGUID(output)
if err != nil {
return err
}
logging.Print("Using Owner UUID %s\n", uuid)
// Create the directories we need and keys
for _, key := range SecureBootKeys {
path := filepath.Join(output, "keys", key.Key)
os.MkdirAll(path, os.ModePerm)
keyPath := filepath.Join(path, key.Key)
pk := CreateKey(keyPath, key.Description)
pk, err := CreateKey(keyPath, key.Description)
if err != nil {
return err
}
SaveKey(pk, keyPath)
KeyToSiglist(uuid, fmt.Sprintf("%s.der", keyPath))
// Confusing code
// TODO: make it cleaner
derSiglist := fmt.Sprintf("%s.der", keyPath)
if err := KeyToSiglist(uuid, derSiglist); err != nil {
return err
}
logging.Print("Created EFI signature list %s.esl...", derSiglist)
signingkeyPath := filepath.Join(output, "keys", key.SignedWith, key.SignedWith)
signingKey := fmt.Sprintf("%s.key", signingkeyPath)
signingCertificate := fmt.Sprintf("%s.pem", signingkeyPath)
SignEFIVariable(signingKey, signingCertificate, key.Key, fmt.Sprintf("%s.der.esl", keyPath), fmt.Sprintf("%s.auth", keyPath))
vardatafile := fmt.Sprintf("%s.der.esl", keyPath)
logging.Print("Signing %s with %s...", vardatafile, key.Key)
SignEFIVariable(signingKey, signingCertificate, key.Key, vardatafile, fmt.Sprintf("%s.auth", keyPath))
}
return nil
}

65
log.go
View File

@ -1,65 +0,0 @@
package sbctl
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"strings"
)
var (
msg *log.Logger
msg2 *log.Logger
warning *log.Logger
warning2 *log.Logger
err1 *log.Logger
err2 *log.Logger
)
var (
red = GetColor("setaf 1")
green = GetColor("setaf 2")
yellow = GetColor("setaf 3")
blue = GetColor("setaf 4")
bold = GetColor("bold")
off = GetColor("sgr0")
// I didn't bother figure out how we get this to the end of the log format
// So we just clear the terminal stuff at the start of each log line
prefix = off
)
var (
rootMsg = "It might be necessary to run this tool as root"
)
func GetColor(args string) string {
out, _ := exec.Command("tput", strings.Split(args, " ")...).Output()
return string(bytes.TrimSuffix(out, []byte("\n")))
}
func ColorsOff() {
fmt.Print(off)
}
func init() {
msgfmt := fmt.Sprintf("%s%s%s==>%s%s ", prefix, bold, green, off, bold)
msg = log.New(os.Stdout, msgfmt, 0)
msg2fmt := fmt.Sprintf("%s%s%s ->%s%s ", prefix, bold, blue, off, bold)
msg2 = log.New(os.Stdout, msg2fmt, 0)
warningfmt := fmt.Sprintf("%s%s%s==> WARNING:%s%s ", prefix, bold, yellow, off, bold)
warning = log.New(os.Stderr, warningfmt, 0)
warning2fmt := fmt.Sprintf("%s%s%s -> WARNING:%s%s ", prefix, bold, yellow, off, bold)
warning2 = log.New(os.Stderr, warning2fmt, 0)
errfmt := fmt.Sprintf("%s%s%s==> ERROR:%s%s ", prefix, bold, red, off, bold)
err1 = log.New(os.Stderr, errfmt, 0)
err2fmt := fmt.Sprintf("%s%s%s -> ERROR:%s%s ", prefix, bold, red, off, bold)
err2 = log.New(os.Stderr, err2fmt, 0)
}

118
logging/logging.go Normal file
View File

@ -0,0 +1,118 @@
package logging
import (
"fmt"
"os"
"github.com/fatih/color"
)
var (
OkSym = "✔"
NotOkSym = "✘"
WarnSym = "‼"
UnkwnSym = "⁇"
)
var (
OkSymText = "[+]"
NotOkSymText = "[-]"
WarnSymText = "[!]"
UnkwnSymText = "[?]"
)
var (
ok string
notok string
warn string
unkwn string
)
var (
on bool
)
func PrintOn() {
on = true
}
func PrintOff() {
on = false
}
func PrintWithFile(f *os.File, msg string, a ...interface{}) {
if on {
fmt.Fprintf(f, msg, a...)
}
}
func Print(msg string, a ...interface{}) {
PrintWithFile(os.Stdout, msg, a...)
}
func Println(msg string) {
PrintWithFile(os.Stdout, msg+"\n")
}
func Okf(m string, a ...interface{}) string {
return fmt.Sprintf("%s %s\n", ok, fmt.Sprintf(m, a...))
}
// Print ok string to stdout
func Ok(m string, a ...interface{}) {
Print(Okf(m, a...))
}
func NotOkf(m string, a ...interface{}) string {
return fmt.Sprintf("%s %s\n", notok, fmt.Sprintf(m, a...))
}
// Print ok string to stdout
func NotOk(m string, a ...interface{}) {
Print(NotOkf(m, a...))
}
func Unknownf(m string, a ...interface{}) string {
return fmt.Sprintf("%s %s\n", unkwn, fmt.Sprintf(m, a...))
}
func Unknown(m string, a ...interface{}) {
Print(Unknownf(m, a...))
}
func Warnf(m string, a ...interface{}) string {
return fmt.Sprintf("%s %s\n", warn, fmt.Sprintf(m, a...))
}
func Warn(m string, a ...interface{}) {
Print(Warnf(m, a...))
}
func Fatalf(m string, a ...interface{}) string {
return color.New(color.FgRed, color.Bold).Sprintf("%s %s\n", UnkwnSym, fmt.Sprintf(m, a...))
}
func Fatal(err error) {
PrintWithFile(os.Stderr, Fatalf(err.Error()))
}
func Errorf(m string, a ...interface{}) string {
return color.New(color.FgRed, color.Bold).Sprintf("%s\n", fmt.Sprintf(m, a...))
}
func Error(err error) {
PrintWithFile(os.Stderr, Errorf(err.Error()))
}
func init() {
if ok := os.Getenv("EFIBOOTCTL_UNICODE"); ok == "0" {
OkSym = OkSymText
NotOkSym = NotOkSymText
WarnSym = WarnSymText
UnkwnSym = UnkwnSymText
}
ok = color.New(color.FgGreen, color.Bold).Sprintf(OkSym)
notok = color.New(color.FgRed, color.Bold).Sprintf(NotOkSym)
warn = color.New(color.FgYellow, color.Bold).Sprintf(WarnSym)
unkwn = color.New(color.FgRed, color.Bold).Sprintf(UnkwnSym)
PrintOn()
}

232
sbctl.go
View File

@ -1,17 +1,13 @@
package sbctl
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/foxboron/go-uefi/efi/attributes"
)
// Functions that doesn't fit anywhere else
@ -89,79 +85,10 @@ func GetESP() string {
return ""
}
func VerifyESP() error {
espPath := GetESP()
files, err := ReadFileDatabase(DBPath)
if err != nil {
err1.Printf("Couldn't read file database: %s", err)
return err
} else {
msg.Printf("Verifying file database and EFI images in %s...", espPath)
}
// Cache files we have looked at.
checked := make(map[string]bool)
for _, file := range files {
normalized := strings.Join(strings.Split(file.OutputFile, "/")[2:], "/")
checked[normalized] = true
// Check output file exists before checking if it's signed
if _, err := os.Open(file.OutputFile); errors.Is(err, os.ErrNotExist) {
err2.Printf("%s does not exist\n", file.OutputFile)
} else if errors.Is(err, os.ErrPermission) {
err2.Printf("%s permission denied. Can't read file\n", file.OutputFile)
} else if VerifyFile(DBCert, file.OutputFile) {
msg2.Printf("%s is signed\n", file.OutputFile)
} else {
warning2.Printf("%s is not signed\n", file.OutputFile)
}
}
err = filepath.Walk(espPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if fi, _ := os.Stat(path); fi.IsDir() {
return nil
}
// Don't check files we have checked
normalized := strings.Join(strings.Split(path, "/")[2:], "/")
if ok := checked[normalized]; ok {
return nil
}
r, _ := os.Open(path)
defer r.Close()
// We are looking for MS-DOS executables.
// They contain "MZ" as the two first bytes
var header [2]byte
if _, err = io.ReadFull(r, header[:]); err != nil {
return nil
}
if !bytes.Equal(header[:], []byte{0x4d, 0x5a}) {
return nil
}
if VerifyFile(DBCert, path) {
msg2.Printf("%s is signed\n", path)
} else {
warning2.Printf("%s is not signed\n", path)
}
return nil
})
if err != nil {
log.Println(err)
}
return nil
}
func Sign(file, output string, enroll bool) error {
file, err := filepath.Abs(file)
if err != nil {
log.Fatal(err)
return err
}
if output == "" {
@ -169,7 +96,7 @@ func Sign(file, output string, enroll bool) error {
} else {
output, err = filepath.Abs(output)
if err != nil {
log.Fatal(err)
return err
}
}
@ -177,8 +104,7 @@ func Sign(file, output string, enroll bool) error {
files, err := ReadFileDatabase(DBPath)
if err != nil {
err2.Printf("Couldn't open database: %s", DBPath)
return err
return fmt.Errorf("couldn't open database: %s", DBPath)
}
if entry, ok := files[file]; ok {
err = SignFile(DBKey, DBCert, entry.File, entry.OutputFile, entry.Checksum)
@ -186,10 +112,15 @@ func Sign(file, output string, enroll bool) error {
if err != nil {
return err
}
checksum := ChecksumFile(file)
checksum, err := ChecksumFile(file)
if err != nil {
return err
}
entry.Checksum = checksum
files[file] = entry
WriteFileDatabase(DBPath, files)
if err := WriteFileDatabase(DBPath, files); err != nil {
return err
}
} else {
err = SignFile(DBKey, DBCert, file, output, "")
// return early if signing fails
@ -199,94 +130,23 @@ func Sign(file, output string, enroll bool) error {
}
if enroll {
checksum := ChecksumFile(file)
checksum, err := ChecksumFile(file)
if err != nil {
return err
}
files[file] = &SigningEntry{File: file, OutputFile: output, Checksum: checksum}
WriteFileDatabase(DBPath, files)
if err := WriteFileDatabase(DBPath, files); err != nil {
return err
}
}
return err
}
func ListFiles() {
files, err := ReadFileDatabase(DBPath)
if err != nil {
err2.Printf("Couldn't open database: %s", DBPath)
return
}
for path, s := range files {
msg.Printf("File: %s", path)
if path != s.OutputFile {
msg2.Printf("Output: %s", s.OutputFile)
}
}
}
func CheckStatus() {
if _, err := os.Stat("/sys/firmware/efi/efivars"); os.IsNotExist(err) {
warning.Println("System is not booted with UEFI!")
os.Exit(1)
}
if sm, err := attributes.ReadEfivars("SetupMode"); err == nil {
if sm.Data[0] == 1 {
warning.Println("Setup Mode: Enabled")
} else {
msg.Println("Setup Mode: Disabled")
}
}
if sb, err := attributes.ReadEfivars("SecureBoot"); err == nil {
if sb.Data[0] == 1 {
msg.Println("Secure Boot: Enabled")
} else {
warning.Println("Secure Boot: Disabled")
}
}
}
func CreateKeys() {
if !CheckIfKeysInitialized(KeysPath) {
msg.Printf("Creating secure boot keys...")
InitializeSecureBootKeys(DatabasePath)
} else {
msg.Printf("Secure boot keys has been created")
}
}
var efivarFSFiles = []string{
"/sys/firmware/efi/efivars/PK-8be4df61-93ca-11d2-aa0d-00e098032b8c",
"/sys/firmware/efi/efivars/KEK-8be4df61-93ca-11d2-aa0d-00e098032b8c",
"/sys/firmware/efi/efivars/db-d719b2cb-3d3a-4596-a3bc-dad00e67656f",
}
func SyncKeys() {
errImmuable := false
for _, file := range efivarFSFiles {
b, err := IsImmutable(file)
if err != nil {
err1.Printf("Couldn't read file: %s\n", file)
os.Exit(1)
}
if b {
err1.Printf("File is immutable: %s\n", file)
errImmuable = true
}
}
if errImmuable {
err1.Println("You need to chattr -i files in efivarfs")
os.Exit(1)
}
synced := SBKeySync(KeysPath)
if !synced {
err1.Println("Couldn't sync keys")
os.Exit(1)
} else {
msg.Println("Synced keys!")
}
}
func CombineFiles(microcode, initramfs string) (*os.File, error) {
tmpFile, err := os.CreateTemp("/var/tmp", "initramfs-")
if err != nil {
err1.Println("Cannot create temporary file", err)
return nil, err
}
one, _ := os.Open(microcode)
@ -297,12 +157,12 @@ func CombineFiles(microcode, initramfs string) (*os.File, error) {
_, err = io.Copy(tmpFile, one)
if err != nil {
return nil, PrintGenerateError(err2, "failed to append microcode file to output: %s", err)
return nil, fmt.Errorf("failed to append microcode file to output: %w", err)
}
_, err = io.Copy(tmpFile, two)
if err != nil {
return nil, PrintGenerateError(err2, "failed to append initramfs file to output: %s", err)
return nil, fmt.Errorf("failed to append initramfs file to output: %w", err)
}
return tmpFile, nil
@ -329,57 +189,13 @@ func CreateBundle(bundle Bundle) error {
bundle.Initramfs = tmpFile.Name()
}
out := GenerateBundle(&bundle)
if !out {
return PrintGenerateError(err2, "failed to generate bundle %s!", bundle.Output)
}
return nil
}
func GenerateAllBundles(sign bool) error {
msg.Println("Generating EFI bundles....")
bundles, err := ReadBundleDatabase(BundleDBPath)
out, err := GenerateBundle(&bundle)
if err != nil {
err2.Printf("Couldn't open database: %s", BundleDBPath)
return err
}
out_create := true
out_sign := true
for _, bundle := range bundles {
err := CreateBundle(*bundle)
if err != nil {
out_create = false
continue
}
if sign {
file := bundle.Output
err = SignFile(DBKey, DBCert, file, file, "")
if err != nil {
out_sign = false
}
}
}
if !out_create {
return PrintGenerateError(err1, "Error generating EFI bundles")
}
if !out_sign {
return PrintGenerateError(err1, "Error signing EFI bundles")
if !out {
return fmt.Errorf("failed to generate bundle %s", bundle.Output)
}
return nil
}
func ListBundles() {
bundles, err := ReadBundleDatabase(BundleDBPath)
if err != nil {
err2.Printf("Couldn't open database: %s", BundleDBPath)
os.Exit(1)
}
for key, bundle := range bundles {
FormatBundle(key, bundle)
}
}

78
util.go
View File

@ -1,30 +1,25 @@
package sbctl
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"log"
"io"
"os"
"path/filepath"
"strings"
)
func PrintGenerateError(logger *log.Logger, msg string, args ...interface{}) error {
msg = fmt.Sprintf(msg, args...)
logger.Println(msg)
return errors.New(msg)
}
func ChecksumFile(file string) string {
func ChecksumFile(file string) (string, error) {
hasher := sha256.New()
s, err := os.ReadFile(file)
if err != nil {
log.Fatal(err)
return "", err
}
hasher.Write(s)
return hex.EncodeToString(hasher.Sum(nil))
return hex.EncodeToString(hasher.Sum(nil)), nil
}
func ReadOrCreateFile(filePath string) ([]byte, error) {
@ -37,17 +32,11 @@ func ReadOrCreateFile(filePath string) ([]byte, error) {
// os.MkdirAll simply returns nil if the directory already exists
fileDir := filepath.Dir(filePath)
if err = os.MkdirAll(fileDir, os.ModePerm); err != nil {
if os.IsPermission(err) {
warning.Printf(rootMsg)
}
return nil, err
}
file, err := os.Create(filePath)
if err != nil {
if os.IsPermission(err) {
warning.Printf(rootMsg)
}
return nil, err
}
file.Close()
@ -56,9 +45,8 @@ func ReadOrCreateFile(filePath string) ([]byte, error) {
f = make([]byte, 0)
} else {
if os.IsPermission(err) {
warning.Printf(rootMsg)
return nil, err
}
return nil, err
}
}
@ -66,19 +54,59 @@ func ReadOrCreateFile(filePath string) ([]byte, error) {
return f, nil
}
func IsImmutable(file string) (bool, error) {
var EfivarFSFiles = []string{
"/sys/firmware/efi/efivars/PK-8be4df61-93ca-11d2-aa0d-00e098032b8c",
"/sys/firmware/efi/efivars/KEK-8be4df61-93ca-11d2-aa0d-00e098032b8c",
"/sys/firmware/efi/efivars/db-d719b2cb-3d3a-4596-a3bc-dad00e67656f",
}
var ErrImmutable = errors.New("file is immutable")
var ErrNotImmutable = errors.New("file is not immutable")
func IsImmutable(file string) error {
f, err := os.Open(file)
if errors.Is(err, os.ErrNotExist) {
return false, nil
} else if err != nil {
return false, err
if err != nil {
return err
}
attr, err := GetAttr(f)
if err != nil {
log.Fatal(err)
return err
}
if (attr & FS_IMMUTABLE_FL) != 0 {
return ErrImmutable
}
return ErrNotImmutable
}
func CheckMSDos(path string) (bool, error) {
r, err := os.Open(path)
if err != nil {
return false, err
}
defer r.Close()
// We are looking for MS-DOS executables.
// They contain "MZ" as the two first bytes
var header [2]byte
if _, err = io.ReadFull(r, header[:]); err != nil {
return false, err
}
if !bytes.Equal(header[:], []byte{0x4d, 0x5a}) {
return false, nil
}
return true, nil
}
var (
checked = make(map[string]bool)
)
func AddChecked(path string) {
normalized := strings.Join(strings.Split(path, "/")[2:], "/")
checked[normalized] = true
}
func InChecked(path string) bool {
normalized := strings.Join(strings.Split(path, "/")[2:], "/")
return checked[normalized]
}