diff --git a/initdata-processor/main.go b/initdata-processor/main.go index 7ffeafb44c6..475c30809dc 100644 --- a/initdata-processor/main.go +++ b/initdata-processor/main.go @@ -5,15 +5,22 @@ package main import ( "bytes" + "context" + "errors" "fmt" "io" "io/fs" "log" + "net" + "net/http" "os" "path/filepath" + "slices" + "strings" "github.com/edgelesssys/contrast/initdata-processor/policy" "github.com/edgelesssys/contrast/initdata-processor/validator" + "github.com/edgelesssys/contrast/internal/attestation/insecure" "github.com/edgelesssys/contrast/internal/initdata" ) @@ -22,7 +29,10 @@ const ( insecureConfigPath = "/run/insecure-cfg" ) -var version = "0.0.0-dev" +var ( + version = "0.0.0-dev" + kernelCmdlinePath = "/proc/cmdline" +) // We always exit with status code 0 so that the Kata agent can start and propagate errors to // the runtime. @@ -30,6 +40,9 @@ func main() { log.Printf("Contrast initdata-processor %s", version) log.Print("Report issues at https://github.com/edgelesssys/contrast/issues") + var hostdata []byte + var insecurePlatform bool + // Handle initdata. if err := os.MkdirAll(measuredConfigPath, 0o755); err != nil { failf("Could not create directory %q: %v", measuredConfigPath, err) @@ -45,7 +58,8 @@ func main() { failf("%s is not an initdata device: %v", device, err) return } - if err := handleInitdata(doc); err != nil { + hostdata, insecurePlatform, err = handleInitdata(doc) + if err != nil { failf("handling initdata: %v", err) return } @@ -59,44 +73,107 @@ func main() { device, err = checkDeviceAvailability("imagepuller") if err != nil { log.Println("No imagepuller auth config found, only unauthenticated pulls will be available") - return - } - doc, err = initdata.FromDevice(device, "imgpullr") - if err != nil { - failf("%s is not an imagepuller config device: %v", device, err) - return + } else { + doc, err = initdata.FromDevice(device, "imgpullr") + if err != nil { + failf("%s is not an imagepuller config device: %v", device, err) + return + } + if err := handleImagepullerAuthConfig(doc); err != nil { + failf("handling imagepuller auth config: %v", err) + return + } + log.Printf("Processed imagepuller auth config from %q ", device) } - if err := handleImagepullerAuthConfig(doc); err != nil { - failf("handling imagepuller auth config: %v", err) - return + + // Signal systemd that initdata processing is complete. + sdNotifyReady() + + // On insecure platforms, serve the hostdata digest via HTTP so that + // the insecure aTLS issuer (running inside containers) can fetch it. + if insecurePlatform { + log.Printf("Starting insecure hostdata server on %s", insecure.HostdataAddr) + if err := serveHostdata(hostdata); err != nil { + log.Printf("Hostdata server error: %v", err) + } } - log.Printf("Processed imagepuller auth config from %q ", device) } -func handleInitdata(doc initdata.Raw) error { +func handleInitdata(doc initdata.Raw) (hostdata []byte, insecurePlatform bool, retErr error) { digest, err := doc.Digest() if err != nil { - return fmt.Errorf("initdata validation failed: %w", err) + return nil, false, fmt.Errorf("computing initdata digest: %w", err) } - validator, err := validator.New() + + allowInsecure, err := allowInsecureAttestation() if err != nil { - return fmt.Errorf("creating validator: %w", err) + return nil, false, err } - if err := validator.ValidateDigest(digest); err != nil { - return fmt.Errorf("validating initdata digest: %w", err) + + v, verr := validator.New() + if errors.Is(verr, validator.ErrNoPlatform) { + if !allowInsecure { + return nil, false, fmt.Errorf("no TEE platform detected and insecure attestation is not allowed") + } + log.Print("WARNING: No TEE platform detected, skipping initdata digest validation. This is expected on insecure platforms.") + } else if verr != nil { + return nil, false, fmt.Errorf("creating validator: %w", verr) + } else if err := v.ValidateDigest(digest); err != nil { + return nil, false, fmt.Errorf("validating initdata digest: %w", err) } + data, err := doc.Parse() if err != nil { - return fmt.Errorf("parsing initdata: %w", err) + return nil, false, fmt.Errorf("parsing initdata: %w", err) } for name, content := range data.Data { name = filepath.Clean(name) path := filepath.Join(measuredConfigPath, name) if err := os.WriteFile(path, []byte(content), 0o644); err != nil { - return fmt.Errorf("writing file %q: %w", path, err) + return nil, false, fmt.Errorf("writing file %q: %w", path, err) } } - return nil + return digest, allowInsecure, nil +} + +func allowInsecureAttestation() (bool, error) { + cmdline, err := os.ReadFile(kernelCmdlinePath) + if err != nil { + return false, fmt.Errorf("reading kernel command line: %w", err) + } + if slices.Contains(strings.Fields(string(cmdline)), "contrast.allow_insecure_attestation=1") { + return true, nil + } + return false, nil +} + +// serveHostdata starts an HTTP server that serves the hostdata digest. +func serveHostdata(hostdata []byte) error { + mux := http.NewServeMux() + mux.HandleFunc("GET /hostdata", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/octet-stream") + if _, err := w.Write(hostdata); err != nil { + log.Printf("hostdata write error: %v", err) + } + }) + return http.ListenAndServe(insecure.HostdataAddr, mux) +} + +// sdNotifyReady signals systemd that the service is ready. +func sdNotifyReady() { + addr := os.Getenv("NOTIFY_SOCKET") + if addr == "" { + return + } + conn, err := (&net.Dialer{}).DialContext(context.Background(), "unixgram", addr) + if err != nil { + log.Printf("sd_notify: dial: %v", err) + return + } + defer conn.Close() + if _, err := conn.Write([]byte("READY=1")); err != nil { + log.Printf("sd_notify: write: %v", err) + } } func handleImagepullerAuthConfig(doc initdata.Raw) error { diff --git a/initdata-processor/main_test.go b/initdata-processor/main_test.go new file mode 100644 index 00000000000..e5945f427dc --- /dev/null +++ b/initdata-processor/main_test.go @@ -0,0 +1,47 @@ +// Copyright 2026 Edgeless Systems GmbH +// SPDX-License-Identifier: BUSL-1.1 + +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAllowInsecureAttestation(t *testing.T) { + oldKernelCmdlinePath := kernelCmdlinePath + t.Cleanup(func() { kernelCmdlinePath = oldKernelCmdlinePath }) + + testCases := map[string]struct { + cmdline string + want bool + }{ + "absent": { + cmdline: "console=hvc0", + want: false, + }, + "disabled": { + cmdline: "console=hvc0 contrast.allow_insecure_attestation=0", + want: false, + }, + "enabled": { + cmdline: "console=hvc0 contrast.allow_insecure_attestation=1 quiet", + want: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + kernelCmdlinePath = filepath.Join(t.TempDir(), "cmdline") + require.NoError(t, os.WriteFile(kernelCmdlinePath, []byte(tc.cmdline), 0o644)) + + got, err := allowInsecureAttestation() + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/initdata-processor/validator/validator.go b/initdata-processor/validator/validator.go index c78ec25c4ac..b09c46d5bb1 100644 --- a/initdata-processor/validator/validator.go +++ b/initdata-processor/validator/validator.go @@ -29,7 +29,7 @@ func New() (*Validator, error) { if terr == nil { return &Validator{&tdxDigestGetter{tqp}}, nil } - return nil, fmt.Errorf("%w:\nTDX:%w\nSNP:%w", errBadPlatform, terr, serr) + return nil, fmt.Errorf("%w:\nTDX:%w\nSNP:%w", ErrNoPlatform, terr, serr) } // ValidateDigest compares the given digest with either MRCONFIGID or HOSTDATA, and returns an error if they don't match. @@ -85,7 +85,9 @@ func getTDXQuoteProvider() (tdxclient.QuoteProvider, error) { } var ( - errBadPlatform = errors.New("no digest getter available for current platform") + // ErrNoPlatform is returned by New when no TEE platform is available for digest validation. + ErrNoPlatform = errors.New("no digest getter available for current platform") + errUnexpectedDigestSize = errors.New("unexpected digest size") errDigestMismatch = errors.New("digests don't match") ) diff --git a/nodeinstaller/internal/kataconfig/config.go b/nodeinstaller/internal/kataconfig/config.go index 80c66ed37fb..af628adf6aa 100644 --- a/nodeinstaller/internal/kataconfig/config.go +++ b/nodeinstaller/internal/kataconfig/config.go @@ -6,6 +6,7 @@ package kataconfig import ( "fmt" "path/filepath" + "strings" "github.com/edgelesssys/contrast/internal/platforms" "github.com/pelletier/go-toml/v2" @@ -75,6 +76,9 @@ func KataRuntimeConfig( // Replace the kernel params entirely (and don't append) since that's // also what we do when calculating the launch measurement. + if platforms.IsInsecure(platform) { + qemuExtraKernelParams = strings.TrimSpace(qemuExtraKernelParams + " contrast.allow_insecure_attestation=1") + } config.Hypervisor["qemu"]["kernel_params"] = qemuExtraKernelParams // Conditionally enable debug mode. config.Hypervisor["qemu"]["enable_debug"] = debug diff --git a/nodeinstaller/internal/kataconfig/testdata/runtime-go/expected-configuration-qemu-insecure-gpu.toml b/nodeinstaller/internal/kataconfig/testdata/runtime-go/expected-configuration-qemu-insecure-gpu.toml index b098f474cdb..f5e6e94c125 100644 --- a/nodeinstaller/internal/kataconfig/testdata/runtime-go/expected-configuration-qemu-insecure-gpu.toml +++ b/nodeinstaller/internal/kataconfig/testdata/runtime-go/expected-configuration-qemu-insecure-gpu.toml @@ -44,7 +44,7 @@ image = '/share/kata-containers.img' indep_iothreads = 0 initrd = '/share/kata-initrd.zst' kernel = '/share/kata-kernel' -kernel_params = '' +kernel_params = 'contrast.allow_insecure_attestation=1' kernel_verity_params = '' machine_accelerators = '' machine_type = 'q35' diff --git a/nodeinstaller/internal/kataconfig/testdata/runtime-go/expected-configuration-qemu-insecure.toml b/nodeinstaller/internal/kataconfig/testdata/runtime-go/expected-configuration-qemu-insecure.toml index 54373ea971a..4e2bfc1cc6c 100644 --- a/nodeinstaller/internal/kataconfig/testdata/runtime-go/expected-configuration-qemu-insecure.toml +++ b/nodeinstaller/internal/kataconfig/testdata/runtime-go/expected-configuration-qemu-insecure.toml @@ -43,7 +43,7 @@ image = '/share/kata-containers.img' indep_iothreads = 0 initrd = '/share/kata-initrd.zst' kernel = '/share/kata-kernel' -kernel_params = '' +kernel_params = 'contrast.allow_insecure_attestation=1' kernel_verity_params = '' machine_accelerators = '' machine_type = 'q35' diff --git a/nodeinstaller/internal/kataconfig/testdata/runtime-rs/expected-configuration-qemu-insecure-gpu.toml b/nodeinstaller/internal/kataconfig/testdata/runtime-rs/expected-configuration-qemu-insecure-gpu.toml index 6b5a2304bf2..29ec890072c 100644 --- a/nodeinstaller/internal/kataconfig/testdata/runtime-rs/expected-configuration-qemu-insecure-gpu.toml +++ b/nodeinstaller/internal/kataconfig/testdata/runtime-rs/expected-configuration-qemu-insecure-gpu.toml @@ -41,7 +41,7 @@ guest_memory_dump_path = '' image = '/share/kata-containers.img' initrd = '/share/kata-initrd.zst' kernel = '/share/kata-kernel' -kernel_params = '' +kernel_params = 'contrast.allow_insecure_attestation=1' kernel_verity_params = '' machine_accelerators = '' machine_type = 'q35' diff --git a/nodeinstaller/internal/kataconfig/testdata/runtime-rs/expected-configuration-qemu-insecure.toml b/nodeinstaller/internal/kataconfig/testdata/runtime-rs/expected-configuration-qemu-insecure.toml index ceb467a45d0..381d098b01b 100644 --- a/nodeinstaller/internal/kataconfig/testdata/runtime-rs/expected-configuration-qemu-insecure.toml +++ b/nodeinstaller/internal/kataconfig/testdata/runtime-rs/expected-configuration-qemu-insecure.toml @@ -41,7 +41,7 @@ guest_memory_dump_path = '' image = '/share/kata-containers.img' initrd = '/share/kata-initrd.zst' kernel = '/share/kata-kernel' -kernel_params = '' +kernel_params = 'contrast.allow_insecure_attestation=1' kernel_verity_params = '' machine_accelerators = '' machine_type = 'q35' diff --git a/packages/by-name/initdata-processor/package.nix b/packages/by-name/initdata-processor/package.nix index 15b278a522b..c24dcd20c27 100644 --- a/packages/by-name/initdata-processor/package.nix +++ b/packages/by-name/initdata-processor/package.nix @@ -48,6 +48,8 @@ buildGoModule (finalAttrs: { (path.append root "go.sum") (path.append root "initdata-processor/go.mod") (path.append root "initdata-processor/go.sum") + (fileset.fileFilter (file: hasSuffix ".go" file.name) (path.append root "internal/attestation")) + (fileset.fileFilter (file: hasSuffix ".go" file.name) (path.append root "internal/oid")) (fileset.fileFilter (file: hasSuffix ".go" file.name) (path.append root "internal/initdata")) (fileset.fileFilter (file: hasSuffix ".go" file.name) (path.append root "initdata-processor")) ]; diff --git a/packages/nixos/kata.nix b/packages/nixos/kata.nix index 2c45f000726..7f188d0451f 100644 --- a/packages/nixos/kata.nix +++ b/packages/nixos/kata.nix @@ -57,7 +57,12 @@ in wants = [ "initdata.target" ]; serviceConfig = { - Type = "oneshot"; + # notify: the process signals READY=1 when initdata processing is + # complete, allowing initdata.target to be reached. On insecure + # platforms the process stays alive to serve hostdata via HTTP; + # on CC platforms it exits after signaling. + Type = "notify"; + NotifyAccess = "main"; RemainAfterExit = "yes"; ExecStart = lib.getExe pkgs.contrastPkgs.initdata-processor; };