diff --git a/bundles.go b/bundles.go index dd1ca97..f7157c9 100644 --- a/bundles.go +++ b/bundles.go @@ -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 { @@ -73,8 +88,8 @@ func NewBundle() *Bundle { Output: "", IntelMicrocode: "", AMDMicrocode: "", - KernelImage: filepath.Join(esp, "vmlinuz-linux"), - Initramfs: filepath.Join(esp, "initramfs-linux.img"), + KernelImage: "/boot/vmlinuz-linux", + Initramfs: "/boot/initramfs-linux.img", Cmdline: "/etc/kernel/cmdline", Splash: "", OSRelease: "/usr/lib/os-release", @@ -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 } diff --git a/cmd/sbctl/bundle.go b/cmd/sbctl/bundle.go new file mode 100644 index 0000000..b509fc8 --- /dev/null +++ b/cmd/sbctl/bundle.go @@ -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, + }) +} diff --git a/cmd/sbctl/completions.go b/cmd/sbctl/completions.go new file mode 100644 index 0000000..fdcb774 --- /dev/null +++ b/cmd/sbctl/completions.go @@ -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, + }) +} diff --git a/cmd/sbctl/create-keys.go b/cmd/sbctl/create-keys.go new file mode 100644 index 0000000..2631c08 --- /dev/null +++ b/cmd/sbctl/create-keys.go @@ -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, + }) +} diff --git a/cmd/sbctl/enroll-keys.go b/cmd/sbctl/enroll-keys.go new file mode 100644 index 0000000..459b096 --- /dev/null +++ b/cmd/sbctl/enroll-keys.go @@ -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, + }) +} diff --git a/cmd/sbctl/generate-bundles.go b/cmd/sbctl/generate-bundles.go new file mode 100644 index 0000000..4ae0bd2 --- /dev/null +++ b/cmd/sbctl/generate-bundles.go @@ -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, + }) +} diff --git a/cmd/sbctl/list-bundles.go b/cmd/sbctl/list-bundles.go new file mode 100644 index 0000000..fc80e11 --- /dev/null +++ b/cmd/sbctl/list-bundles.go @@ -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, + }) +} diff --git a/cmd/sbctl/list-files.go b/cmd/sbctl/list-files.go new file mode 100644 index 0000000..3d46399 --- /dev/null +++ b/cmd/sbctl/list-files.go @@ -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, + }) +} diff --git a/cmd/sbctl/main.go b/cmd/sbctl/main.go old mode 100755 new mode 100644 index 215e85d..d427f46 --- a/cmd/sbctl/main.go +++ b/cmd/sbctl/main.go @@ -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() - }, - } -} - -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 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) - }, - } +type cliCommand struct { + Cmd *cobra.Command } -func statusCmd() *cobra.Command { - return &cobra.Command{ - Use: "status", - Short: "Show current boot status", - Run: func(cmd *cobra.Command, args []string) { - sbctl.CheckStatus() - }, +var ( + cmdOptions = CmdOptions{} + CliCommands = []cliCommand{} + ErrSilent = errors.New("SilentErr") + rootCmd = &cobra.Command{ + Use: "sbctl", + Short: "Secure Boot Key Manager", + SilenceUsage: true, + SilenceErrors: true, } -} - -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", filepath.Join(esp, "vmlinuz-linux"), "Kernel image location") - f.StringVarP(&cmdline, "cmdline", "c", "/etc/kernel/cmdline", "Cmdline location") - f.StringVarP(&initramfs, "initramfs", "f", filepath.Join(esp, "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) - } +func baseFlags(cmd *cobra.Command) { + flags := cmd.PersistentFlags() + flags.BoolVar(&cmdOptions.JsonOutput, "json", false, "Output as json") - 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) - }, + cmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { + if cmdOptions.JsonOutput { + logging.PrintOff() + } } } -func completionBashCmd() *cobra.Command { - var completionCmd = &cobra.Command{ - Use: "bash", - Hidden: true, - Run: func(cmd *cobra.Command, args []string) { - rootCmd.GenBashCompletion(os.Stdout) - }, +func JsonOut(v interface{}) error { + b, err := json.MarshalIndent(v, "", " ") + if err != nil { + return fmt.Errorf("could not marshal json: %w", err) } - return completionCmd + fmt.Fprint(os.Stdout, string(b)) + return nil } -func completionZshCmd() *cobra.Command { - var completionCmd = &cobra.Command{ - Use: "zsh", - Hidden: true, - Run: func(cmd *cobra.Command, args []string) { - rootCmd.GenZshCompletion(os.Stdout) - }, +func main() { + for _, cmd := range CliCommands { + rootCmd.AddCommand(cmd.Cmd) } - 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 -} + baseFlags(rootCmd) -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()) + // 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() } diff --git a/cmd/sbctl/remove-bundle.go b/cmd/sbctl/remove-bundle.go new file mode 100644 index 0000000..1977f31 --- /dev/null +++ b/cmd/sbctl/remove-bundle.go @@ -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, + }) +} diff --git a/cmd/sbctl/remove-files.go b/cmd/sbctl/remove-files.go new file mode 100644 index 0000000..2367ebc --- /dev/null +++ b/cmd/sbctl/remove-files.go @@ -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, + }) +} diff --git a/cmd/sbctl/sign-all.go b/cmd/sbctl/sign-all.go new file mode 100644 index 0000000..18297c0 --- /dev/null +++ b/cmd/sbctl/sign-all.go @@ -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, + }) +} diff --git a/cmd/sbctl/sign.go b/cmd/sbctl/sign.go new file mode 100644 index 0000000..7e48ca2 --- /dev/null +++ b/cmd/sbctl/sign.go @@ -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, + }) +} diff --git a/cmd/sbctl/status.go b/cmd/sbctl/status.go new file mode 100644 index 0000000..6a5fda2 --- /dev/null +++ b/cmd/sbctl/status.go @@ -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, + }) +} diff --git a/cmd/sbctl/verify.go b/cmd/sbctl/verify.go new file mode 100644 index 0000000..c3cd6f5 --- /dev/null +++ b/cmd/sbctl/verify.go @@ -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, + }) +} diff --git a/cmds.go b/cmds.go deleted file mode 100644 index 681ced5..0000000 --- a/cmds.go +++ /dev/null @@ -1 +0,0 @@ -package sbctl diff --git a/database.go b/database.go index b382da8..530702d 100644 --- a/database.go +++ b/database.go @@ -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 } diff --git a/docs/sbctl.8.txt b/docs/sbctl.8.txt index 33385ae..4f97495 100644 --- a/docs/sbctl.8.txt +++ b/docs/sbctl.8.txt @@ -144,6 +144,16 @@ All commands that take path arguments convert them into absolute paths when saving them to the database. +Environment variables +--------------------- + +**SYSTEMD_ESP_PATH**, **ESP_PATH**:: + Defines the EFI system partition (ESP) location. This overrides the + behaviour from **sbctl** where we query for the correct partition with + **lsblk**. No checks are performed on this path and can be usefull for testing + purposes. + + Files ---- **/usr/share/secureboot**:: diff --git a/go.mod b/go.mod index c236a89..db6d678 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 27bdcf5..7c84549 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/guid.go b/guid.go new file mode 100644 index 0000000..a8ab9c3 --- /dev/null +++ b/guid.go @@ -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 +} diff --git a/keys.go b/keys.go index f187923..b00ad27 100644 --- a/keys.go +++ b/keys.go @@ -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() +func InitializeSecureBootKeys(output string) error { + os.MkdirAll(output, os.ModePerm) + uuid, err := CreateGUID(output) 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 err } - return uuid -} - -func InitializeSecureBootKeys(output string) { - os.MkdirAll(output, os.ModePerm) - uuid := CreateGUID(output) + 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 } diff --git a/log.go b/log.go deleted file mode 100644 index 6d58689..0000000 --- a/log.go +++ /dev/null @@ -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) -} diff --git a/logging/logging.go b/logging/logging.go new file mode 100644 index 0000000..b225f47 --- /dev/null +++ b/logging/logging.go @@ -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() +} diff --git a/sbctl.go b/sbctl.go index 7b32cfe..070ca45 100644 --- a/sbctl.go +++ b/sbctl.go @@ -1,118 +1,94 @@ package sbctl import ( - "bytes" - "errors" + "encoding/json" + "fmt" "io" "log" "os" "os/exec" "path/filepath" - "strings" - - "github.com/foxboron/go-uefi/efi/attributes" ) // Functions that doesn't fit anywhere else -// Veryvery simple check +type LsblkEntry struct { + Parttype string `json:"parttype"` + Mountpoint string `json:"mountpoint"` + Pttype string `json:"pttype"` + Fstype string `json:"fstype"` +} + +type LsblkRoot struct { + Blockdevices []LsblkEntry `json:"blockdevices"` +} + +// Slightly more advanced check func GetESP() string { - if _, err := os.Stat("/efi"); !os.IsNotExist(err) { - return "/efi" - } - out, err := exec.Command("lsblk", "-o", "PARTTYPE,MOUNTPOINT").Output() - if err != nil { - log.Fatal(err) - } - data := string(out) - for _, lines := range strings.Split(data, "\n") { - if len(lines) < 1 { - continue - } - l := strings.Split(lines, " ") - if len(l) != 2 { - continue - } - if l[0] == "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" { - return l[1] + + for _, env := range []string{"SYSTEMD_ESP_PATH", "ESP_PATH"} { + envEspPath, found := os.LookupEnv(env) + if found { + return envEspPath } } - return "" -} -func VerifyESP() error { - espPath := GetESP() - files, err := ReadFileDatabase(DBPath) + out, err := exec.Command( + "lsblk", + "--json", + "--output", "PARTTYPE,MOUNTPOINT,PTTYPE,FSTYPE").Output() 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) + log.Panic(err) } - // 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) + var lsblkRoot LsblkRoot + json.Unmarshal(out, &lsblkRoot) + + var pathBootEntry *LsblkEntry + var pathBootEfiEntry *LsblkEntry + var pathEfiEntry *LsblkEntry + + for _, lsblkEntry := range lsblkRoot.Blockdevices { + switch lsblkEntry.Mountpoint { + case "/boot": + pathBootEntry = new(LsblkEntry) + *pathBootEntry = lsblkEntry + case "/boot/efi": + pathBootEfiEntry = new(LsblkEntry) + *pathBootEfiEntry = lsblkEntry + case "/efi": + pathEfiEntry = new(LsblkEntry) + *pathEfiEntry = lsblkEntry } } - 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 + for _, entryToCheck := range []*LsblkEntry{pathEfiEntry, pathBootEntry, pathBootEfiEntry} { + if entryToCheck == nil { + continue } - // Don't check files we have checked - normalized := strings.Join(strings.Split(path, "/")[2:], "/") - if ok := checked[normalized]; ok { - return nil + if entryToCheck.Pttype != "gpt" { + continue } - 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 entryToCheck.Fstype != "vfat" { + continue } - if VerifyFile(DBCert, path) { - msg2.Printf("%s is signed\n", path) - } else { - warning2.Printf("%s is not signed\n", path) + if entryToCheck.Parttype != "c12a7328-f81f-11d2-ba4b-00a0c93ec93b" { + continue } - return nil - }) - if err != nil { - log.Println(err) + + return entryToCheck.Mountpoint } - return nil + return "" } func Sign(file, output string, enroll bool) error { file, err := filepath.Abs(file) if err != nil { - log.Fatal(err) + return err } if output == "" { @@ -120,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 } } @@ -128,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) @@ -137,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 @@ -150,94 +130,23 @@ func Sign(file, output string, enroll bool) error { } if enroll { - checksum := ChecksumFile(file) - files[file] = &SigningEntry{File: file, OutputFile: output, Checksum: checksum} - WriteFileDatabase(DBPath, files) - } - - 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) + checksum, err := ChecksumFile(file) if err != nil { - err1.Printf("Couldn't read file: %s\n", file) - os.Exit(1) + return err } - if b { - err1.Printf("File is immutable: %s\n", file) - errImmuable = true + files[file] = &SigningEntry{File: file, OutputFile: output, Checksum: checksum} + if err := WriteFileDatabase(DBPath, files); err != nil { + return err } } - 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!") - } + + return err } 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) @@ -248,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 @@ -280,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) - } -} diff --git a/util.go b/util.go index ab9cace..ebc77a0 100644 --- a/util.go +++ b/util.go @@ -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] +}