Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 98 additions & 21 deletions initdata-processor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -22,14 +29,20 @@ 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.
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)
Expand All @@ -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
}
Expand All @@ -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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably add a http.Server.Shutdown hook, just for cleanliness' sake, since this currently can never exit cleanly, right?

}

// 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 {
Expand Down
47 changes: 47 additions & 0 deletions initdata-processor/main_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
6 changes: 4 additions & 2 deletions initdata-processor/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")
)
4 changes: 4 additions & 0 deletions nodeinstaller/internal/kataconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package kataconfig
import (
"fmt"
"path/filepath"
"strings"

"github.com/edgelesssys/contrast/internal/platforms"
"github.com/pelletier/go-toml/v2"
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions packages/by-name/initdata-processor/package.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
];
Expand Down
7 changes: 6 additions & 1 deletion packages/nixos/kata.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down