diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml index 1576786..ed6418f 100644 --- a/.github/workflows/unit-tests.yaml +++ b/.github/workflows/unit-tests.yaml @@ -18,6 +18,8 @@ jobs: with: fetch-depth: 0 - uses: earthly/actions-setup@v1 + with: + version: 0.8.12 - name: Run Lint checks run: earthly +golint unit-tests: @@ -32,6 +34,8 @@ jobs: with: fetch-depth: 0 - uses: earthly/actions-setup@v1 + with: + version: 0.8.12 - name: Build run: earthly +build --GO_VERSION=${{ matrix.go-version }} - name: Run tests diff --git a/internal/constants/constants.go b/internal/constants/constants.go index baa0424..a0c2319 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -1,6 +1,8 @@ package constants -import "errors" +import ( + "errors" +) func DefaultRWPaths() []string { // Default RW_PATHS to mount if not override by the cos-layout.env file @@ -22,6 +24,7 @@ func GenericKernelDrivers() []string { "ata_piix", "cdrom", "dm_mod", + "dm-verity", "e1000", "e1000e", "ehci_hcd", @@ -96,6 +99,8 @@ const ( OpKcryptUpgrade = "upgrade-kcrypt" OpUkiKcrypt = "uki-unlock" OpUkiMountLivecd = "mount-livecd" + OpUkiExtractCerts = "extract-certs" + OpUkiCopySysExtensions = "copy-sysextensions" UkiLivecdMountPoint = "/run/initramfs/live" UkiIsoBaseTree = "/run/rootfsbase" UkiIsoBootImage = "efiboot.img" @@ -109,4 +114,8 @@ const ( PathAppend = "/usr/bin:/usr/sbin:/bin:/sbin" PATH = "PATH" DefaultPCR = 11 + SourceSysExtDir = "/.extra/sysext/" + DestSysExtDir = "/run/extensions" + VerityCertDir = "/run/verity.d/" + SysextDefaultPolicy = "--image-policy=\"root=verity+signed+absent:usr=verity+signed+absent\"" ) diff --git a/internal/utils/common.go b/internal/utils/common.go index 7bc9529..ecc4fe6 100644 --- a/internal/utils/common.go +++ b/internal/utils/common.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "errors" "fmt" + "io" "os" "os/exec" "path/filepath" @@ -298,3 +299,32 @@ func PCRExtend(pcr int, data []byte) error { return nil } + +// Copy copies src to dst like the cp command. +func Copy(src, dst string) error { + if dst == src { + return os.ErrInvalid + } + + srcF, err := os.Open(src) + if err != nil { + return err + } + defer srcF.Close() + + info, err := srcF.Stat() + if err != nil { + return err + } + + dstF, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer dstF.Close() + + if _, err := io.Copy(dstF, srcF); err != nil { + return err + } + return nil +} diff --git a/pkg/dag/dag_uki_boot.go b/pkg/dag/dag_uki_boot.go index 6ce45c6..cb6ea1a 100644 --- a/pkg/dag/dag_uki_boot.go +++ b/pkg/dag/dag_uki_boot.go @@ -32,6 +32,9 @@ func RegisterUKI(s *state.State, g *herd.Graph) error { // Mount ESP partition under efi if it exists s.LogIfError(s.UKIMountESPPartition(g, herd.WithDeps(cnst.OpSentinel, cnst.OpUkiUdev)), "mount ESP partition") + // Extract EFI public certs for sysextensions validation + s.LogIfError(s.ExtractCerts(g, herd.WithDeps(cnst.OpSentinel, cnst.OpUkiUdev)), "extract certs") + // Mount cdrom under /run/initramfs/livecd and /run/rootfsbase for the efiboot.img contents s.LogIfError(s.UKIMountLiveCd(g, herd.WithDeps(cnst.OpSentinel, cnst.OpUkiUdev)), "Mount LiveCD") @@ -60,8 +63,13 @@ func RegisterUKI(s *state.State, g *herd.Graph) error { // Depends on mount binds as that usually mounts COS_PERSISTENT s.LogIfError(s.MountCustomBindsDagStep(g, herd.WeakDeps), "custom binds mount") + // Copy any sysextensions found under cnst.SourceSysExtDir into cnst.DestSysExtDir so its loaded by systemd automatically on start + // always after cnst.OpMountBind stage so we have a persistent cnst.DestSysExtDir + // Note that the loading of the extensions is done by systemd with the systemd-sysext service + s.LogIfError(s.CopySysExtensionsDagStep(g, herd.WithDeps(cnst.OpMountBind)), "copy sysextensions") + // run initramfs stage - s.LogIfError(s.InitramfsStageDagStep(g, herd.WeakDeps, herd.WithDeps(cnst.OpMountBind)), "uki initramfs") + s.LogIfError(s.InitramfsStageDagStep(g, herd.WeakDeps, herd.WithDeps(cnst.OpMountBind, cnst.OpUkiCopySysExtensions)), "uki initramfs") s.LogIfError(s.WriteFstabDagStep(g, herd.WithDeps(cnst.OpLoadConfig, cnst.OpCustomMounts, cnst.OpMountBind, cnst.OpOverlayMount), diff --git a/pkg/state/steps_uki.go b/pkg/state/steps_uki.go index 137f0f4..2a44ba6 100644 --- a/pkg/state/steps_uki.go +++ b/pkg/state/steps_uki.go @@ -3,7 +3,9 @@ package state import ( "context" "encoding/json" + "encoding/pem" "fmt" + "io/fs" "os" "path/filepath" "strings" @@ -16,6 +18,7 @@ import ( internalUtils "github.com/kairos-io/immucore/internal/utils" "github.com/kairos-io/immucore/pkg/op" "github.com/kairos-io/immucore/pkg/schema" + "github.com/kairos-io/kairos-sdk/signatures" "github.com/kairos-io/kairos-sdk/state" kcrypt "github.com/kairos-io/kcrypt/pkg/lib" "github.com/mudler/go-kdetect" @@ -557,3 +560,102 @@ func (s *State) UKIMountESPPartition(g *herd.Graph, opts ...herd.OpOption) error return nil }))...) } + +func (s *State) ExtractCerts(g *herd.Graph, opts ...herd.OpOption) error { + return g.Add(cnst.OpUkiExtractCerts, append(opts, herd.WithCallback(func(_ context.Context) error { + // Get all the full certs + certs, err := signatures.GetAllFullCerts() + if err != nil { + return err + } + + err = os.MkdirAll(s.path(cnst.VerityCertDir), 0755) + if err != nil { + return err + } + // Write all certs in x509 PEM format to /run/verity.d/ for sysextensions to verify against + for i, cert := range certs.PK { + publicKeyBlock := pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + publicKeyPem := pem.EncodeToMemory(&publicKeyBlock) + err := os.WriteFile(filepath.Join(s.path(cnst.VerityCertDir), fmt.Sprintf("PK%d.crt", i)), publicKeyPem, 0644) + if err != nil { + return err + } + } + for i, cert := range certs.KEK { + publicKeyBlock := pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + publicKeyPem := pem.EncodeToMemory(&publicKeyBlock) + err := os.WriteFile(filepath.Join(s.path(cnst.VerityCertDir), fmt.Sprintf("KEK%d.crt", i)), publicKeyPem, 0644) + if err != nil { + return err + } + } + for i, cert := range certs.DB { + publicKeyBlock := pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + publicKeyPem := pem.EncodeToMemory(&publicKeyBlock) + err := os.WriteFile(filepath.Join(s.path(cnst.VerityCertDir), fmt.Sprintf("DB%d.crt", i)), publicKeyPem, 0644) + if err != nil { + return err + } + } + + return nil + }))...) +} + +// CopySysExtensionsDagStep Copies extensions from the EFI partitions to the persistent one so they can be started. +func (s *State) CopySysExtensionsDagStep(g *herd.Graph, opts ...herd.OpOption) error { + return g.Add(cnst.OpUkiCopySysExtensions, append(opts, herd.WithCallback(func(_ context.Context) error { + if !state.EfiBootFromInstall(internalUtils.Log) { + internalUtils.Log.Debug().Msg("Not copying sysextensions as we think we are booting from removable media") + return nil + } + + // Copy the sys extensions to the rootfs + // Remember that we use s.path for the destination as it adds the future /sysroot prefix + // But for source, we are in initramfs so it should be without the prefix + // return if the source or dest dir is not there + if _, err := os.Stat(cnst.SourceSysExtDir); os.IsNotExist(err) { + internalUtils.Log.Debug().Str("dir", cnst.SourceSysExtDir).Msg("No sysextensions found") + return nil + } + if _, err := os.Stat(s.path(cnst.DestSysExtDir)); os.IsNotExist(err) { + _ = os.MkdirAll(s.path(cnst.DestSysExtDir), 0755) + } + err := filepath.WalkDir(s.path(cnst.SourceSysExtDir), func(_ string, d fs.DirEntry, err error) error { + if d.IsDir() { + return nil + } + src := filepath.Join(cnst.SourceSysExtDir, d.Name()) + dest := s.path(filepath.Join(cnst.DestSysExtDir, d.Name())) + + // TODO: Use the policy from the system config if exists, otherwise drop to default? + // This is to make it work also in non-uki envs where we might have a relaxed policy + output, err2 := internalUtils.CommandWithPath(fmt.Sprintf("systemd-dissect --validate %s %s", cnst.SysextDefaultPolicy, src)) + if err2 != nil { + // If the file didn't pass the validation, we don't copy it + internalUtils.Log.Warn().Str("src", src).Msg("Sysextension does not pass validation") + internalUtils.Log.Debug().Err(err2).Str("src", src).Str("output", output).Msg("Validating sysextension") + return nil + } + // Copy the file to the sys-extensions directory + err2 = internalUtils.Copy(src, dest) + if err != nil { + internalUtils.Log.Err(err2).Str("src", src).Str("dest", dest).Msg("Copying sysextension") + } + internalUtils.Log.Debug().Str("src", src).Str("dest", dest).Msg("Copied sysextension") + + return err2 + }) + return err + }))...) +}