Skip to content

Commit

Permalink
Add method to validate signature of efi file (#337)
Browse files Browse the repository at this point in the history
Co-authored-by: Dimitris Karakasilis <[email protected]>
  • Loading branch information
Itxaka and jimmykarily authored May 22, 2024
1 parent d4a9180 commit cfa2c61
Show file tree
Hide file tree
Showing 12 changed files with 370 additions and 90 deletions.
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ require (
github.com/distribution/distribution v2.8.3+incompatible
github.com/erikgeiser/promptkit v0.9.0
github.com/google/go-containerregistry v0.19.1
github.com/google/go-github/v62 v62.0.0
github.com/hashicorp/go-multierror v1.1.1
github.com/jaypipes/ghw v0.12.0
github.com/joho/godotenv v1.5.1
Expand All @@ -35,7 +34,6 @@ require (
github.com/sanity-io/litter v1.5.5
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.18.2
github.com/twpayne/go-vfs/v5 v5.0.4 // v5 requires go1.20
github.com/urfave/cli/v2 v2.27.2
golang.org/x/net v0.25.0
golang.org/x/oauth2 v0.20.0
Expand All @@ -45,6 +43,9 @@ require (
)

require (
github.com/foxboron/go-uefi v0.0.0-20240128152106-48be911532c2
github.com/google/go-github/v40 v40.0.0
github.com/saferwall/pe v1.5.3
github.com/google/go-github/v62 v62.0.0
github.com/twpayne/go-vfs/v4 v4.3.0
)
Expand All @@ -59,6 +60,7 @@ require (
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/StackExchange/wmi v1.2.1 // indirect
github.com/alecthomas/assert/v2 v2.6.0 // indirect
github.com/anatol/devmapper.go v0.0.0-20220907161421-ba4de5fc0fd1 // indirect
github.com/anatol/luks.go v0.0.0-20230423170605-fb3724ed7db7 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
Expand Down Expand Up @@ -90,9 +92,9 @@ require (
github.com/docker/docker-credential-helpers v0.7.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/eliukblau/pixterm v1.3.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/foxboron/go-uefi v0.0.0-20240128152106-48be911532c2 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gen2brain/shm v0.0.0-20200228170931-49f9650110c5 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
Expand All @@ -116,7 +118,6 @@ require (
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/gojq v0.12.15 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/jaypipes/pcidb v1.0.0 // indirect
Expand Down Expand Up @@ -171,6 +172,7 @@ require (
github.com/samber/lo v1.37.0 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shirou/gopsutil/v3 v3.23.7 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
Expand All @@ -181,7 +183,6 @@ require (
github.com/spectrocloud-labs/herd v0.4.2 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggest/jsonschema-go v0.3.62 // indirect
Expand Down Expand Up @@ -225,7 +226,6 @@ require (
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
howett.net/plist v1.0.0 // indirect
k8s.io/apimachinery v0.26.2 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
pault.ag/go/modprobe v0.1.2 // indirect
Expand Down
126 changes: 44 additions & 82 deletions go.sum

Large diffs are not rendered by default.

149 changes: 147 additions & 2 deletions pkg/uki/common.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
package uki

import (
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"strings"

sdkTypes "github.com/kairos-io/kairos-sdk/types"

"github.com/edsrzf/mmap-go"
"github.com/foxboron/go-uefi/efi"
"github.com/foxboron/go-uefi/efi/pecoff"
"github.com/foxboron/go-uefi/efi/pkcs7"
"github.com/foxboron/go-uefi/efi/signature"
"github.com/kairos-io/kairos-agent/v2/pkg/constants"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
"github.com/kairos-io/kairos-sdk/signatures"
sdkTypes "github.com/kairos-io/kairos-sdk/types"
sdkutils "github.com/kairos-io/kairos-sdk/utils"
peparser "github.com/saferwall/pe"
"github.com/sanity-io/litter"
)

Expand Down Expand Up @@ -153,3 +161,140 @@ func copyFile(src, dst string) error {

return destinationFile.Close()
}

// checkArtifactSignatureIsValid checks that a given efi artifact is signed properly with a signature that would allow it to
// boot correctly in the current node if secureboot is enabled
func checkArtifactSignatureIsValid(fs v1.FS, artifact string, logger sdkTypes.KairosLogger) error {
var err error
logger.Logger.Info().Str("what", artifact).Msg("Checking artifact for valid signature")
info, err := fs.Stat(artifact)
if errors.Is(err, os.ErrNotExist) {
logger.Warnf("%s does not exist", artifact)
return fmt.Errorf("%s does not exist", artifact)
} else if errors.Is(err, os.ErrPermission) {
logger.Warnf("%s permission denied. Can't read file", artifact)
return fmt.Errorf("%s permission denied. Can't read file", artifact)
} else if err != nil {
return err
}
if info.Size() == 0 {
logger.Warnf("%s file is empty denied", artifact)
return fmt.Errorf("%s file has zero size", artifact)
}
logger.Logger.Debug().Str("what", artifact).Msg("Reading artifact")

// MMAP the file, seems to save memory rather than reading the full file
// Unfortunately we have to do some type conversion to keep using the v1.Fs
f, err := fs.Open(artifact)
defer f.Close()
if err != nil {
return err
}
// type conversion, ugh
fOS := f.(*os.File)
data, err := mmap.Map(fOS, mmap.RDONLY, 0)
defer data.Unmap()
if err != nil {
return err
}

// Get sha256 of the artifact
// Note that this is a PEFile, so it's a bit different from a normal file as there are some sections that need to be
// excluded when calculating the sha
logger.Logger.Debug().Str("what", artifact).Msg("Parsing PE artifact")
file, _ := peparser.NewBytes(data, &peparser.Options{Fast: true})
err = file.Parse()
if err != nil {
logger.Logger.Error().Err(err).Msg("parsing PE file for hash")
return err
}

logger.Logger.Debug().Str("what", artifact).Msg("Checking if its an EFI file")
// Check for proper header in the efi file
if file.DOSHeader.Magic != peparser.ImageDOSZMSignature && file.DOSHeader.Magic != peparser.ImageDOSSignature {
logger.Error(fmt.Errorf("no pe file header: %d", file.DOSHeader.Magic))
return fmt.Errorf("no pe file header: %d", file.DOSHeader.Magic)
}

// Get hash to compare in dbx if we have hashes
hashArtifact := hex.EncodeToString(file.Authentihash())

logger.Logger.Debug().Str("what", artifact).Msg("Getting DB certs")
// We need to read the current db database to have the proper certs to check against
db, err := efi.Getdb()
if err != nil {
logger.Logger.Error().Err(err).Msg("Getting DB certs")
return err
}

dbCerts := signatures.ExtractCertsFromSignatureDatabase(db)

logger.Logger.Debug().Str("what", artifact).Msg("Getting signatures from artifact")
// Get signatures from the artifact
sigs, err := pecoff.GetSignatures(data)
if err != nil {
return fmt.Errorf("%s: %w", artifact, err)
}
if len(sigs) == 0 {
return fmt.Errorf("no signatures in the file %s", artifact)
}

logger.Logger.Debug().Str("what", artifact).Msg("Getting DBX certs")
dbx, err := efi.Getdbx()
if err != nil {
logger.Logger.Error().Err(err).Msg("getting DBX certs")
return err
}

// First check the dbx database as it has precedence, on match, return immediately
for _, k := range *dbx {
switch k.SignatureType {
case signature.CERT_SHA256_GUID: // SHA256 hash
// Compare it against the dbx
for _, k1 := range k.Signatures {
shaSign := hex.EncodeToString(k1.Data)
logger.Logger.Debug().Str("artifact", string(hashArtifact)).Str("signature", shaSign).Msg("Comparing hashes")
if hashArtifact == shaSign {
return fmt.Errorf("hash appears on DBX: %s", hashArtifact)
}

}
case signature.CERT_X509_GUID: // Certificate
var result []*x509.Certificate
for _, k1 := range k.Signatures {
certificates, err := x509.ParseCertificates(k1.Data)
if err != nil {
continue
}
result = append(result, certificates...)
}
for _, sig := range sigs {
for _, cert := range result {
logger.Logger.Debug().Str("what", artifact).Str("subject", cert.Subject.CommonName).Msg("checking signature")
ok, _ := pkcs7.VerifySignature(cert, sig.Certificate)
// If cert matches then it means its blacklisted so return error
if ok {
return fmt.Errorf("artifact is signed with a blacklisted cert")
}

}
}
default:
logger.Logger.Debug().Str("what", artifact).Str("cert type", string(signature.ValidEFISignatureSchemes[k.SignatureType])).Msg("not supported type of cert")
}
}

// Now check against the DB to see if its allowed
for _, sig := range sigs {
for _, cert := range dbCerts {
logger.Logger.Debug().Str("what", artifact).Str("subject", cert.Subject.CommonName).Msg("checking signature")
ok, _ := pkcs7.VerifySignature(cert, sig.Certificate)
if ok {
logger.Logger.Info().Str("what", artifact).Str("subject", cert.Subject.CommonName).Msg("verified")
return nil
}
}
}
// If we reach this point, we need to fail as we haven't matched anything, so default is to fail
return fmt.Errorf("could not find a signature in EFIVars DB that matches the artifact")
}
147 changes: 147 additions & 0 deletions pkg/uki/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package uki

import (
"bytes"
"fmt"
"github.com/foxboron/go-uefi/efi/attributes"
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
fsutils "github.com/kairos-io/kairos-agent/v2/pkg/utils/fs"
sdkTypes "github.com/kairos-io/kairos-sdk/types"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/twpayne/go-vfs/v4/vfst"
"os"
"path/filepath"
)

// This tests require prepared files to work unless we prepare them here in the test which is a bit costly
// 2 efi files, one signed and one unsigned
// fbx64.efi -> unsigned
// fbx64.signed.efi -> signed
// 2 db files extracted from a real db, one with the proper certificate that signed the efi file one without it
// db-wrong -> extracted db, contains signatures but they don't have the signature that signed the efi file
// db -> extracted db, contains signatures, including the one that signed the efi file
// 2 dbx files extracted from a real db, one that has nothing on it and one that has the efi file blacklisted
// TODO: have just 1 efi file and generate all of this on the fly:
// sign it when needed
// create the db/dbx efivars on the fly with the proper signatures
// Use efi.EfivarFs for this
var _ = Describe("Uki utils", Label("uki", "utils"), func() {
var fs v1.FS
var logger sdkTypes.KairosLogger
var memLog *bytes.Buffer
var cleanup func()

BeforeEach(func() {
var err error
fs, cleanup, err = vfst.NewTestFS(map[string]interface{}{})
Expect(err).Should(BeNil())
// create fs with proper setup
err = fsutils.MkdirAll(fs, "/sys/firmware/efi/efivars", os.ModeDir|os.ModePerm)
file, err := os.ReadFile("tests/fbx64.efi")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efitest.efi", file, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
file, err = os.ReadFile("tests/fbx64.signed.efi")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile("/efitest.signed.efi", file, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
memLog = &bytes.Buffer{}
logger = sdkTypes.NewBufferLogger(memLog)
// Override the Efivars location to point to our fake ones
// so the go-uefi lib looks in there
fakeEfivars, err := fs.RawPath("/sys/firmware/efi/efivars")
Expect(err).ToNot(HaveOccurred())
attributes.Efivars = fakeEfivars
})
AfterEach(func() {
cleanup()
})
It("Fails if it cant find the file to check", func() {
err := checkArtifactSignatureIsValid(fs, "/notexists.efi", logger)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("does not exist"))
})

It("Fails if the file is empty", func() {
// File needs to not be empty for the parser to try to parse it
err := fs.WriteFile("/nonefi.file", []byte(""), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = checkArtifactSignatureIsValid(fs, "/nonefi.file", logger)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("has zero size"))
})

It("Fails if the file is not a valid efi file", func() {
// File needs to not be empty for the parser to try to parse it
err := fs.WriteFile("/nonefi.file", []byte("asdkljhfjklahsdfjk,hbasdfjkhas"), os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = checkArtifactSignatureIsValid(fs, "/nonefi.file", logger)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("not a PE file"))
})

It("Fails if the file to check has no signatures", func() {
dbFile := fmt.Sprintf("db-%s", attributes.EFI_IMAGE_SECURITY_DATABASE_GUID.Format())
file, err := os.ReadFile("tests/db")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile(filepath.Join("/sys/firmware/efi/efivars", dbFile), file, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = checkArtifactSignatureIsValid(fs, "/efitest.efi", logger)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("no signatures in the file"))
})

It("fails when signature doesn't match the db", func() {
dbFile := fmt.Sprintf("db-%s", attributes.EFI_IMAGE_SECURITY_DATABASE_GUID.Format())
file, err := os.ReadFile("tests/db-wrong")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile(filepath.Join("/sys/firmware/efi/efivars", dbFile), file, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = checkArtifactSignatureIsValid(fs, "/efitest.signed.efi", logger)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("could not find a signature in EFIVars DB that matches the artifact"))
})

It("matches the DB", func() {
dbFile := fmt.Sprintf("db-%s", attributes.EFI_IMAGE_SECURITY_DATABASE_GUID.Format())
file, err := os.ReadFile("tests/db")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile(filepath.Join("/sys/firmware/efi/efivars", dbFile), file, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = checkArtifactSignatureIsValid(fs, "/efitest.signed.efi", logger)
Expect(err).ToNot(HaveOccurred())
})

It("doesn't fail when it matches the DB and not DBX", func() {
dbFile := fmt.Sprintf("db-%s", attributes.EFI_IMAGE_SECURITY_DATABASE_GUID.Format())
dbxFile := fmt.Sprintf("dbx-%s", attributes.EFI_IMAGE_SECURITY_DATABASE_GUID.Format())
file, err := os.ReadFile("tests/db")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile(filepath.Join("/sys/firmware/efi/efivars", dbFile), file, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
file, err = os.ReadFile("tests/dbx-wrong")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile(filepath.Join("/sys/firmware/efi/efivars", dbxFile), file, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = checkArtifactSignatureIsValid(fs, "/efitest.signed.efi", logger)
Expect(err).ToNot(HaveOccurred())
})

It("Fails if signature is in DBX, even if its also on DB", func() {
dbFile := fmt.Sprintf("db-%s", attributes.EFI_IMAGE_SECURITY_DATABASE_GUID.Format())
dbxFile := fmt.Sprintf("dbx-%s", attributes.EFI_IMAGE_SECURITY_DATABASE_GUID.Format())
file, err := os.ReadFile("tests/db")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile(filepath.Join("/sys/firmware/efi/efivars", dbFile), file, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
file, err = os.ReadFile("tests/dbx")
Expect(err).ToNot(HaveOccurred())
err = fs.WriteFile(filepath.Join("/sys/firmware/efi/efivars", dbxFile), file, os.ModePerm)
Expect(err).ToNot(HaveOccurred())
err = checkArtifactSignatureIsValid(fs, "/efitest.signed.efi", logger)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("hash appears on DBX"))
})

})
Binary file added pkg/uki/tests/db
Binary file not shown.
Binary file added pkg/uki/tests/db-wrong
Binary file not shown.
Binary file added pkg/uki/tests/dbx
Binary file not shown.
Binary file added pkg/uki/tests/dbx-wrong
Binary file not shown.
Binary file added pkg/uki/tests/fbx64.efi
Binary file not shown.
Binary file added pkg/uki/tests/fbx64.signed.efi
Binary file not shown.
Loading

0 comments on commit cfa2c61

Please sign in to comment.