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
This commit is contained in:
dorcaslitunya
2025-03-12 12:24:51 +03:00
committed by GitHub
parent 67f939fc66
commit 4bb732ebf9
9 changed files with 316 additions and 7 deletions
+7 -1
View File
@@ -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)
+19
View File
@@ -100,3 +100,22 @@ When defining the manifest dataset and algorithm checksums are required. This ca
```bash
./build/cocos-cli checksum <path_to_dataset_or_algorithm>
```
#### 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
```
+22
View File
@@ -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 <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")
}
inputFile := args[0]
return cli.measurement.Run(inputFile)
},
}
return igvmmeasureCmd
}
func sevsnpverify(cmd *cobra.Command, args []string) error {
cmd.Println("Checking attestation")
+56 -3
View File
@@ -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()
+4 -1
View File
@@ -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,
}
}
+12 -2
View File
@@ -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(
@@ -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
}
@@ -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)
}
}
})
}
}
+40
View File
@@ -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"