COCOS-255 - Human readable attestation (#289)

* human readable attestation

Signed-off-by: SammyOina <sammyoina@gmail.com>

* add tests

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix tests

Signed-off-by: SammyOina <sammyoina@gmail.com>

* err check

Signed-off-by: SammyOina <sammyoina@gmail.com>

* fix roundtrip

Signed-off-by: SammyOina <sammyoina@gmail.com>

---------

Signed-off-by: SammyOina <sammyoina@gmail.com>
This commit is contained in:
Sammy Kerata Oina
2024-10-31 18:02:41 +03:00
committed by GitHub
parent 534ad91623
commit f6a93fe2a1
2 changed files with 221 additions and 2 deletions
+54 -2
View File
@@ -4,16 +4,19 @@ package cli
import (
"encoding/hex"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/fatih/color"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/google/go-sev-guest/tools/lib/report"
"github.com/google/go-sev-guest/validate"
"github.com/google/go-sev-guest/verify"
"github.com/google/go-sev-guest/verify/trust"
@@ -41,6 +44,7 @@ const (
size48 = 48
size64 = 64
attestationFilePath = "attestation.bin"
attestationJson = "attestation.json"
sevProductNameMilan = "Milan"
sevProductNameGenoa = "Genoa"
exampleJSONConfig = `
@@ -115,6 +119,8 @@ var (
empty32 = [size32]byte{}
empty64 = [size64]byte{}
defaultReportIdMa = []byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}
getJsonAttestation bool
errReportSize = errors.New("attestation contents too small")
)
func (cli *CLI) NewAttestationCmd() *cobra.Command {
@@ -148,7 +154,7 @@ func (cli *CLI) NewAttestationCmd() *cobra.Command {
}
func (cli *CLI) NewGetAttestationCmd() *cobra.Command {
return &cobra.Command{
cmd := &cobra.Command{
Use: "get",
Short: "Retrieve attestation information from agent. Report data expected in hex enoded string of length 64 bytes.",
Example: "get <report_data>",
@@ -173,7 +179,17 @@ func (cli *CLI) NewGetAttestationCmd() *cobra.Command {
return
}
if err = os.WriteFile(attestationFilePath, result, 0o644); err != nil {
filename := attestationFilePath
if getJsonAttestation {
result, err = attesationToJSON(result)
if err != nil {
printError(cmd, "Error converting attestation to json: %v ❌ ", err)
return
}
filename = attestationJson
}
if err = os.WriteFile(filename, result, 0o644); err != nil {
printError(cmd, "Error saving attestation result: %v ❌ ", err)
return
}
@@ -181,6 +197,35 @@ func (cli *CLI) NewGetAttestationCmd() *cobra.Command {
cmd.Println("Attestation result retrieved and saved successfully!")
},
}
cmd.Flags().BoolVarP(&getJsonAttestation, "json", "j", false, "Get attestation in json format")
return cmd
}
func attesationToJSON(report []byte) ([]byte, error) {
if len(report) < abi.ReportSize {
return nil, errors.Wrap(errReportSize, fmt.Errorf("attestation contents too small (0x%x bytes). Want at least 0x%x bytes", len(report), abi.ReportSize))
}
attestationPB, err := abi.ReportCertsToProto(report[:abi.ReportSize])
if err != nil {
return nil, err
}
return json.MarshalIndent(attestationPB, "", " ")
}
func attesationFromJSON(reportFile []byte) ([]byte, error) {
var attestationPB sevsnp.Attestation
if err := json.Unmarshal(reportFile, &attestationPB); err != nil {
return nil, err
}
return report.Transform(&attestationPB, "bin")
}
func isFileJSON(filename string) bool {
return strings.HasSuffix(filename, ".json")
}
func (cli *CLI) NewValidateAttestationValidationCmd() *cobra.Command {
@@ -564,6 +609,13 @@ func parseFiles() error {
return err
}
attestation = file
if isFileJSON(attestationFile) {
attestation, err = attesationFromJSON(attestation)
if err != nil {
return err
}
}
for _, path := range trustedAuthorKeys {
file, err := os.ReadFile(path)
if err != nil {
+167
View File
@@ -5,14 +5,18 @@ package cli
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/absmach/magistrala/pkg/errors"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/proto/check"
"github.com/google/go-sev-guest/proto/sevsnp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/ultravioletrs/cocos/agent"
"github.com/ultravioletrs/cocos/pkg/sdk/mocks"
)
@@ -188,3 +192,166 @@ func TestGetBase(t *testing.T) {
assert.Equal(t, 2, getBase("0b1010"))
assert.Equal(t, 10, getBase("123"))
}
func TestAttestationToJSON(t *testing.T) {
validReport, err := os.ReadFile("../attestation.bin")
require.NoError(t, err)
tests := []struct {
name string
input []byte
err error
}{
{
name: "Valid report",
input: validReport,
err: nil,
},
{
name: "Invalid report size",
input: make([]byte, abi.ReportSize-1),
err: errReportSize,
},
{
name: "Nil input",
input: nil,
err: errReportSize,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := attesationToJSON(tt.input)
assert.True(t, errors.Contains(err, tt.err))
if tt.err != nil {
assert.Nil(t, got)
return
}
require.NotNil(t, got)
var js map[string]interface{}
err = json.Unmarshal(got, &js)
assert.NoError(t, err)
})
}
}
func TestAttestationFromJSON(t *testing.T) {
tests := []struct {
name string
input []byte
err error
validate func(t *testing.T, output []byte)
}{
{
name: "Valid JSON",
input: func() []byte {
att := &sevsnp.Attestation{
Report: &sevsnp.Report{
CurrentTcb: 1,
FamilyId: make([]byte, 16),
ImageId: make([]byte, 16),
ReportData: make([]byte, 64),
Measurement: make([]byte, 48),
HostData: make([]byte, 32),
IdKeyDigest: make([]byte, 48),
AuthorKeyDigest: make([]byte, 48),
ReportId: make([]byte, 32),
ReportIdMa: make([]byte, 32),
ChipId: make([]byte, 64),
Signature: make([]byte, 512),
},
}
data, err := json.Marshal(att)
require.NoError(t, err)
return data
}(),
err: nil,
validate: func(t *testing.T, output []byte) {
assert.NotEmpty(t, output)
},
},
{
name: "Invalid JSON",
input: []byte(`{"invalid": json`),
err: errors.New("invalid character 'j' looking for beginning of value"),
validate: func(t *testing.T, output []byte) {
assert.Nil(t, output)
},
},
{
name: "Empty input",
input: []byte{},
err: errors.New("unexpected end of JSON input"),
validate: func(t *testing.T, output []byte) {
assert.Nil(t, output)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := attesationFromJSON(tt.input)
assert.True(t, errors.Contains(err, tt.err))
tt.validate(t, got)
})
}
}
func TestIsFileJSON(t *testing.T) {
tests := []struct {
name string
filename string
want bool
}{
{
name: "Valid JSON extension",
filename: "test.json",
want: true,
},
{
name: "Valid JSON extension with path",
filename: "/path/to/test.json",
want: true,
},
{
name: "Invalid extension",
filename: "test.txt",
want: false,
},
{
name: "No extension",
filename: "test",
want: false,
},
{
name: "JSON in filename",
filename: "json.txt",
want: false,
},
{
name: "Empty string",
filename: "",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isFileJSON(tt.filename)
assert.Equal(t, tt.want, got)
})
}
}
func TestRoundTrip(t *testing.T) {
originalReport, err := os.ReadFile("../attestation.bin")
require.NoError(t, err)
jsonData, err := attesationToJSON(originalReport)
require.NoError(t, err)
require.NotNil(t, jsonData)
roundTripReport, err := attesationFromJSON(jsonData)
require.NoError(t, err)
require.NotNil(t, roundTripReport)
}