Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

COCOS-365-Add igvm measurement #379

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
34 changes: 34 additions & 0 deletions cli/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"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"
Expand Down Expand Up @@ -557,6 +558,39 @@
return cmd
}

var newMeasurementFunc = igvmmeasure.NewIgvmMeasurement

func (cli *CLI) NewMeasureCmd(igvmBinaryPath string) *cobra.Command {
igvmmeasureCmd := &cobra.Command{
Use: "igvmmeasure <INPUT>",
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")
}

Check warning on line 574 in cli/attestation.go

View check run for this annotation

Codecov / codecov/patch

cli/attestation.go#L573-L574

Added lines #L573 - L574 were not covered by tests

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)
}

Check warning on line 585 in cli/attestation.go

View check run for this annotation

Codecov / codecov/patch

cli/attestation.go#L584-L585

Added lines #L584 - L585 were not covered by tests

return nil
},
}

return igvmmeasureCmd
}

func sevsnpverify(cmd *cobra.Command, args []string) error {
cmd.Println("Checking attestation")

Expand Down
33 changes: 33 additions & 0 deletions cli/attestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"testing"

Expand All @@ -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"
)

Expand Down Expand Up @@ -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()
Expand Down
4 changes: 3 additions & 1 deletion cmd/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -136,6 +137,7 @@ func main() {

// measure.
rootCmd.AddCommand(cmd.NewRootCmd())
rootCmd.AddCommand(cliSVC.NewMeasureCmd(cfg.IgvmBinaryPath))

// Flags
keysCmd.PersistentFlags().StringVarP(
Expand Down
75 changes: 75 additions & 0 deletions pkg/attestation/igvmmeasure/igvmmeasure.go
Original file line number Diff line number Diff line change
@@ -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)
}

Check warning on line 67 in pkg/attestation/igvmmeasure/igvmmeasure.go

View check run for this annotation

Codecov / codecov/patch

pkg/attestation/igvmmeasure/igvmmeasure.go#L66-L67

Added lines #L66 - L67 were not covered by tests

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

Check warning on line 74 in pkg/attestation/igvmmeasure/igvmmeasure.go

View check run for this annotation

Codecov / codecov/patch

pkg/attestation/igvmmeasure/igvmmeasure.go#L73-L74

Added lines #L73 - L74 were not covered by tests
}
121 changes: 121 additions & 0 deletions pkg/attestation/igvmmeasure/igvmmeasure_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Comment on lines +31 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

use assertions for expectations


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)
}
}
Comment on lines +45 to +89
Copy link
Contributor

Choose a reason for hiding this comment

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

combine tests into a single test with multiple test cases as we do for other tests


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)
}
}
30 changes: 30 additions & 0 deletions scripts/igvmmeasure/igvm.sh
Original file line number Diff line number Diff line change
@@ -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
Loading