diff --git a/cli/attestation.go b/cli/attestation.go index 38762919..77d18717 100644 --- a/cli/attestation.go +++ b/cli/attestation.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/ultravioletrs/cocos/agent" + "github.com/ultravioletrs/cocos/pkg/attestation/igvmmeasure" "github.com/ultravioletrs/cocos/pkg/attestation/quoteprovider" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/prototext" @@ -557,6 +558,39 @@ func (cli *CLI) NewValidateAttestationValidationCmd() *cobra.Command { return cmd } +var newMeasurementFunc = igvmmeasure.NewIgvmMeasurement + +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] + + measurement, err := newMeasurementFunc(inputFile, os.Stderr, os.Stdout) + if err != nil { + return fmt.Errorf("error initializing measurement: %v", err) + } + + if err := measurement.Run(igvmBinaryPath); err != nil { + return fmt.Errorf("error running measurement: %v", err) + } + + return nil + }, + } + + 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 e8542568..00255512 100644 --- a/cli/attestation_test.go +++ b/cli/attestation_test.go @@ -7,6 +7,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "io" "os" "testing" @@ -19,6 +20,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/ultravioletrs/cocos/agent" + "github.com/ultravioletrs/cocos/pkg/attestation/igvmmeasure" "github.com/ultravioletrs/cocos/pkg/sdk/mocks" ) @@ -252,6 +254,37 @@ func TestNewValidateAttestationValidationCmd(t *testing.T) { }) } +func TestNewMeasureCmd_RunSuccess(t *testing.T) { + cli := &CLI{} + cmd := cli.NewMeasureCmd("/mock/igvmBinary") + + var buf bytes.Buffer + cmd.SetOut(&buf) + cmd.SetArgs([]string{"/valid/input.igvm"}) + + err := cmd.Execute() + assert.NoError(t, err) +} + +func TestNewMeasureCmd_RunError(t *testing.T) { + newMeasurementFunc = func(pathToFile string, stderr io.Writer, stdout io.Writer) (*igvmmeasure.IgvmMeasurement, error) { + return nil, fmt.Errorf("mock error: Error initializing measurement") + } + defer func() { newMeasurementFunc = igvmmeasure.NewIgvmMeasurement }() + + cli := &CLI{} + cmd := cli.NewMeasureCmd("/mock/igvmBinary") + + var buf bytes.Buffer + cmd.SetErr(&buf) + cmd.SetArgs([]string{"/invalid/input.igvm"}) + + err := cmd.Execute() + + assert.Error(t, err, "Expected an error but got nil") + assert.Contains(t, buf.String(), "Error initializing measurement", "Expected error message to be present") +} + func TestParseConfig(t *testing.T) { cfgString = "" err := parseConfig() diff --git a/cmd/cli/main.go b/cmd/cli/main.go index ace4d1a0..c0d12dda 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -28,7 +28,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:"./svsm/bin/igvmmeasure"` } func main() { @@ -136,6 +137,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..bf36c7db --- /dev/null +++ b/pkg/attestation/igvmmeasure/igvmmeasure.go @@ -0,0 +1,75 @@ +// Copyright (c) Ultraviolet +// SPDX-License-Identifier: Apache-2.0 +package igvmmeasure + +import ( + "fmt" + "io" + "os/exec" + "strings" +) + +type IgvmMeasurement struct { + pathToFile string + options []string + stderr io.Writer + stdout io.Writer + cmd *exec.Cmd + execCommand func(name string, arg ...string) *exec.Cmd +} + +func NewIgvmMeasurement(pathToFile string, stderr, stdout io.Writer) (*IgvmMeasurement, error) { + if pathToFile == "" { + return nil, fmt.Errorf("pathToFile cannot be empty") + } + + return &IgvmMeasurement{ + pathToFile: pathToFile, + stderr: stderr, + stdout: stdout, + execCommand: exec.Command, + }, nil +} + +func (m *IgvmMeasurement) Run(igvmBinaryPath string) error { + binary := igvmBinaryPath + args := []string{} + args = append(args, m.options...) + args = append(args, m.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..49385a9c --- /dev/null +++ b/pkg/attestation/igvmmeasure/igvmmeasure_test.go @@ -0,0 +1,121 @@ +// Copyright (c) Ultraviolet +// SPDX-License-Identifier: Apache-2.0 +package igvmmeasure + +import ( + "os" + "os/exec" + "strings" + "testing" +) + +type MockCmd struct { + output string + err error +} + +func (m *MockCmd) CombinedOutput() ([]byte, error) { + return []byte(m.output), m.err +} + +func MockExecCommand(output string, err error) func(name string, arg ...string) *MockCmd { + return func(name string, arg ...string) *MockCmd { + return &MockCmd{ + output: output, + err: err, + } + } +} + +func TestNewIgvmMeasurement(t *testing.T) { + _, err := NewIgvmMeasurement("", nil, nil) + if err == nil { + t.Errorf("expected error for empty pathToFile, got nil") + } + + igvm, err := NewIgvmMeasurement("/valid/path", nil, nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if igvm == nil { + t.Errorf("expected non-nil IgvmMeasurement") + } +} + +func TestIgvmMeasurement_Run_Success(t *testing.T) { + mockOutput := "measurement successful" // Ensure it's a **single-line output** + + m := &IgvmMeasurement{ + pathToFile: "/valid/path", + execCommand: func(name string, arg ...string) *exec.Cmd { + cmd := exec.Command("sh", "-c", "echo '"+mockOutput+"'") // Single line output + return cmd + }, + } + + err := m.Run("/mock/igvmBinary") + if err != nil { + t.Errorf("expected no error, got %v", err) + } +} + +func TestIgvmMeasurement_Run_Error(t *testing.T) { + mockOutput := "some error occurred" + + m := &IgvmMeasurement{ + pathToFile: "/invalid/path", + execCommand: func(name string, arg ...string) *exec.Cmd { + cmd := exec.Command("sh", "-c", "echo '"+mockOutput+"' && echo 'extra line' && exit 1") // Simulate multiline error + return cmd + }, + } + + err := m.Run("/mock/igvmBinary") + + if err == nil { + t.Errorf("expected an error, got nil") + } else if !strings.Contains(err.Error(), "error: "+mockOutput) { + t.Errorf("expected error message to contain 'error: %s', got: %s", mockOutput, err) + } +} + +func TestIgvmMeasurement_Stop_NoProcess(t *testing.T) { + m := &IgvmMeasurement{} + + err := m.Stop() + if err == nil || err.Error() != "no running process to stop" { + t.Errorf("expected 'no running process to stop' error, got: %v", err) + } +} + +func TestIgvmMeasurement_Stop_ProcessNil(t *testing.T) { + m := &IgvmMeasurement{ + cmd: &exec.Cmd{}, + } + + err := m.Stop() + if err == nil || err.Error() != "no running process to stop" { + t.Errorf("expected 'no running process to stop' error, got: %v", err) + } +} + +func TestIgvmMeasurement_Stop_Success(t *testing.T) { + process, err := os.StartProcess("/bin/sleep", []string{"sleep", "10"}, &os.ProcAttr{}) + if err != nil { + t.Fatalf("failed to start mock process: %v", err) + } + defer func() { + if err := process.Kill(); err != nil { + t.Logf("Failed to kill process: %v", err) + } + }() + + m := &IgvmMeasurement{ + cmd: &exec.Cmd{Process: process}, + } + + err = m.Stop() + if err != nil { + t.Errorf("expected no error, got: %v", err) + } +} diff --git a/scripts/igvmmeasure/igvm.sh b/scripts/igvmmeasure/igvm.sh new file mode 100755 index 00000000..80fc66af --- /dev/null +++ b/scripts/igvmmeasure/igvm.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Define variables +REPO_URL="https://github.com/coconut-svsm/svsm.git" +TARGET_DIR="svsm" +SUBDIR="igvmmeasure" + +# Clone the repository if it doesn't exist +if [ -d "$TARGET_DIR" ]; then + echo "Repository already exists. Pulling latest changes..." + cd "$TARGET_DIR" && git pull +else + echo "Cloning repository..." + git clone --recurse-submodules "$REPO_URL" +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