Use signature from sdk (#361)

This commit is contained in:
Itxaka
2024-06-04 16:03:39 +02:00
committed by GitHub
parent a256ef6060
commit 85bb274b38
6 changed files with 18 additions and 374 deletions

View File

@@ -1,27 +1,17 @@
package uki
import (
"bytes"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/edsrzf/mmap-go"
"github.com/foxboron/go-uefi/authenticode"
"github.com/foxboron/go-uefi/efi"
"github.com/foxboron/go-uefi/efi/signature"
"github.com/foxboron/go-uefi/pkcs7"
"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"
)
@@ -162,155 +152,3 @@ 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
binary, err := authenticode.Parse(bytes.NewReader(data))
if err != nil {
return fmt.Errorf("%s: %w", artifact, err)
}
if binary.Datadir.Size == 0 {
return fmt.Errorf("no signatures in the file %s", artifact)
}
sigs, err := binary.Signatures()
if err != nil {
return fmt.Errorf("%s: %w", artifact, err)
}
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")
p, err := pkcs7.ParsePKCS7(sig.Certificate)
if err != nil {
logger.Logger.Info().Str("error", err.Error()).Msg("parsing signature")
return err
}
ok, _ := p.Verify(cert)
// 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")
p, err := pkcs7.ParsePKCS7(sig.Certificate)
if err != nil {
logger.Logger.Info().Str("error", err.Error()).Msg("parsing signature")
return err
}
ok, _ := p.Verify(cert)
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")
}

View File

@@ -1,147 +0,0 @@
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"))
})
})

View File

@@ -1,13 +0,0 @@
package uki
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestTypes(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "UKI test suite")
}

View File

@@ -12,6 +12,7 @@ import (
v1 "github.com/kairos-io/kairos-agent/v2/pkg/types/v1"
elementalUtils "github.com/kairos-io/kairos-agent/v2/pkg/utils"
events "github.com/kairos-io/kairos-sdk/bus"
"github.com/kairos-io/kairos-sdk/signatures"
"github.com/kairos-io/kairos-sdk/utils"
)
@@ -65,7 +66,7 @@ func (i *UpgradeAction) Run() (err error) {
}
// Check if the upgrade artifact contains the proper signature before copying
err = checkArtifactSignatureIsValid(i.cfg.Fs, filepath.Join(constants.UkiEfiDir, "EFI", "Kairos", fmt.Sprintf("%s.efi", UnassignedArtifactRole)), i.cfg.Logger)
err = signatures.CheckArtifactSignatureIsValid(i.cfg.Fs, filepath.Join(constants.UkiEfiDir, "EFI", "Kairos", fmt.Sprintf("%s.efi", UnassignedArtifactRole)), i.cfg.Logger)
if err != nil {
i.cfg.Logger.Logger.Error().Err(err).Msg("Checking signature before upgrading")
// Remove efi file to not occupy space and leave stuff around