From 4bb732ebf9c44b1c8d9e365a3b6c34f0f74ef1d6 Mon Sep 17 00:00:00 2001 From: dorcaslitunya <36160963+dorcaslitunya@users.noreply.github.com> Date: Wed, 12 Mar 2025 12:24:51 +0300 Subject: [PATCH] Add igvm measurement (#379) Add copyright information to package Add testing to igvm measurements Remove trailing white space Improve testing Resolve PR comments Add measure to cli Add README for feature Fix PR comments Added new line to shell script Add measurement interface Fix ci Refactor code for IgvmMeasurement to become a CLI dependency Refactor code for IgvmMeasurement to become a CLI dependency Refactor based on ci failures Fix error handling Add header Fix ci --- Makefile | 8 +- cli/README.md | 19 +++++ cli/attestation.go | 22 ++++++ cli/attestation_test.go | 59 +++++++++++++- cli/sdk.go | 5 +- cmd/cli/main.go | 14 +++- pkg/attestation/igvmmeasure/igvmmeasure.go | 79 +++++++++++++++++++ .../igvmmeasure/igvmmeasure_test.go | 77 ++++++++++++++++++ scripts/igvmmeasure/igvm.sh | 40 ++++++++++ 9 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 pkg/attestation/igvmmeasure/igvmmeasure.go create mode 100644 pkg/attestation/igvmmeasure/igvmmeasure_test.go create mode 100755 scripts/igvmmeasure/igvm.sh diff --git a/Makefile b/Makefile index 569549eb..5d655424 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ CONFIG_DIR ?= /etc/cocos SERVICE_NAME ?= cocos-manager SERVICE_DIR ?= /etc/systemd/system SERVICE_FILE = init/systemd/$(SERVICE_NAME).service +IGVM_BUILD_SCRIPT := ./scripts/igvmmeasure/igvm.sh define compile_service CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \ @@ -27,8 +28,9 @@ endef all: $(SERVICES) -$(SERVICES): +$(SERVICES): $(call compile_service,$@) + @if [ "$@" = "cli" ]; then $(MAKE) build-igvm; fi $(ATTESTATION_POLICY): $(MAKE) -C ./scripts/attestation_policy @@ -61,3 +63,7 @@ stop: install_service: sudo install -m 644 $(SERVICE_FILE) $(SERVICE_DIR)/$(SERVICE_NAME).service sudo systemctl daemon-reload + +build-igvm: + @echo "Running build script for igvmmeasure..." + @$(IGVM_BUILD_SCRIPT) diff --git a/cli/README.md b/cli/README.md index 2856050c..b3a05a92 100644 --- a/cli/README.md +++ b/cli/README.md @@ -100,3 +100,22 @@ When defining the manifest dataset and algorithm checksums are required. This ca ```bash ./build/cocos-cli checksum ``` + +#### Measure IGVM file +We assume that our current working directory is the root of the cocos repository, both on the host machine and in the VM. + +`igvmmeasure` calculates the launch measurement for an IGVM file and can generate a signed version. It ensures integrity by precomputing the expected launch digest, which can be verified against the attestation report. The tool parses IGVM directives, outputs the measurement as a hex string, or creates a signed file for verification at guest launch. + +##### Example +We measure an IGVM file using our measure command, run: + +```bash +./build/cocos-cli igvmmeasure /path/to/igvm/file +``` + +The tool will parse the directives in the IGVM file, calculate the launch measurement, and output the computed digest. If successful, it prints the measurement to standard output. + +Here is a sample output +``` +91c4929bec2d0ecf11a708e09f0a57d7d82208bcba2451564444a4b01c22d047995ca27f9053f86de4e8063e9f810548 +``` \ No newline at end of file diff --git a/cli/attestation.go b/cli/attestation.go index 36232fbb..73ef5daa 100644 --- a/cli/attestation.go +++ b/cli/attestation.go @@ -624,6 +624,28 @@ func (cli *CLI) NewValidateAttestationValidationCmd() *cobra.Command { return cmd } +func (cli *CLI) NewMeasureCmd(igvmBinaryPath string) *cobra.Command { + igvmmeasureCmd := &cobra.Command{ + Use: "igvmmeasure ", + Short: "Measure an IGVM file", + Long: `igvmmeasure measures an IGVM file and outputs the calculated measurement. + It ensures integrity verification for the IGVM file.`, + + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("error: No input file provided") + } + + inputFile := args[0] + + return cli.measurement.Run(inputFile) + }, + } + + return igvmmeasureCmd +} + func sevsnpverify(cmd *cobra.Command, args []string) error { cmd.Println("Checking attestation") diff --git a/cli/attestation_test.go b/cli/attestation_test.go index 9678efa6..8aa21d67 100644 --- a/cli/attestation_test.go +++ b/cli/attestation_test.go @@ -34,8 +34,6 @@ func TestNewAttestationCmd(t *testing.T) { var buf bytes.Buffer cmd.SetOut(&buf) - cmd.SetOutput(&buf) - reportData := bytes.Repeat([]byte{0x01}, quoteprovider.Nonce) mockSDK.On("Attestation", mock.Anything, [quoteprovider.Nonce]byte(reportData), mock.Anything).Return(nil) @@ -159,7 +157,7 @@ func TestNewGetAttestationCmd(t *testing.T) { } cmd := cli.NewGetAttestationCmd() var buf bytes.Buffer - cmd.SetOutput(&buf) + cmd.SetOut(&buf) mockSDK.On("Attestation", mock.Anything, [quoteprovider.Nonce]byte(bytes.Repeat([]byte{0x00}, quoteprovider.Nonce)), [vtpm.Nonce]byte(bytes.Repeat([]byte{0x00}, vtpm.Nonce)), mock.Anything, mock.Anything).Return(tc.mockError).Run(func(args mock.Arguments) { _, err := args.Get(4).(*os.File).Write(tc.mockResponse) @@ -285,6 +283,61 @@ func TestNewValidateAttestationValidationCmd(t *testing.T) { }) } +type MockMeasurement struct { + mock.Mock +} + +func (m *MockMeasurement) Run(igvmBinaryPath string) error { + args := m.Called(igvmBinaryPath) + return args.Error(0) +} + +func (m *MockMeasurement) Stop() error { + args := m.Called() + return args.Error(0) +} + +func TestNewMeasureCmd_RunSuccess(t *testing.T) { + cliInstance := &CLI{} + mockMeasurement := new(MockMeasurement) + cliInstance.measurement = mockMeasurement + + mockMeasurement.On("Run", "testfile.igvm").Return(nil) + + cmd := cliInstance.NewMeasureCmd("fake_binary_path") + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{"testfile.igvm"}) + + err := cmd.Execute() + + assert.NoError(t, err) + mockMeasurement.AssertExpectations(t) +} + +func TestNewMeasureCmd_RunError(t *testing.T) { + cliInstance := &CLI{} + mockMeasurement := new(MockMeasurement) + cliInstance.measurement = mockMeasurement + expectedError := errors.New("mocked measurement error") + + mockMeasurement.On("Run", "testfile.igvm").Return(expectedError) + + cmd := cliInstance.NewMeasureCmd("fake_binary_path") + + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetErr(buf) + cmd.SetArgs([]string{"testfile.igvm"}) + + err := cmd.Execute() + + assert.Error(t, err) + assert.Equal(t, expectedError.Error(), err.Error()) + mockMeasurement.AssertExpectations(t) +} + func TestParseConfig(t *testing.T) { cfgString = "" err := parseConfig() diff --git a/cli/sdk.go b/cli/sdk.go index 76eebf78..53b1d9b0 100644 --- a/cli/sdk.go +++ b/cli/sdk.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/ultravioletrs/cocos/manager" + "github.com/ultravioletrs/cocos/pkg/attestation/igvmmeasure" "github.com/ultravioletrs/cocos/pkg/clients/grpc" "github.com/ultravioletrs/cocos/pkg/clients/grpc/agent" managergrpc "github.com/ultravioletrs/cocos/pkg/clients/grpc/manager" @@ -22,12 +23,14 @@ type CLI struct { client grpc.Client managerClient manager.ManagerServiceClient connectErr error + measurement igvmmeasure.MeasurementProvider } -func New(agentConfig grpc.AgentClientConfig, managerConfig grpc.ManagerClientConfig) *CLI { +func New(agentConfig grpc.AgentClientConfig, managerConfig grpc.ManagerClientConfig, measurement igvmmeasure.MeasurementProvider) *CLI { return &CLI{ agentConfig: agentConfig, managerConfig: managerConfig, + measurement: measurement, } } diff --git a/cmd/cli/main.go b/cmd/cli/main.go index ace4d1a0..64bb2302 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/ultravioletrs/cocos/cli" + "github.com/ultravioletrs/cocos/pkg/attestation/igvmmeasure" "github.com/ultravioletrs/cocos/pkg/clients/grpc" cmd "github.com/virtee/sev-snp-measure-go/sevsnpmeasure/cmd" ) @@ -28,7 +29,8 @@ const ( ) type config struct { - LogLevel string `env:"AGENT_LOG_LEVEL" envDefault:"info"` + LogLevel string `env:"AGENT_LOG_LEVEL" envDefault:"info"` + IgvmBinaryPath string `env:"IGVM_BINARY_PATH" envDefault:"./build/igvmmeasure"` } func main() { @@ -106,7 +108,14 @@ func main() { return } - cliSVC := cli.New(agentGRPCConfig, managerGRPCConfig) + measurement, err := igvmmeasure.NewIgvmMeasurement(cfg.IgvmBinaryPath, os.Stderr, os.Stdout) + if err != nil { + message := color.New(color.FgRed).Sprintf("failed to initialize measurement: %s", err) // Use %s instead of %w + rootCmd.Println(message) + return + } + + cliSVC := cli.New(agentGRPCConfig, managerGRPCConfig, measurement) if err := cliSVC.InitializeAgentSDK(rootCmd); err == nil { defer cliSVC.Close() @@ -136,6 +145,7 @@ func main() { // measure. rootCmd.AddCommand(cmd.NewRootCmd()) + rootCmd.AddCommand(cliSVC.NewMeasureCmd(cfg.IgvmBinaryPath)) // Flags keysCmd.PersistentFlags().StringVarP( diff --git a/pkg/attestation/igvmmeasure/igvmmeasure.go b/pkg/attestation/igvmmeasure/igvmmeasure.go new file mode 100644 index 00000000..4e760ce5 --- /dev/null +++ b/pkg/attestation/igvmmeasure/igvmmeasure.go @@ -0,0 +1,79 @@ +// Copyright (c) Ultraviolet +// SPDX-License-Identifier: Apache-2.0 +package igvmmeasure + +import ( + "fmt" + "io" + "os/exec" + "strings" +) + +type MeasurementProvider interface { + Run(igvmBinaryPath string) error + Stop() error +} +type IgvmMeasurement struct { + binPath string + options []string + stderr io.Writer + stdout io.Writer + cmd *exec.Cmd + execCommand func(name string, arg ...string) *exec.Cmd +} + +func NewIgvmMeasurement(binPath string, stderr, stdout io.Writer) (*IgvmMeasurement, error) { + if binPath == "" { + return nil, fmt.Errorf("pathToBinary cannot be empty") + } + + return &IgvmMeasurement{ + binPath: binPath, + stderr: stderr, + stdout: stdout, + execCommand: exec.Command, + }, nil +} + +func (m *IgvmMeasurement) Run(pathToFile string) error { + binary := m.binPath + args := []string{} + args = append(args, m.options...) + args = append(args, pathToFile) + args = append(args, "measure") + args = append(args, "-b") + + out, err := m.execCommand(binary, args...).CombinedOutput() + if err != nil { + fmt.Println("Error:", err) + } + outputString := string(out) + + lines := strings.Split(strings.TrimSpace(outputString), "\n") + + if len(lines) == 1 { + outputString = strings.ToLower(outputString) + fmt.Print(outputString) + } else { + return fmt.Errorf("error: %s", outputString) + } + + return nil +} + +func (m *IgvmMeasurement) Stop() error { + if m.cmd == nil || m.cmd.Process == nil { + return fmt.Errorf("no running process to stop") + } + + if err := m.cmd.Process.Kill(); err != nil { + return fmt.Errorf("failed to stop process: %v", err) + } + + return nil +} + +// SetExecCommand allows tests to inject a mock execCommand function. +func (m *IgvmMeasurement) SetExecCommand(cmdFunc func(name string, arg ...string) *exec.Cmd) { + m.execCommand = cmdFunc +} diff --git a/pkg/attestation/igvmmeasure/igvmmeasure_test.go b/pkg/attestation/igvmmeasure/igvmmeasure_test.go new file mode 100644 index 00000000..ec049a8d --- /dev/null +++ b/pkg/attestation/igvmmeasure/igvmmeasure_test.go @@ -0,0 +1,77 @@ +// Copyright (c) Ultraviolet +// SPDX-License-Identifier: Apache-2.0 +package igvmmeasure + +import ( + "bytes" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIgvmMeasurement(t *testing.T) { + tests := []struct { + name string + setup func() *IgvmMeasurement + runArgs string + expectErr bool + expectedErr string + }{ + { + name: "NewIgvmMeasurement - Empty pathToBinary", + setup: func() *IgvmMeasurement { + igvm, err := NewIgvmMeasurement("", nil, nil) + assert.Error(t, err) + assert.Nil(t, igvm) + return nil + }, + expectErr: true, + expectedErr: "pathToBinary cannot be empty", + }, + { + name: "Run - Successful Execution", + setup: func() *IgvmMeasurement { + igvm, _ := NewIgvmMeasurement("/valid/path", nil, nil) + igvm.SetExecCommand(func(name string, arg ...string) *exec.Cmd { + return exec.Command("sh", "-c", "echo 'measurement successful'") + }) + return igvm + }, + expectErr: false, + }, + { + name: "Run - Failure Execution", + setup: func() *IgvmMeasurement { + igvm, _ := NewIgvmMeasurement("/invalid/path", nil, nil) + igvm.SetExecCommand(func(name string, arg ...string) *exec.Cmd { + return exec.Command("sh", "-c", "echo 'some error occurred\nextra line' && exit 1") + }) + return igvm + }, + expectErr: true, + expectedErr: "error: some error occurred\nextra line", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + igvm := tc.setup() + + if igvm != nil { + buf := new(bytes.Buffer) + igvm.stdout = buf + igvm.stderr = buf + + err := igvm.Run(tc.runArgs) + if tc.expectErr { + assert.Error(t, err) + assert.Equal(t, strings.TrimSpace(tc.expectedErr), strings.TrimSpace(err.Error())) + } else { + assert.NoError(t, err) + } + } + }) + } +} diff --git a/scripts/igvmmeasure/igvm.sh b/scripts/igvmmeasure/igvm.sh new file mode 100755 index 00000000..974cef1f --- /dev/null +++ b/scripts/igvmmeasure/igvm.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +REPO_URL="https://github.com/coconut-svsm/svsm.git" +BUILD_DIR="$(cd "$(dirname "$0")/../.." && pwd)/build" + + +mkdir -p "$BUILD_DIR" + +# Define the target directory for cloning inside the build directory +TARGET_DIR="$BUILD_DIR/svsm" +SUBDIR="igvmmeasure" + +# Clone the repository if it doesn't exist +if [ -d "$TARGET_DIR" ]; then + echo "Repository already exists in $TARGET_DIR. Pulling latest changes..." + cd "$TARGET_DIR" && git pull +else + echo "Cloning repository into $TARGET_DIR..." + git clone --recurse-submodules "$REPO_URL" "$TARGET_DIR" +fi + +# Ensure submodules are up to date +cd "$TARGET_DIR" +git submodule update --init --recursive + +# Check if the required subdirectory exists +if [ -d "$SUBDIR" ]; then + echo "Successfully cloned repository and found '$SUBDIR' directory." +else + echo "Error: '$SUBDIR' directory not found inside '$TARGET_DIR'." + exit 1 +fi + +echo "Building the Rust crate..." + +RELEASE=1 make bin/igvmmeasure BUILDDIR="$BUILD_DIR" + +mv bin/igvmmeasure "$BUILD_DIR/" + +echo "Binary stored in: $BUILD_DIR/igvmmeasure"