From d2d3840c2e9e510f4ac44159adbcf4010e747618 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Tue, 12 May 2026 16:18:07 +0200 Subject: [PATCH 1/3] nodeinstaller/kataconfig: allow insecure attestation via kernel cmdline --- nodeinstaller/internal/kataconfig/config.go | 4 ++++ .../runtime-go/expected-configuration-qemu-insecure-gpu.toml | 2 +- .../runtime-go/expected-configuration-qemu-insecure.toml | 2 +- .../runtime-rs/expected-configuration-qemu-insecure-gpu.toml | 2 +- .../runtime-rs/expected-configuration-qemu-insecure.toml | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/nodeinstaller/internal/kataconfig/config.go b/nodeinstaller/internal/kataconfig/config.go index 80c66ed37f..af628adf6a 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 b098f474cd..f5e6e94c12 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 54373ea971..4e2bfc1cc6 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 6b5a2304bf..29ec890072 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 ceb467a45d..381d098b01 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' From 412b920952f981a1a7539990d4b4f724a629d3d1 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Mon, 27 Apr 2026 14:56:06 +0200 Subject: [PATCH 2/3] initdata-processor: support insecure platforms --- initdata-processor/main.go | 98 ++++++++++++++----- initdata-processor/validator/validator.go | 6 +- .../by-name/initdata-processor/package.nix | 2 + packages/nixos/kata.nix | 7 +- 4 files changed, 88 insertions(+), 25 deletions(-) diff --git a/initdata-processor/main.go b/initdata-processor/main.go index 7ffeafb44c..2f3090229d 100644 --- a/initdata-processor/main.go +++ b/initdata-processor/main.go @@ -5,15 +5,20 @@ package main import ( "bytes" + "context" + "errors" "fmt" "io" "io/fs" "log" + "net" + "net/http" "os" "path/filepath" "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" ) @@ -30,6 +35,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 +53,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 +68,89 @@ 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) - } - validator, err := validator.New() - if err != nil { - return fmt.Errorf("creating validator: %w", err) + return nil, false, fmt.Errorf("computing initdata digest: %w", 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) { + log.Print("WARNING: No TEE platform detected, skipping initdata digest validation. This is expected on insecure platforms.") + insecurePlatform = true + } 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, insecurePlatform, 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/validator/validator.go b/initdata-processor/validator/validator.go index c78ec25c4a..b09c46d5bb 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/packages/by-name/initdata-processor/package.nix b/packages/by-name/initdata-processor/package.nix index 15b278a522..c24dcd20c2 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 2c45f00072..7f188d0451 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; }; From c168265dfd12d7db6cc44f7e73e45e1e47d13e17 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Tue, 12 May 2026 16:19:53 +0200 Subject: [PATCH 3/3] initdata-processor: gate insecure attestation via cmdline --- initdata-processor/main.go | 29 +++++++++++++++++--- initdata-processor/main_test.go | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 initdata-processor/main_test.go diff --git a/initdata-processor/main.go b/initdata-processor/main.go index 2f3090229d..475c30809d 100644 --- a/initdata-processor/main.go +++ b/initdata-processor/main.go @@ -15,6 +15,8 @@ import ( "net/http" "os" "path/filepath" + "slices" + "strings" "github.com/edgelesssys/contrast/initdata-processor/policy" "github.com/edgelesssys/contrast/initdata-processor/validator" @@ -27,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. @@ -100,10 +105,17 @@ func handleInitdata(doc initdata.Raw) (hostdata []byte, insecurePlatform bool, r return nil, false, fmt.Errorf("computing initdata digest: %w", err) } + allowInsecure, err := allowInsecureAttestation() + if err != nil { + return nil, false, 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.") - insecurePlatform = true } else if verr != nil { return nil, false, fmt.Errorf("creating validator: %w", verr) } else if err := v.ValidateDigest(digest); err != nil { @@ -121,7 +133,18 @@ func handleInitdata(doc initdata.Raw) (hostdata []byte, insecurePlatform bool, r return nil, false, fmt.Errorf("writing file %q: %w", path, err) } } - return digest, insecurePlatform, 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. diff --git a/initdata-processor/main_test.go b/initdata-processor/main_test.go new file mode 100644 index 0000000000..e5945f427d --- /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) + }) + } +}