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

Add SevProduct raw cert representation. #110

Merged
merged 1 commit into from
Feb 21, 2024
Merged
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
139 changes: 127 additions & 12 deletions abi/abi.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ const (
// a single machine can use both VCEK and VLEK report signing.
AsvkGUID = "00000000-0000-0000-0000-000000000000"

// ExtraPlatformInfoGUID represents more information about the machine collecting an attestation
// report than just the report to help interpret the attestation report.
ExtraPlatformInfoGUID = "ecae0c0f-9502-43b1-afa2-0ae2e0d565b6"
// ExtraPlatformInfoV0Size is the minimum size for an ExtraPlatformInfo blob.
ExtraPlatformInfoV0Size = 8

// CpuidProductMask keeps only the SevProduct-relevant bits from the CPUID(1).EAX result.
CpuidProductMask = 0x0fff0f0f
extendedFamilyShift = 20
extendedModelShift = 16
familyShift = 8
sevExtendedFamily = 0xA
sevFamily = 0xF
milanExtendedModel = 0
genoaExtendedModel = 1

// ExpectedReportVersion is set by the SNP API specification
// https://www.amd.com/system/files/TechDocs/56860.pdf
ExpectedReportVersion = 2
Expand Down Expand Up @@ -874,34 +890,31 @@ func (c *CertTable) Proto() *pb.CertificateChain {
// See assembly implementations in cpuid_*.s
var cpuid func(op uint32) (eax, ebx, ecx, edx uint32)

// SevProduct returns the SEV product enum for the CPU that runs this
// function. Ought to be called from the client, not the verifier.
func SevProduct() *pb.SevProduct {
// CPUID[EAX=1] is the processor info. The only bits we care about are in
// the eax result.
eax, _, _, _ := cpuid(1)
// SevProductFromCpuid1Eax returns the SevProduct that is represented by cpuid(1).eax.
func SevProductFromCpuid1Eax(eax uint32) *pb.SevProduct {
// 31:28 reserved
// 27:20 Extended Family ID
extendedFamily := (eax >> 20) & 0xff
extendedFamily := (eax >> extendedFamilyShift) & 0xff
// 19:16 Extended Model ID
extendedModel := (eax >> 16) & 0xf
extendedModel := (eax >> extendedModelShift) & 0xf
// 15:14 reserved
// 11:8 Family ID
family := (eax >> 8) & 0xf
family := (eax >> familyShift) & 0xf
// 3:0 Stepping
stepping := eax & 0xf
// Ah, Fh, {0h,1h} values from the KDS specification,
// section "Determining the Product Name".
var productName pb.SevProduct_SevProductName
// Product information specified by processor programming reference publications.
if extendedFamily == 0xA && family == 0xF {
if extendedFamily == sevExtendedFamily && family == sevFamily {
switch extendedModel {
case 0:
case milanExtendedModel:
productName = pb.SevProduct_SEV_PRODUCT_MILAN
case 1:
case genoaExtendedModel:
productName = pb.SevProduct_SEV_PRODUCT_GENOA
default:
productName = pb.SevProduct_SEV_PRODUCT_UNKNOWN
stepping = 0 // Reveal nothing.
}
}
return &pb.SevProduct{
Expand All @@ -910,10 +923,112 @@ func SevProduct() *pb.SevProduct {
}
}

// MaskedCpuid1EaxFromSevProduct returns the Cpuid1Eax value expected from the given product
// when masked with CpuidProductMask.
func MaskedCpuid1EaxFromSevProduct(product *pb.SevProduct) uint32 {
var stepping uint32
if product.MachineStepping != nil {
stepping = product.MachineStepping.Value & 0xf
}
extendedFamily := uint32(sevExtendedFamily) << extendedFamilyShift
family := uint32(sevFamily) << familyShift

var extendedModel uint32
switch product.Name {
case pb.SevProduct_SEV_PRODUCT_MILAN:
extendedModel = milanExtendedModel
case pb.SevProduct_SEV_PRODUCT_GENOA:
extendedModel = genoaExtendedModel
default:
return 0
}
return extendedFamily | family | stepping | (extendedModel << extendedModelShift)
}

// SevProduct returns the SEV product enum for the CPU that runs this
// function. Ought to be called from the client, not the verifier.
func SevProduct() *pb.SevProduct {
// CPUID[EAX=1] is the processor info. The only bits we care about are in
// the eax result.
eax, _, _, _ := cpuid(1)
return SevProductFromCpuid1Eax(eax & CpuidProductMask)
}

// MakeExtraPlatformInfo returns the representation of platform info needed on top of what an
// attestation report provides in order to interpret it with the help of the AMD KDS.
func MakeExtraPlatformInfo() *ExtraPlatformInfo {
eax, _, _, _ := cpuid(1)
return &ExtraPlatformInfo{
Size: ExtraPlatformInfoV0Size,
Cpuid1Eax: eax & CpuidProductMask,
}
}

// DefaultSevProduct returns the initial product version for a commercially available AMD SEV-SNP chip.
func DefaultSevProduct() *pb.SevProduct {
return &pb.SevProduct{
Name: pb.SevProduct_SEV_PRODUCT_MILAN,
MachineStepping: &wrapperspb.UInt32Value{Value: 1},
}
}

// ExtraPlatformInfo represents environment information needed to interpret an attestation report when
// the VCEK certificate is not available in the auxblob.
type ExtraPlatformInfo struct {
Size uint32 // Size doubles as Version, following the Linux ABI expansion methodology.
Cpuid1Eax uint32 // Provides product information
}

// ParseExtraPlatformInfo extracts an ExtraPlatformInfo from a blob if it matches expectations, or
// errors.
func ParseExtraPlatformInfo(data []byte) (*ExtraPlatformInfo, error) {
if len(data) < ExtraPlatformInfoV0Size {
return nil, fmt.Errorf("%d bytes is too small for ExtraPlatformInfoSize. Want >= %d bytes",
len(data), ExtraPlatformInfoV0Size)
}
// Populate V0 data.
result := &ExtraPlatformInfo{
Size: binary.LittleEndian.Uint32(data[0:0x04]),
Cpuid1Eax: binary.LittleEndian.Uint32(data[0x04:0x08]),
}
if uint32(len(data)) != result.Size {
return nil, fmt.Errorf("actual size %d bytes != reported size %d bytes", len(data), result.Size)
}
return result, nil
}

// Marshal returns ExtraPlatformInfo in its ABI format or errors.
func (i *ExtraPlatformInfo) Marshal() ([]byte, error) {
if i.Size != ExtraPlatformInfoV0Size {
return nil, fmt.Errorf("unsupported ExtraPlatformInfo size %d bytes", i.Size)
}
data := make([]byte, ExtraPlatformInfoV0Size)
binary.LittleEndian.PutUint32(data[0:0x04], i.Size)
binary.LittleEndian.PutUint32(data[0x04:0x08], i.Cpuid1Eax)
return data, nil
}

// ExtendPlatformCertTable is a convenience function for parsing a CertTable, adding the
// ExtraPlatformInfoGUID entry, and returning the marshaled extended table.
func ExtendPlatformCertTable(data []byte, info *ExtraPlatformInfo) ([]byte, error) {
certs := new(CertTable)
if err := certs.Unmarshal(data); err != nil {
return nil, err
}
// A directly constructed info cannot have a marshaling error.
extra, err := info.Marshal()
if err != nil {
return nil, fmt.Errorf("could not marshal ExtraPlatformInfo: %v", err)
}
certs.Entries = append(certs.Entries, CertTableEntry{
GUID: uuid.Parse(ExtraPlatformInfoGUID),
RawCert: extra,
})
return certs.Marshal(), nil
}

// ExtendedPlatformCertTable is a convenience function for parsing a CertTable, adding the
// ExtraPlatformInfoGUID entry, and returning the marshaled extended table.
func ExtendedPlatformCertTable(data []byte) ([]byte, error) {
return ExtendPlatformCertTable(data, MakeExtraPlatformInfo())
}
100 changes: 88 additions & 12 deletions abi/abi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,15 @@ func TestCpuid(t *testing.T) {
}
}

func TestCertTableProto(t *testing.T) {
type testCertTable struct {
table []byte
extraraw []byte
}

const extraGUID = "00000000-0000-c0de-0000-000000000000"

func testRawCertTable(t testing.TB) *testCertTable {
t.Helper()
headers := make([]CertTableHeaderEntry, 6) // ARK, ASK, VCEK, VLEK, extra, NULL
arkraw := []byte("ark")
askraw := []byte("ask")
Expand All @@ -245,33 +253,40 @@ func TestCertTableProto(t *testing.T) {
headers[3].Offset = headers[2].Offset + headers[2].Length
headers[3].Length = uint32(len(vlekraw))

extraGUID := "00000000-0000-c0de-0000-000000000000"
headers[4].GUID = uuid.Parse(extraGUID)
headers[4].Offset = headers[3].Offset + headers[3].Length
headers[4].Length = uint32(len(extraraw))

result := make([]byte, headers[4].Offset+headers[4].Length)
result := &testCertTable{
table: make([]byte, headers[4].Offset+headers[4].Length),
extraraw: extraraw,
}
for i, cert := range [][]byte{arkraw, askraw, vcekraw, vlekraw, extraraw} {
if err := (&headers[i]).Write(result[i*CertTableEntrySize:]); err != nil {
if err := (&headers[i]).Write(result.table[i*CertTableEntrySize:]); err != nil {
t.Fatalf("could not write header %d: %v", i, err)
}
copy(result[headers[i].Offset:], cert)
copy(result.table[headers[i].Offset:], cert)
}
return result
}

func TestCertTableProto(t *testing.T) {
result := testRawCertTable(t)
c := new(CertTable)
if err := c.Unmarshal(result); err != nil {
t.Errorf("c.Unmarshal(%s) = %v, want nil", hex.Dump(result), err)
if err := c.Unmarshal(result.table); err != nil {
t.Errorf("c.Unmarshal(%s) = %v, want nil", hex.Dump(result.table), err)
}
p := c.Proto()
if len(p.Extras) != 1 {
t.Fatalf("got cert table Extras length %d, want 1", len(p.Extras))
}
gotExtra, ok := p.Extras[extraGUID]
if !ok || !bytes.Equal(gotExtra, extraraw) {
t.Fatalf("Extras[%q] = %v, want %v", extraGUID, gotExtra, extraraw)
if !ok || !bytes.Equal(gotExtra, result.extraraw) {
t.Fatalf("Extras[%q] = %v, want %v", extraGUID, gotExtra, result.extraraw)
}
bs := c.Marshal()
if !bytes.Equal(bs, result) {
t.Errorf("c.Marshal() = %v, want %v", bs, result)
if !bytes.Equal(bs, result.table) {
t.Errorf("c.Marshal() = %v, want %v", bs, result.table)
}
}

Expand Down Expand Up @@ -314,10 +329,71 @@ func TestSevProduct(t *testing.T) {
},
}
for _, tc := range tcs {
cpuid = func(op uint32) (uint32, uint32, uint32, uint32) { return tc.eax, 0, 0, 0 }
cpuid = func(uint32) (uint32, uint32, uint32, uint32) { return tc.eax, 0, 0, 0 }
got := SevProduct()
if diff := cmp.Diff(got, tc.want, protocmp.Transform()); diff != "" {
t.Errorf("SevProduct() = %+v, want %+v. Diff: %s", got, tc.want, diff)
}
got2 := SevProductFromCpuid1Eax(tc.eax)
if diff := cmp.Diff(got2, got, protocmp.Transform()); diff != "" {
t.Errorf("SevProductFromCpuid1Eax(0x%x) = %+v, want %+v. Diff: %s", tc.eax, got2, tc.want, diff)
}
}
}

func TestExtendedPlatformCertTable(t *testing.T) {
oldCpuid := cpuid
defer func() { cpuid = oldCpuid }()
table := testRawCertTable(t).table
oldt := new(CertTable)
_ = oldt.Unmarshal(table)
pold := oldt.Proto()

tcs := []struct {
name string
pname spb.SevProduct_SevProductName
eax uint32
stepping uint32
}{
{name: "Genoa-B2 cruft", pname: spb.SevProduct_SEV_PRODUCT_GENOA, eax: 0x00a10f12, stepping: 2},
{name: "Milan-B1 cruft", pname: spb.SevProduct_SEV_PRODUCT_MILAN, eax: 0x00a00f11, stepping: 1},
{name: "Milan-B0", pname: spb.SevProduct_SEV_PRODUCT_MILAN, eax: 0x00a00f00, stepping: 0},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
cpuid = func(uint32) (uint32, uint32, uint32, uint32) { return tc.eax, 0, 0, 0 }
nextTable, err := ExtendedPlatformCertTable(table)
if err != nil {
t.Fatalf("ExtendedPlatformCertTable(%v) =_, %v. Want nil", table, err)
}

newt := new(CertTable)
if err := newt.Unmarshal(nextTable); err != nil {
t.Fatalf("ExtendedPlatformCertTable(_) _ %v, which could not be unmarshaled: %v", nextTable, err)
}
pnew := newt.Proto()
if len(pnew.Extras) != len(pold.Extras)+1 {
t.Fatalf("ExtendedPlatformCertTable(_) table extras size is %d, want %d", len(pnew.Extras), len(pold.Extras)+1)
}
blob, ok := pnew.Extras[ExtraPlatformInfoGUID]
if !ok {
t.Fatalf("ExtendedPlatfromCertTable(_) table %v extras missing ExtraPlatformInfoGUID", pnew)
}
info, err := ParseExtraPlatformInfo(blob)
if err != nil {
t.Fatalf("ParseExtraPlatformInfo(%v) = _, %v. Want nil", blob, err)
}
if info.Size != ExtraPlatformInfoV0Size {
t.Errorf("ExtraPltaformInfo Size %d is not %d", info.Size, ExtraPlatformInfoV0Size)
}
if info.Cpuid1Eax != tc.eax&CpuidProductMask {
t.Errorf("ExtraPlatformInfo Cpuid1Eax 0x%x is not 0x%x", info.Cpuid1Eax, tc.eax&CpuidProductMask)
}
got := SevProductFromCpuid1Eax(info.Cpuid1Eax)
want := &spb.SevProduct{Name: tc.pname, MachineStepping: &wrapperspb.UInt32Value{Value: tc.stepping}}
if diff := cmp.Diff(got, want, protocmp.Transform()); diff != "" {
t.Errorf("ExtraPlatformInfo Cpuid1Eax product %v is not %v: %s", got, want, diff)
}
})
}
}
5 changes: 5 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ type LeveledQuoteProvider interface {
// GetRawQuote returns a raw report with the given privilege level.
GetRawQuoteAtLevel(reportData [64]byte, vmpl uint) ([]uint8, error)
// Product returns AMD SEV-related CPU information of the calling CPU.
//
// Deprecated: Use abi.ExtraPlatformInfoGUID in raw quote certificate table.
Product() *pb.SevProduct
}

Expand All @@ -57,6 +59,8 @@ type QuoteProvider interface {
// GetRawQuote returns a raw report with the default privilege level.
GetRawQuote(reportData [64]byte) ([]uint8, error)
// Product returns AMD SEV-related CPU information of the calling CPU.
//
// Deprecated: Use abi.ExtraPlatformInfoGUID in the raw quote certificate table.
Product() *pb.SevProduct
}

Expand Down Expand Up @@ -203,6 +207,7 @@ func GetQuoteProto(qp QuoteProvider, reportData [64]byte) (*pb.Attestation, erro
if err != nil {
return nil, err
}
// TODO(Issue#109): Remove when Product is removed.
attestation.Product = qp.Product()
return attestation, nil
}
Expand Down
Loading
Loading