COCOS-153 - Add host-data option (#163)

* add host_data option

* add CLI hostdata option and rename platform to backend

* move code for computation hash to a function

* rename getComputationHash to computationHash

* add default for backend information field switch
This commit is contained in:
Danko Miladinovic
2024-07-08 15:32:13 +02:00
committed by GitHub
parent 0e236bf2e8
commit 006897a57c
10 changed files with 211 additions and 81 deletions
+144
View File
@@ -0,0 +1,144 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
"os"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/proto/check"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type fieldType int
const (
measurementField fieldType = iota
hostDataField
)
const (
// 0o744 file permission gives RWX permission to the user and only the R permission to others.
filePermission = 0o744
// Length of the expected host data and measurement field in bytes.
hostDataLength = 32
measurementLength = 48
)
var (
errDecode = errors.New("base64 string could not be decoded")
errDataLength = errors.New("data does not have an adequate length")
errReadingBackendInfoFile = errors.New("error while reading the backend information file")
errUnmarshalJSON = errors.New("failed to unmarshal json")
errMarshalJSON = errors.New("failed to marshal json")
errWriteFile = errors.New("failed to write to file")
errBackendField = errors.New("the specified field type does not exist in the backend information")
)
type AttestationConfiguration struct {
SNPPolicy *check.Policy `json:"snp_policy,omitempty"`
RootOfTrust *check.RootOfTrust `json:"root_of_trust,omitempty"`
}
func (cli *CLI) NewBackendCmd() *cobra.Command {
return &cobra.Command{
Use: "backend [command]",
Short: "Change backend information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Change backend information\n\n")
fmt.Printf("Usage:\n %s [command]\n\n", cmd.CommandPath())
fmt.Printf("Available Commands:\n")
// Filter out "completion" command
availableCommands := make([]*cobra.Command, 0)
for _, subCmd := range cmd.Commands() {
if subCmd.Name() != "completion" {
availableCommands = append(availableCommands, subCmd)
}
}
for _, subCmd := range availableCommands {
fmt.Printf(" %-15s%s\n", subCmd.Name(), subCmd.Short)
}
fmt.Printf("\nFlags:\n")
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
fmt.Printf(" -%s, --%s %s\n", flag.Shorthand, flag.Name, flag.Usage)
})
fmt.Printf("\nUse \"%s [command] --help\" for more information about a command.\n", cmd.CommandPath())
},
}
}
func (cli *CLI) NewAddMeasurementCmd() *cobra.Command {
return &cobra.Command{
Use: "measurement",
Short: "Add measurement to the backend info file. The value should be in base64. The second parameter is backend_info.json file",
Example: "measurement <measurement> <backend_info.json>",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
if err := changeAttestationConfiguration(args[1], args[0], measurementLength, measurementField); err != nil {
log.Fatalf("Error could not change measurement data %v", err)
}
},
}
}
func (cli *CLI) NewAddHostDataCmd() *cobra.Command {
return &cobra.Command{
Use: "hostdata",
Short: "Add host data to the backend info file. The value should be in base64. The second parameter is backend_info.json file",
Example: "hostdata <host-data> <backend_info.json>",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
if err := changeAttestationConfiguration(args[1], args[0], hostDataLength, hostDataField); err != nil {
log.Fatalf("Error could not change host data %v", err)
}
},
}
}
func changeAttestationConfiguration(fileName string, base64Data string, expectedLength int, field fieldType) error {
data, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return errDecode
}
if len(data) != expectedLength {
return errDataLength
}
ac := AttestationConfiguration{}
backendInfo, err := os.ReadFile(fileName)
if err != nil {
return errors.Wrap(errReadingBackendInfoFile, err)
}
if err = json.Unmarshal(backendInfo, &ac); err != nil {
return errors.Wrap(errUnmarshalJSON, err)
}
switch field {
case measurementField:
ac.SNPPolicy.Measurement = data
case hostDataField:
ac.SNPPolicy.HostData = data
default:
return errBackendField
}
fileJson, err := json.MarshalIndent(ac, "", " ")
if err != nil {
return errors.Wrap(errMarshalJSON, err)
}
if err = os.WriteFile(fileName, fileJson, filePermission); err != nil {
return errors.Wrap(errWriteFile, err)
}
return nil
}
-57
View File
@@ -1,57 +0,0 @@
// Copyright (c) Ultraviolet
// SPDX-License-Identifier: Apache-2.0
package cli
import (
"encoding/base64"
"encoding/json"
"log"
"os"
"github.com/spf13/cobra"
"github.com/ultravioletrs/cocos/pkg/clients/grpc"
)
const filePermision = 0o644
func (cli *CLI) NewAddMeasurementCmd() *cobra.Command {
return &cobra.Command{
Use: "measurement",
Short: "Add measurement to the platform info file. The value should be in base64. The second parameter is platform_info.json file",
Example: "measurement <measurement> <platform_info.json>",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
measurement, err := base64.StdEncoding.DecodeString(args[0])
if err != nil {
log.Fatalf("Error could not decode base64: %v", err)
}
attestationConfiguration := grpc.AttestationConfiguration{}
manifest, err := os.OpenFile(args[1], os.O_RDWR, filePermision)
if err != nil {
log.Fatalf("Error opening the platform information file: %v", err)
}
defer manifest.Close()
decoder := json.NewDecoder(manifest)
err = decoder.Decode(&attestationConfiguration)
if err != nil {
log.Fatalf("Error decoding the platform information file: %v", err)
}
attestationConfiguration.SNPPolicy.Measurement = measurement
if err = manifest.Truncate(0); err != nil {
log.Fatalf("Error could not truncate platform information JSON file: %v", err)
}
fileJson, err := json.MarshalIndent(attestationConfiguration, "", " ")
if err != nil {
log.Fatalf("Error marshaling the platform information JSON: %v", err)
}
if err = os.WriteFile(manifest.Name(), fileJson, filePermision); err != nil {
log.Fatalf("Error writing into platform information JSON file: %v", err)
}
},
}
}
+6 -1
View File
@@ -102,6 +102,7 @@ func main() {
keysCmd := cliSVC.NewKeysCmd()
attestationCmd := cliSVC.NewAttestationCmd()
backendCmd := cliSVC.NewBackendCmd()
// Agent Commands
rootCmd.AddCommand(cliSVC.NewAlgorithmCmd())
@@ -109,7 +110,7 @@ func main() {
rootCmd.AddCommand(cliSVC.NewResultsCmd())
rootCmd.AddCommand(attestationCmd)
rootCmd.AddCommand(cliSVC.NewFileHashCmd())
rootCmd.AddCommand(cliSVC.NewAddMeasurementCmd())
rootCmd.AddCommand(backendCmd)
rootCmd.AddCommand(keysCmd)
rootCmd.AddCommand(cliSVC.NewCABundleCmd(directoryCachePath))
@@ -126,6 +127,10 @@ func main() {
"User Key type",
)
// Backend information commands
backendCmd.AddCommand(cliSVC.NewAddMeasurementCmd())
backendCmd.AddCommand(cliSVC.NewAddHostDataCmd())
if err := rootCmd.Execute(); err != nil {
logger.Error(fmt.Sprintf("Command execution failed: %s", err))
return
+9 -2
View File
@@ -50,6 +50,7 @@ type SevConfig struct {
ID string `env:"SEV_ID" envDefault:"sev0"`
CBitPos int `env:"SEV_CBITPOS" envDefault:"51"`
ReducedPhysBits int `env:"SEV_REDUCED_PHYS_BITS" envDefault:"1"`
HostData string `env:"HOST_DATA" envDefault:""`
}
type VSockConfig struct {
@@ -177,9 +178,14 @@ func constructQemuArgs(config Config) []string {
if config.EnableSEV || config.EnableSEVSNP {
sevType := "sev-guest"
kernelHash := ""
hostData := ""
if config.EnableSEVSNP {
sevType = "sev-snp-guest"
if config.SevConfig.HostData != "" {
hostData = fmt.Sprintf(",host-data=%s", config.SevConfig.HostData)
}
}
if config.KernelHash {
@@ -187,12 +193,13 @@ func constructQemuArgs(config Config) []string {
}
args = append(args, "-object",
fmt.Sprintf("%s,id=%s,cbitpos=%d,reduced-phys-bits=%d%s",
fmt.Sprintf("%s,id=%s,cbitpos=%d,reduced-phys-bits=%d%s%s",
sevType,
config.SevConfig.ID,
config.SevConfig.CBitPos,
config.SevConfig.ReducedPhysBits,
kernelHash))
kernelHash,
hostData))
args = append(args, "-machine",
fmt.Sprintf("memory-encryption=%s", config.SevConfig.ID))
+23
View File
@@ -4,6 +4,7 @@ package manager
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log/slog"
@@ -15,6 +16,7 @@ import (
"github.com/ultravioletrs/cocos/agent"
"github.com/ultravioletrs/cocos/manager/qemu"
"github.com/ultravioletrs/cocos/pkg/manager"
"golang.org/x/crypto/sha3"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -36,6 +38,9 @@ var (
ErrFailedToAllocatePort = errors.New("failed to allocate free port on host")
errInvalidHashLength = errors.New("hash must be of byte length 32")
// ErrFailedToCalculateHash indicates that agent computation returned an error while calculating the hash of the computation.
ErrFailedToCalculateHash = errors.New("error while calculating the hash of the computation")
)
// Service specifies an API that must be fulfilled by the domain service
@@ -104,6 +109,15 @@ func (ms *managerService) Run(ctx context.Context, c *manager.ComputationRunReq)
}
ms.qemuCfg.HostFwdAgent = agentPort
ch, err := computationHash(ac)
if err != nil {
ms.publishEvent("vm-provision", c.Id, "failed", json.RawMessage{})
return "", errors.Wrap(ErrFailedToCalculateHash, err)
}
// Define host-data value of QEMU for SEV-SNP, with a base64 encoding of the computation hash.
ms.qemuCfg.SevConfig.HostData = base64.StdEncoding.EncodeToString(ch[:])
ms.publishEvent("vm-provision", c.Id, "in-progress", json.RawMessage{})
if _, err = qemu.CreateVM(ctx, ms.qemuCfg); err != nil {
ms.publishEvent("vm-provision", c.Id, "failed", json.RawMessage{})
@@ -156,3 +170,12 @@ func (ms *managerService) publishEvent(event, cmpID, status string, details json
},
}
}
func computationHash(ac agent.Computation) ([32]byte, error) {
jsonData, err := json.Marshal(ac)
if err != nil {
return [32]byte{}, err
}
return sha3.Sum256(jsonData), nil
}
@@ -1,5 +1,5 @@
[package]
name = "platform_info"
name = "backend_info"
version = "0.1.0"
edition = "2021"
@@ -1,7 +1,7 @@
CARGO = cargo
TARGET = target
BUILD_DIR = $(TARGET)/release
BIN_NAME = platform_info
BIN_NAME = backend_info
all: build
@@ -1,4 +1,4 @@
# Rust project for fetching platform info
# Rust project for fetching backend info
This rust project fetches information from the host system needed for validation of the attestation report. It outputs a JSON file that contains the said information.
The JSON file is in a format that can be used with the [go-sev-guest](https://github.com/google/go-sev-guest) library.
@@ -6,7 +6,7 @@ The JSON file is in a format that can be used with the [go-sev-guest](https://gi
Clone `cocos` repository:
```bash
git clone git@github.com:ultravioletrs/cocos.git
cd ./cocos/scripts/platform_info
cd ./cocos/scripts/backend_info
make
```
@@ -15,5 +15,5 @@ Then run the binary. Keep in mind that you have to specify the policy of the Gue
cd ./target/releas
# Run with option --policy (policy is 64 bit number)
./platform_info --policy 196608
./backend_info --policy 196608
```
@@ -6,7 +6,7 @@ use sysinfo::System;
use regex::Regex;
use sev::firmware::host::*;
const PLATFORM_INFO_JSON : &str = "platform_info.json";
const BACKEND_INFO_JSON : &str = "backend_info.json";
#[derive(Serialize)]
struct Vmpl {
@@ -70,7 +70,7 @@ fn get_uint64_from_tcb(tcb_version : &TcbVersion) -> u64 {
}
fn main() {
let matches = Command::new("Platform info")
let matches = Command::new("Backend info")
.about("Processes command line options and outputs a JSON file for Attestation verification")
.arg(Arg::new("policy")
.long("policy")
@@ -130,8 +130,8 @@ fn main() {
};
let json = serde_json::to_string_pretty(&computation).expect("Failed to serialize to JSON");
let mut file = File::create(PLATFORM_INFO_JSON).expect("Failed to create file");
let mut file = File::create(BACKEND_INFO_JSON).expect("Failed to create file");
file.write_all(json.as_bytes()).expect("Failed to write to file");
println!("Computation JSON has been written to {}", PLATFORM_INFO_JSON);
println!("Computation JSON has been written to {}", BACKEND_INFO_JSON);
}
+20 -12
View File
@@ -39,21 +39,29 @@ In the following text, we can see an example of how the CLI tool is used.
```bash
export AGENT_GRPC_URL=localhost:7002
# For attested TLS, the CLI should also be aware of the VM measurement. To
# add the measurement to the .json file that contains the information about
# the platform, run CLI with the measurement in base64 format and the path
# of the platform_info.json file.:
go run cmd/cli/main.go measurement '<measurement>' '<platform_info.json>'
# The platform_info.json file can be generated using Rust by running:
cd scripts/platform_info
# For attested TLS, the CLI needs a file containing the necessary information
# about the SEV-SNP capable backend. This information will be used to verify
# the attestation report received from the agent.
# The backend_info.json file can be generated using Rust by running:
cd scripts/backend_info
make
sudo ./target/release/platform_info --policy 196608 # Default value of the policy should be 196608
# The output file platform_info.json will be generated in the directory from which the executable has been called.
sudo ./target/release/backend_info --policy 196608 # Default value of the policy should be 196608
# The output file backend_info.json will be generated in the directory from which the executable has been called.
cd ../..
# For attested TLS, also define the path to the platform_info.json that contains reference values for the fields of the attestation report
export AGENT_GRPC_MANIFEST=./scripts/platform_info/platform_info.json
# The CLI should also be aware of the VM measurement. To add the measurement
# to the .json file that contains the information about the platform, run CLI
# with the measurement in base64 format and the path of the backend_info.json file.:
go run cmd/cli/main.go backend measurement '<measurement>' '<backend_info.json>'
# If the VM is booted with the QEMU host data option, the CLI should also know
# the host data information. To add the host data to the .json file that contains
# the information about the platform, run CLI with the host data in base64 format
# and the path of the backend_info.json file.:
go run cmd/cli/main.go backend measurement '<host-data>' '<backend_info.json>'
# For attested TLS, also define the path to the backend_info.json that contains reference values for the fields of the attestation report
export AGENT_GRPC_MANIFEST=./scripts/backend_info/backend_info.json
export AGENT_GRPC_ATTESTED_TLS=true
# Retieve Attestation