From 5b59372a84df0041ccd43b4d128a3eefd9b8cccf Mon Sep 17 00:00:00 2001 From: Mauro Morales Date: Thu, 23 May 2024 16:06:30 +0200 Subject: [PATCH] Bump sdk to v0.1.8 (#349) * Bump sdk to v0.1.8 Signed-off-by: Mauro Morales * Use new signing methods Signed-off-by: Mauro Morales --------- Signed-off-by: Mauro Morales --- go.mod | 8 ++- go.sum | 10 +-- pkg/uki/common.go | 162 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 172 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index c3ab5112..94a1db85 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/jaypipes/ghw v0.12.0 github.com/joho/godotenv v1.5.1 - github.com/kairos-io/kairos-sdk v0.1.7 + github.com/kairos-io/kairos-sdk v0.1.8 github.com/kairos-io/kcrypt v0.11.1 github.com/labstack/echo/v4 v4.12.0 github.com/mitchellh/mapstructure v1.5.0 @@ -44,6 +44,11 @@ require ( k8s.io/mount-utils v0.27.4 ) +require ( + github.com/edsrzf/mmap-go v1.1.0 + github.com/foxboron/go-uefi v0.0.0-20240522180132-205d5597883a +) + require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect @@ -87,7 +92,6 @@ require ( github.com/docker/go-units v0.5.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 diff --git a/go.sum b/go.sum index 46651d86..e03c2102 100644 --- a/go.sum +++ b/go.sum @@ -138,6 +138,8 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/eliukblau/pixterm v1.3.1 h1:XeouQViH+lmzCa7sMUoK2cd7qlgHYGLIjwRKaOdJbKA= github.com/eliukblau/pixterm v1.3.1/go.mod h1:on5ueknFt+ZFVvIVVzQ7/JXwPjv5fJd8Q1Ybh7XixfU= @@ -149,8 +151,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikgeiser/promptkit v0.9.0 h1:3qL1mS/ntCrXdb8sTP/ka82CJ9kEQaGuYXNrYJkWYBc= github.com/erikgeiser/promptkit v0.9.0/go.mod h1:pU9dtogSe3Jlc2AY77EP7R4WFP/vgD4v+iImC83KsCo= -github.com/foxboron/go-uefi v0.0.0-20240128152106-48be911532c2 h1:qGlg/7H49H30Eu7nkCBA7YxNmW30ephqBf7xIxlAGuQ= -github.com/foxboron/go-uefi v0.0.0-20240128152106-48be911532c2/go.mod h1:ffg/fkDeOYicEQLoO2yFFGt00KUTYVXI+rfnc8il6vQ= +github.com/foxboron/go-uefi v0.0.0-20240522180132-205d5597883a h1:Q/VIO3QAlaF95JqVVF39udInPR76lu02yrMDInavm8Q= +github.com/foxboron/go-uefi v0.0.0-20240522180132-205d5597883a/go.mod h1:ffg/fkDeOYicEQLoO2yFFGt00KUTYVXI+rfnc8il6vQ= github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -286,8 +288,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 h1:G+9t9cEtnC9jFiTxyptEKuNIAbiN5ZCQzX2a74lj3xg= github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004/go.mod h1:KmHnJWQrgEvbuy0vcvj00gtMqbvNn1L+3YUZLK/B92c= -github.com/kairos-io/kairos-sdk v0.1.7 h1:h2H1/sG4+4xEPh0zMFFtl4yEgzrXI8IDdDiQZe4ib6g= -github.com/kairos-io/kairos-sdk v0.1.7/go.mod h1:sR1X4B3F1nkaECQ1vdsJ78OIkfLfyB22/aIpdRQJ/Mo= +github.com/kairos-io/kairos-sdk v0.1.8 h1:TKigA+3Nmzn/NLztbLVBLacpx0cK1oJl1AoZarohU98= +github.com/kairos-io/kairos-sdk v0.1.8/go.mod h1:asSOyJanH10Cnxl9zx5RzyYNMhEworaiMh/7uRnS4GA= github.com/kairos-io/kcrypt v0.11.1 h1:azIX1QI5dEzVLvgftNleCY4AyklhTXewCoi4eTC7jhU= github.com/kairos-io/kcrypt v0.11.1/go.mod h1:Gz1izzOWwbnJwtq+XqiZQ8cPktWcDIKw03YM1PWAk4c= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= diff --git a/pkg/uki/common.go b/pkg/uki/common.go index ed49c844..809cd520 100644 --- a/pkg/uki/common.go +++ b/pkg/uki/common.go @@ -1,14 +1,20 @@ package uki import ( + "bytes" + "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/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" @@ -153,3 +159,155 @@ 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") +}