mirror of https://github.com/ericonr/sbctl.git
Merge branch 'morten/cli'
This commit is contained in:
commit
5d528ff82a
54
bundles.go
54
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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
22
database.go
22
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
|
||||
}
|
||||
|
|
2
go.mod
2
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
|
||||
)
|
||||
|
|
10
go.sum
10
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=
|
||||
|
|
|
@ -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
156
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()
|
||||
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
65
log.go
|
@ -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)
|
||||
}
|
|
@ -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
232
sbctl.go
|
@ -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
78
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]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue