Skip to content

Commit

Permalink
capture BIOS, extract logo and hash it
Browse files Browse the repository at this point in the history
  • Loading branch information
DoctorVin committed Dec 21, 2023
1 parent e89eb63 commit bc75de9
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 65 deletions.
3 changes: 2 additions & 1 deletion actions/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ type TPMCollector interface {
// FirmwareChecksumCollector defines an interface to collect firmware checksums
type FirmwareChecksumCollector interface {
UtilAttributeGetter
BIOSLogoChecksum(ctx context.Context) (sha256 [32]byte, err error)
// return the sha-256 of the BIOS logo as a string, or the associated error
BIOSLogoChecksum(ctx context.Context) (string, error)
}

// UEFIVarsCollector defines an interface to collect EFI variables
Expand Down
21 changes: 12 additions & 9 deletions actions/inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package actions

import (
"context"
"fmt"
"runtime/debug"
"strings"

Expand Down Expand Up @@ -145,7 +144,10 @@ func NewInventoryCollectorAction(options ...Option) *InventoryCollectorAction {
utils.NewHdparmCmd(a.trace),
utils.NewNvmeCmd(a.trace),
},
FirmwareChecksumCollector: firmware.NewChecksumCollector(a.trace),
FirmwareChecksumCollector: firmware.NewChecksumCollector(
firmware.MakeOutputPath(),
firmware.TraceExecution(a.trace),
),
// implement uefi vars collector and plug in here
// UEFIVarsCollector: ,
}
Expand Down Expand Up @@ -686,24 +688,25 @@ func (a *InventoryCollectorAction) CollectFirmwareChecksums(ctx context.Context)
return nil
}

// skip collector if its been disabled
// skip collector if we explicitly disable anything related to firmware checksumming.
collectorKind, _, _ := a.collectors.FirmwareChecksumCollector.Attributes()
if slices.Contains(a.disabledCollectorUtilities, collectorKind) {
if slices.Contains(a.disabledCollectorUtilities, collectorKind) ||
slices.Contains(a.disabledCollectorUtilities, firmware.FirmwareDumpUtility) ||
slices.Contains(a.disabledCollectorUtilities, firmware.UEFIParserUtility) {
return nil
}

sum, err := a.collectors.BIOSLogoChecksum(ctx)
sumStr, err := a.collectors.FirmwareChecksumCollector.BIOSLogoChecksum(ctx)
if err != nil {
return err
}

if len(sum) == 0 || a.device.BIOS == nil {
if a.device.BIOS == nil {
// XXX: how did we get here?
return nil
}

// not sure if this is the ideal way to cover the byte array
// maybe the interface method should return a checksum string instead?
a.device.BIOS.Metadata["bios-checksum"] = fmt.Sprintf("%s", sum)
a.device.BIOS.Metadata["bios-logo-checksum"] = sumStr

return nil
}
Expand Down
192 changes: 192 additions & 0 deletions firmware/bios_checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
//nolint:wsl // it's useless
package firmware

import (
"context"
"crypto/sha256"
"fmt"
"io"
"io/fs"
"os"
"strings"

"github.com/metal-toolbox/ironlib/model"
"github.com/metal-toolbox/ironlib/utils"
"github.com/pkg/errors"
)

const FirmwareDumpUtility model.CollectorUtility = "flashrom"
const UEFIParserUtility model.CollectorUtility = "uefi-firmware-parser"
const ChecksumComposedCollector model.CollectorUtility = "checksum-collector"
const hashPrefix = "SHA256"
const uefiDefaultBMPLogoGUID = "7bb28b99-61bb-11d5-9a5d-0090273fc14d"

var defaultOutputPath = "/tmp/bios_checksum"
var defaultBIOSImgName = "bios_img.bin"
var expectedLogoSuffix = fmt.Sprintf("file-%s/section0/section0.raw", uefiDefaultBMPLogoGUID)

var directoryPermissions fs.FileMode = 0o750
var errNoLogo = errors.New("no logo found")

// ChecksumCollector implements the FirmwareChecksumCollector interface
type ChecksumCollector struct {
biosOutputPath string
biosOutputFilename string
makeOutputPath bool
trace bool
biosImgFile string // this is computed when we write out the BIOS image
extractPath string // this is computed when we extract the compressed BIOS image
}

type ChecksumOption func(*ChecksumCollector)

func WithOutputPath(p string) ChecksumOption {
return func(cc *ChecksumCollector) {
cc.biosOutputPath = p
}
}

func WithOutputFile(n string) ChecksumOption {
return func(cc *ChecksumCollector) {
cc.biosOutputFilename = n
}
}

func MakeOutputPath() ChecksumOption {
return func(cc *ChecksumCollector) {
cc.makeOutputPath = true
}
}

func TraceExecution(tf bool) ChecksumOption {
return func(cc *ChecksumCollector) {
cc.trace = tf
}
}

func NewChecksumCollector(opts ...ChecksumOption) *ChecksumCollector {
cc := &ChecksumCollector{
biosOutputPath: defaultOutputPath,
biosOutputFilename: defaultBIOSImgName,
}
for _, o := range opts {
o(cc)
}
return cc
}

// Attributes implements the actions.UtilAttributeGetter interface
//
// Unlike most usages, BIOS checksums rely on several discrete executables. This function returns its own name,
// and it's incumbent on the caller to check if FirmwareDumpUtility or UEFIParserUtility are denied as well.
func (*ChecksumCollector) Attributes() (utilName model.CollectorUtility, absolutePath string, err error) {
return ChecksumComposedCollector, "", nil
}

// BIOSLogoChecksum implements the FirmwareChecksumCollector interface.
func (cc *ChecksumCollector) BIOSLogoChecksum(ctx context.Context) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}

if cc.makeOutputPath {
err := os.MkdirAll(cc.biosOutputPath, directoryPermissions)
if err != nil {
return "", errors.Wrap(err, "creating firmware extraction area")
}
}
if err := cc.dumpBIOS(ctx); err != nil {
return "", errors.Wrap(err, "reading firmware binary image")
}
if err := cc.extractBIOSImage(ctx); err != nil {
return "", errors.Wrap(err, "extracting firmware binary image")
}

logoFileName, err := cc.findExtractedRawLogo(ctx)
if err != nil {
return "", errors.Wrap(err, "finding raw logo filename")
}

return cc.hashDiscoveredLogo(ctx, logoFileName)
}

func (cc *ChecksumCollector) hashDiscoveredLogo(ctx context.Context, logoFileName string) (string, error) {
handle, err := os.Open(cc.extractPath + "/" + logoFileName)
if err != nil {
return "", errors.Wrap(err, "opening logo file")
}
defer handle.Close()

hasher := sha256.New()
if _, err = io.Copy(hasher, handle); err != nil {
return "", errors.Wrap(err, "copying logo data to hasher")
}

return fmt.Sprintf("%s: %x", hashPrefix, hasher.Sum(nil)), nil
}

func (cc *ChecksumCollector) dumpBIOS(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}

cc.biosImgFile = fmt.Sprintf("%s/%s", cc.biosOutputPath, cc.biosOutputFilename)

frc := utils.NewFlashromCmd(cc.trace)

return frc.WriteBIOSImage(ctx, cc.biosImgFile)
}

func (cc *ChecksumCollector) extractBIOSImage(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}

cc.extractPath = fmt.Sprintf("%s/extract", cc.biosOutputPath)

ufp := utils.NewUefiFirmwareParserCmd(cc.trace)

return ufp.ExtractLogo(ctx, cc.extractPath, cc.biosImgFile)
}

func (cc *ChecksumCollector) findExtractedRawLogo(ctx context.Context) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}

var filename string

dirHandle := os.DirFS(cc.extractPath)
err := fs.WalkDir(dirHandle, ".", func(path string, _ fs.DirEntry, err error) error {
if err != nil {
return err
}
if cc.trace {
fmt.Printf("dir-walk: %s\n", path)
}
if strings.HasSuffix(path, expectedLogoSuffix) {
filename = path
return fs.SkipAll
}
// XXX: Check the DirEntry for a bogus size so we don't blow up trying to hash the thing!
return nil
})

if err != nil {
return "", errors.Wrap(err, "walking the extract directory")
}

if filename == "" {
return "", errNoLogo
}

return filename, nil
}
77 changes: 77 additions & 0 deletions firmware/bios_checksum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//nolint:all
package firmware

import (
"context"
"os"
"testing"

"github.com/stretchr/testify/require"
)

func TestFindExtractedRawLogo(t *testing.T) {
t.Parallel()
t.Run("context expired", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.TODO())
cancel()
cc := &ChecksumCollector{
extractPath: "foo",
}
_, err := cc.findExtractedRawLogo(ctx)
require.ErrorIs(t, err, context.Canceled)
})
t.Run("not found", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
cc := &ChecksumCollector{
extractPath: t.TempDir(),
}
_, err := cc.findExtractedRawLogo(ctx)
require.ErrorIs(t, err, errNoLogo)
})
t.Run("found it", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
rootDir := t.TempDir()
err := os.MkdirAll(rootDir+"/foo/bar/baz", 0o750)
require.NoError(t, err, "prerequisite dir setup 1")
err = os.MkdirAll(rootDir+"/zip/zop/zoop/file-7bb28b99-61bb-11d5-9a5d-0090273fc14d/section0", 0o750)
require.NoError(t, err, "prerequisite dir setup 2")
logo, err := os.Create(rootDir + "/zip/zop/zoop/file-7bb28b99-61bb-11d5-9a5d-0090273fc14d/section0/section0.raw")
require.NoError(t, err, "creating bogus logo")
_, err = logo.WriteString("test logo file")
require.NoError(t, err, "writing bogus logo")
logo.Close()

cc := &ChecksumCollector{
extractPath: rootDir,
}

filename, err := cc.findExtractedRawLogo(ctx)
require.NoError(t, err)
require.Equal(t, "zip/zop/zoop/file-7bb28b99-61bb-11d5-9a5d-0090273fc14d/section0/section0.raw", filename)
})
}

func TestHashDiscoveredLogo(t *testing.T) {
t.Parallel()

rootDir := t.TempDir()
err := os.MkdirAll(rootDir+"/zip/zop/zoop/file-7bb28b99-61bb-11d5-9a5d-0090273fc14d/section0", 0o750)
require.NoError(t, err, "prerequisite dir setup")
logo, err := os.Create(rootDir + "/zip/zop/zoop/file-7bb28b99-61bb-11d5-9a5d-0090273fc14d/section0/section0.raw")
require.NoError(t, err, "creating bogus logo")
_, err = logo.WriteString("test file data")
require.NoError(t, err, "writing bogus logo")
logo.Close()

cc := &ChecksumCollector{
extractPath: rootDir,
}
hash, err := cc.hashDiscoveredLogo(context.TODO(), "zip/zop/zoop/file-7bb28b99-61bb-11d5-9a5d-0090273fc14d/section0/section0.raw")
require.NoError(t, err)
require.Equal(t, "SHA256: 1be7aaf1938cc19af7d2fdeb48a11c381dff8a98d4c4b47b3b0a5044a5255c04", hash)
}
32 changes: 0 additions & 32 deletions firmware/extract.go

This file was deleted.

2 changes: 1 addition & 1 deletion utils/flashrom.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (f *Flashrom) Attributes() (utilName model.CollectorUtility, absolutePath s
}

// ExtractBIOSImage writes the BIOS image to the given file system path.
func (f *Flashrom) ExtractBIOSImage(ctx context.Context, path string) error {
func (f *Flashrom) WriteBIOSImage(ctx context.Context, path string) error {
// flashrom -p internal --ifd -i bios -r /tmp/bios_region.img
f.Executor.SetArgs([]string{"-p", "internal", "--ifd", "-i", "bios", "-r", path})

Expand Down
Loading

0 comments on commit bc75de9

Please sign in to comment.