mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-10-21 19:44:39 +00:00
Use signature from sdk (#361)
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
})
|
||||
|
||||
})
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user