diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 649b514..ad994d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: build: strategy: matrix: - go-version: [1.19.x] + go-version: [1.19.x, 1.20.x] os: [macos-latest, ubuntu-latest] name: Build/Test (${{ matrix.os}}, Go ${{ matrix.go-version }}) diff --git a/README.md b/README.md index 6a0880f..df79d3c 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,10 @@ This function's name is selected to discourage its use in a Cloud setting. See Closes the device. +### `func (d Device) Product() *spb.SevProduct` + +Returns a representation of CPU info relevant to the AMD SEV product version. + ## `verify` This library will check the signature and basic well-formedness properties of an @@ -179,6 +183,9 @@ P-384 public keys are considered. All other certificates are quietly ignored. * `TrustedIDKeyHashes`: An array of SHA-384 hashes of the SEV-SNP API format for an ECDSA public key. Has the same validation behavior as `TrustedIDKeys`. +* `Product`: A replacement or supplemental `SevProduct` value to use for + a given attestation or report. If nil, uses the information present in + the attestation proto, or provides a default `Milan-B0` value. ## License diff --git a/abi/abi.go b/abi/abi.go index ea7aec1..bfeef84 100644 --- a/abi/abi.go +++ b/abi/abi.go @@ -96,7 +96,7 @@ const ( // The following GUIDs are defined by the AMD Guest-host communication block specification // for MSG_REPORT_REQ: - // https://developer.amd.com/wp-content/resources/56421.pdf + // https://www.amd.com/system/files/TechDocs/56421-guest-hypervisor-communication-block-standardization.pdf // VcekGUID is the Versioned Chip Endorsement Key GUID VcekGUID = "63da758d-e664-4564-adc5-f4b93be8accd" @@ -702,3 +702,47 @@ func (c *CertTable) Proto() *pb.CertificateChain { FirmwareCert: firmware, } } + +// cpuid returns the 4 register results of CPUID[EAX=op,ECX=0]. +// 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) + // 31:28 reserved + // 27:20 Extended Family ID + extendedFamily := (eax >> 20) & 0xff + // 19:16 Extended Model ID + extendedModel := (eax >> 16) & 0xf + // 15:14 reserved + // 11:8 Family ID + family := (eax >> 8) & 0xf + // 7:4 Model, 3:0 Stepping + modelStepping := eax & 0xff + // Ah, Fh, {0h,1h} values from the KDS specification, + // section "Determining the Product Name". + var productName pb.SevProduct_SevProductName + if extendedFamily == 0xA && family == 0xF { + switch extendedModel { + case 0: + productName = pb.SevProduct_SEV_PRODUCT_MILAN + case 1: + productName = pb.SevProduct_SEV_PRODUCT_GENOA + default: + productName = pb.SevProduct_SEV_PRODUCT_UNKNOWN + } + } + return &pb.SevProduct{ + Name: productName, + ModelStepping: modelStepping, + } +} + +// 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, ModelStepping: 0xB0} +} diff --git a/abi/cpuid_amd64.s b/abi/cpuid_amd64.s new file mode 100644 index 0000000..e436d2b --- /dev/null +++ b/abi/cpuid_amd64.s @@ -0,0 +1,26 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//+build amd64,!gccgo + +// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32) +TEXT ·asmCpuid(SB), 7, $0 + XORQ CX, CX + MOVL op+0(FP), AX + CPUID + MOVL AX, eax+8(FP) + MOVL BX, ebx+12(FP) + MOVL CX, ecx+16(FP) + MOVL DX, edx+20(FP) + RET diff --git a/client/client.go b/client/client.go index 469e325..5f48b60 100644 --- a/client/client.go +++ b/client/client.go @@ -29,9 +29,14 @@ var sevGuestPath = flag.String("sev_guest_device_path", "default", // Device encapsulates the possible commands to the AMD SEV guest device. type Device interface { + // Open prepares the Device from the given path. Open(path string) error + // Close releases the device resource. Close() error + // Ioctl performs the given command with the given argument. Ioctl(command uintptr, argument any) (uintptr, error) + // Product returns AMD SEV-related CPU information of the calling CPU. + Product() *pb.SevProduct } // UseDefaultSevGuest returns true iff -sev_guest_device_path=default. @@ -169,7 +174,11 @@ func GetExtendedReportAtVmpl(d Device, reportData [64]byte, vmpl int) (*pb.Attes if err := certs.Unmarshal(certBytes); err != nil { return nil, err } - return &pb.Attestation{Report: report, CertificateChain: certs.Proto()}, nil + return &pb.Attestation{ + Report: report, + CertificateChain: certs.Proto(), + Product: d.Product(), + }, nil } // GetExtendedReport gets an extended attestation report at VMPL0 into a structured type. diff --git a/client/client_linux.go b/client/client_linux.go index f98636f..4a83e21 100644 --- a/client/client_linux.go +++ b/client/client_linux.go @@ -22,7 +22,9 @@ import ( "fmt" "time" + "github.com/google/go-sev-guest/abi" labi "github.com/google/go-sev-guest/client/linuxabi" + spb "github.com/google/go-sev-guest/proto/sevsnp" "golang.org/x/sys/unix" ) @@ -116,3 +118,8 @@ func (d *LinuxDevice) Ioctl(command uintptr, req any) (uintptr, error) { } return 0, fmt.Errorf("unexpected request value: %v", req) } + +// Product returns the current CPU's associated AMD SEV product information. +func (d *LinuxDevice) Product() *spb.SevProduct { + return abi.SevProduct() +} diff --git a/client/client_macos.go b/client/client_macos.go index af2c91f..824272a 100644 --- a/client/client_macos.go +++ b/client/client_macos.go @@ -18,6 +18,8 @@ package client import ( "fmt" + + spb "github.com/google/go-sev-guest/proto/sevsnp" ) // DefaultSevGuestDevicePath is the platform's usual device path to the SEV guest. @@ -45,3 +47,8 @@ func (*MacOSDevice) Close() error { func (*MacOSDevice) Ioctl(_ uintptr, _ any) (uintptr, error) { return 0, fmt.Errorf("MacOS is unsupported") } + +// Product is not supported on MacOS. +func (*MacOSDevice) Product() *spb.SevProduct { + return &spb.SevProduct{} +} diff --git a/client/client_windows.go b/client/client_windows.go index 9f70cda..14436f5 100644 --- a/client/client_windows.go +++ b/client/client_windows.go @@ -18,6 +18,8 @@ package client import ( "fmt" + + spb "github.com/google/go-sev-guest/proto/sevsnp" ) // WindowsDevice implements the Device interface with Linux ioctls. @@ -43,3 +45,8 @@ func (*WindowsDevice) Ioctl(_ uintptr, _ any) (uintptr, error) { // The GuestAttestation library on Windows is closed source. return 0, fmt.Errorf("Windows is unsupported") } + +// Product is not supported on Windows. +func (*MacOSDevice) Product() *spb.SevProduct { + return &spb.SevProduct{} +} diff --git a/kds/kds.go b/kds/kds.go index 4a1a112..416e04e 100644 --- a/kds/kds.go +++ b/kds/kds.go @@ -27,6 +27,7 @@ import ( "strings" "github.com/google/go-sev-guest/abi" + pb "github.com/google/go-sev-guest/proto/sevsnp" "go.uber.org/multierr" ) @@ -58,7 +59,8 @@ var ( // OidUcodeSpl is the x509v3 extension for VCEK microcode security patch level. OidUcodeSpl = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 3704, 1, 3, 8}) // OidHwid is the x509v3 extension for VCEK certificate associated hardware identifier. - OidHwid = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 3704, 1, 4}) + OidHwid = asn1.ObjectIdentifier([]int{1, 3, 6, 1, 4, 1, 3704, 1, 4}) + authorityKeyOid = asn1.ObjectIdentifier([]int{2, 5, 29, 35}) // Short forms of the asn1 Object identifiers to use in map lookups, since []int are invalid key // types. @@ -516,3 +518,51 @@ func ParseVCEKCertURL(kdsurl string) (VCEKCert, error) { result.TCB = uint64(tcb) return result, nil } + +// ProductString returns the KDS product argument to use for the product associated with +// an attestation report proto. +func ProductString(product *pb.SevProduct) string { + if product == nil { + product = abi.DefaultSevProduct() + } + switch product.Name { + case pb.SevProduct_SEV_PRODUCT_MILAN: + return "Milan" + case pb.SevProduct_SEV_PRODUCT_GENOA: + return "Genoa" + default: + return "Unknown" + } +} + +// ProductName returns the expected productName extension value for the product associated +// with an attestation report proto. +func ProductName(product *pb.SevProduct) string { + if product == nil { + product = abi.DefaultSevProduct() + } + return fmt.Sprintf("%s-%02X", ProductString(product), product.ModelStepping) +} + +// ParseProductName returns the KDS project input value, and the model, stepping numbers represented +// by a given VCEK productName extension value, or an error. +func ParseProductName(productName string) (*pb.SevProduct, error) { + subs := strings.SplitN(productName, "-", 2) + if len(subs) != 2 { + return nil, fmt.Errorf("productName value %q does not match the expected Name-ModelStepping format", productName) + } + var name pb.SevProduct_SevProductName + switch subs[0] { + case "Milan": + name = pb.SevProduct_SEV_PRODUCT_MILAN + case "Genoa": + name = pb.SevProduct_SEV_PRODUCT_GENOA + default: + return nil, fmt.Errorf("unknown AMD SEV product: %q", subs[0]) + } + modelStepping, err := strconv.ParseUint(subs[1], 16, 8) + if err != nil { + return nil, fmt.Errorf("model stepping in productName is not a hexadecimal byte: %q", subs[1]) + } + return &pb.SevProduct{Name: name, ModelStepping: uint32(modelStepping)}, nil +} diff --git a/kds/kds_test.go b/kds/kds_test.go index 30ef83d..194e131 100644 --- a/kds/kds_test.go +++ b/kds/kds_test.go @@ -23,6 +23,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-sev-guest/abi" + pb "github.com/google/go-sev-guest/proto/sevsnp" + "google.golang.org/protobuf/testing/protocmp" ) func TestProductCertChainURL(t *testing.T) { @@ -156,3 +158,100 @@ func TestParseVCEKCertURL(t *testing.T) { }) } } + +func TestProductName(t *testing.T) { + tcs := []struct { + name string + input *pb.SevProduct + want string + }{ + { + name: "nil", + want: "Milan-B0", + }, + { + name: "unknown", + input: &pb.SevProduct{ + ModelStepping: 0x1A, + }, + want: "Unknown-1A", + }, + { + name: "Milan-00", + input: &pb.SevProduct{ + Name: pb.SevProduct_SEV_PRODUCT_MILAN, + }, + want: "Milan-00", + }, + { + name: "Genoa-FF", + input: &pb.SevProduct{ + Name: pb.SevProduct_SEV_PRODUCT_GENOA, + ModelStepping: 0xFF, + }, + want: "Genoa-FF", + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + if got := ProductName(tc.input); got != tc.want { + t.Errorf("ProductName(%v) = %q, want %q", tc.input, got, tc.want) + } + }) + } +} + +func TestParseProductName(t *testing.T) { + tcs := []struct { + name string + input string + want *pb.SevProduct + wantErr string + }{ + { + name: "empty", + wantErr: "does not match", + }, + { + name: "Too much", + input: "Milan-B0-and some extra", + wantErr: "not a hexadecimal byte: \"B0-and some extra\"", + }, + { + name: "start-", + input: "-00", + wantErr: "unknown AMD SEV product: \"\"", + }, + { + name: "end-", + input: "Milan-", + wantErr: "model stepping in productName is not a hexadecimal byte: \"\"", + }, + { + name: "Too big", + input: "Milan-100", + wantErr: "model stepping in productName is not a hexadecimal byte: \"100\"", + }, + { + name: "happy path", + input: "Genoa-9C", + want: &pb.SevProduct{ + Name: pb.SevProduct_SEV_PRODUCT_GENOA, + ModelStepping: 0x9C, + }, + }, + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + got, err := ParseProductName(tc.input) + if (err == nil && tc.wantErr != "") || (err != nil && (tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr))) { + t.Fatalf("ParseProductName(%v) errored unexpectedly: %v, want %q", tc.input, err, tc.wantErr) + } + if tc.wantErr == "" { + if diff := cmp.Diff(got, tc.want, protocmp.Transform()); diff != "" { + t.Fatalf("ParseProductName(%v) = %v, want %v\nDiff: %s", tc.input, got, tc.want, diff) + } + } + }) + } +} diff --git a/proto/sevsnp.proto b/proto/sevsnp.proto index 216a3b8..5d7c5f1 100644 --- a/proto/sevsnp.proto +++ b/proto/sevsnp.proto @@ -71,8 +71,25 @@ message CertificateChain { bytes firmware_cert = 4; } +// The CPUID[EAX=1] version information includes product info as described in +// the AMD KDS specification. The product name, model, and stepping values are +// important for determining the required parameters to KDS when requesting the +// endorsement key's certificate. +message SevProduct { + enum SevProductName { + SEV_PRODUCT_UNKNOWN = 0; + SEV_PRODUCT_MILAN = 1; + SEV_PRODUCT_GENOA = 2; + } + + SevProductName name = 1; + uint32 model_stepping = 2; // Must be a byte +} + message Attestation { Report report = 1; CertificateChain certificate_chain = 2; + + SevProduct product = 3; } diff --git a/proto/sevsnp/sevsnp.pb.go b/proto/sevsnp/sevsnp.pb.go index 3cdf2bb..dfdf85f 100644 --- a/proto/sevsnp/sevsnp.pb.go +++ b/proto/sevsnp/sevsnp.pb.go @@ -37,6 +37,55 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type SevProduct_SevProductName int32 + +const ( + SevProduct_SEV_PRODUCT_UNKNOWN SevProduct_SevProductName = 0 + SevProduct_SEV_PRODUCT_MILAN SevProduct_SevProductName = 1 + SevProduct_SEV_PRODUCT_GENOA SevProduct_SevProductName = 2 +) + +// Enum value maps for SevProduct_SevProductName. +var ( + SevProduct_SevProductName_name = map[int32]string{ + 0: "SEV_PRODUCT_UNKNOWN", + 1: "SEV_PRODUCT_MILAN", + 2: "SEV_PRODUCT_GENOA", + } + SevProduct_SevProductName_value = map[string]int32{ + "SEV_PRODUCT_UNKNOWN": 0, + "SEV_PRODUCT_MILAN": 1, + "SEV_PRODUCT_GENOA": 2, + } +) + +func (x SevProduct_SevProductName) Enum() *SevProduct_SevProductName { + p := new(SevProduct_SevProductName) + *p = x + return p +} + +func (x SevProduct_SevProductName) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (SevProduct_SevProductName) Descriptor() protoreflect.EnumDescriptor { + return file_sevsnp_proto_enumTypes[0].Descriptor() +} + +func (SevProduct_SevProductName) Type() protoreflect.EnumType { + return &file_sevsnp_proto_enumTypes[0] +} + +func (x SevProduct_SevProductName) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use SevProduct_SevProductName.Descriptor instead. +func (SevProduct_SevProductName) EnumDescriptor() ([]byte, []int) { + return file_sevsnp_proto_rawDescGZIP(), []int{2, 0} +} + // Report represents an SEV-SNP ATTESTATION_REPORT, specified in SEV SNP API // // documentation https://www.amd.com/system/files/TechDocs/56860.pdf @@ -382,6 +431,65 @@ func (x *CertificateChain) GetFirmwareCert() []byte { return nil } +// The CPUID[EAX=1] version information includes product info as described in +// the AMD KDS specification. The product name, model, and stepping values are +// important for determining the required parameters to KDS when requesting the +// endorsement key's certificate. +type SevProduct struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name SevProduct_SevProductName `protobuf:"varint,1,opt,name=name,proto3,enum=sevsnp.SevProduct_SevProductName" json:"name,omitempty"` + ModelStepping uint32 `protobuf:"varint,2,opt,name=model_stepping,json=modelStepping,proto3" json:"model_stepping,omitempty"` // Must be a byte +} + +func (x *SevProduct) Reset() { + *x = SevProduct{} + if protoimpl.UnsafeEnabled { + mi := &file_sevsnp_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SevProduct) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SevProduct) ProtoMessage() {} + +func (x *SevProduct) ProtoReflect() protoreflect.Message { + mi := &file_sevsnp_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SevProduct.ProtoReflect.Descriptor instead. +func (*SevProduct) Descriptor() ([]byte, []int) { + return file_sevsnp_proto_rawDescGZIP(), []int{2} +} + +func (x *SevProduct) GetName() SevProduct_SevProductName { + if x != nil { + return x.Name + } + return SevProduct_SEV_PRODUCT_UNKNOWN +} + +func (x *SevProduct) GetModelStepping() uint32 { + if x != nil { + return x.ModelStepping + } + return 0 +} + type Attestation struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -389,12 +497,13 @@ type Attestation struct { Report *Report `protobuf:"bytes,1,opt,name=report,proto3" json:"report,omitempty"` CertificateChain *CertificateChain `protobuf:"bytes,2,opt,name=certificate_chain,json=certificateChain,proto3" json:"certificate_chain,omitempty"` + Product *SevProduct `protobuf:"bytes,3,opt,name=product,proto3" json:"product,omitempty"` } func (x *Attestation) Reset() { *x = Attestation{} if protoimpl.UnsafeEnabled { - mi := &file_sevsnp_proto_msgTypes[2] + mi := &file_sevsnp_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -407,7 +516,7 @@ func (x *Attestation) String() string { func (*Attestation) ProtoMessage() {} func (x *Attestation) ProtoReflect() protoreflect.Message { - mi := &file_sevsnp_proto_msgTypes[2] + mi := &file_sevsnp_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -420,7 +529,7 @@ func (x *Attestation) ProtoReflect() protoreflect.Message { // Deprecated: Use Attestation.ProtoReflect.Descriptor instead. func (*Attestation) Descriptor() ([]byte, []int) { - return file_sevsnp_proto_rawDescGZIP(), []int{2} + return file_sevsnp_proto_rawDescGZIP(), []int{3} } func (x *Attestation) GetReport() *Report { @@ -437,6 +546,13 @@ func (x *Attestation) GetCertificateChain() *CertificateChain { return nil } +func (x *Attestation) GetProduct() *SevProduct { + if x != nil { + return x.Product + } + return nil +} + var File_sevsnp_proto protoreflect.FileDescriptor var file_sevsnp_proto_rawDesc = []byte{ @@ -509,18 +625,33 @@ var file_sevsnp_proto_rawDesc = []byte{ 0x01, 0x28, 0x0c, 0x52, 0x07, 0x61, 0x72, 0x6b, 0x43, 0x65, 0x72, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x66, 0x69, 0x72, 0x6d, 0x77, 0x61, 0x72, 0x65, 0x43, 0x65, 0x72, - 0x74, 0x22, 0x7c, 0x0a, 0x0b, 0x41, 0x74, 0x74, 0x65, 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x26, 0x0a, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0e, 0x2e, 0x73, 0x65, 0x76, 0x73, 0x6e, 0x70, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x45, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, - 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x73, 0x65, 0x76, 0x73, 0x6e, 0x70, 0x2e, 0x43, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x10, 0x63, - 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x42, - 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x67, 0x6f, 0x2d, 0x73, 0x65, 0x76, 0x2d, 0x67, 0x75, 0x65, 0x73, - 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x76, 0x73, 0x6e, 0x70, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x22, 0xc3, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x76, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, + 0x12, 0x35, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, + 0x2e, 0x73, 0x65, 0x76, 0x73, 0x6e, 0x70, 0x2e, 0x53, 0x65, 0x76, 0x50, 0x72, 0x6f, 0x64, 0x75, + 0x63, 0x74, 0x2e, 0x53, 0x65, 0x76, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, + 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x5f, 0x73, 0x74, 0x65, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x0d, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x53, 0x74, 0x65, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x22, 0x57, + 0x0a, 0x0e, 0x53, 0x65, 0x76, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x17, 0x0a, 0x13, 0x53, 0x45, 0x56, 0x5f, 0x50, 0x52, 0x4f, 0x44, 0x55, 0x43, 0x54, 0x5f, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x45, 0x56, + 0x5f, 0x50, 0x52, 0x4f, 0x44, 0x55, 0x43, 0x54, 0x5f, 0x4d, 0x49, 0x4c, 0x41, 0x4e, 0x10, 0x01, + 0x12, 0x15, 0x0a, 0x11, 0x53, 0x45, 0x56, 0x5f, 0x50, 0x52, 0x4f, 0x44, 0x55, 0x43, 0x54, 0x5f, + 0x47, 0x45, 0x4e, 0x4f, 0x41, 0x10, 0x02, 0x22, 0xaa, 0x01, 0x0a, 0x0b, 0x41, 0x74, 0x74, 0x65, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x73, 0x65, 0x76, 0x73, 0x6e, 0x70, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, + 0x45, 0x0a, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x73, 0x65, 0x76, + 0x73, 0x6e, 0x70, 0x2e, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, + 0x68, 0x61, 0x69, 0x6e, 0x52, 0x10, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x2c, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x73, 0x65, 0x76, 0x73, 0x6e, 0x70, + 0x2e, 0x53, 0x65, 0x76, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x52, 0x07, 0x70, 0x72, 0x6f, + 0x64, 0x75, 0x63, 0x74, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x67, 0x6f, 0x2d, 0x73, 0x65, 0x76, + 0x2d, 0x67, 0x75, 0x65, 0x73, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x76, + 0x73, 0x6e, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -535,20 +666,25 @@ func file_sevsnp_proto_rawDescGZIP() []byte { return file_sevsnp_proto_rawDescData } -var file_sevsnp_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_sevsnp_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_sevsnp_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_sevsnp_proto_goTypes = []interface{}{ - (*Report)(nil), // 0: sevsnp.Report - (*CertificateChain)(nil), // 1: sevsnp.CertificateChain - (*Attestation)(nil), // 2: sevsnp.Attestation + (SevProduct_SevProductName)(0), // 0: sevsnp.SevProduct.SevProductName + (*Report)(nil), // 1: sevsnp.Report + (*CertificateChain)(nil), // 2: sevsnp.CertificateChain + (*SevProduct)(nil), // 3: sevsnp.SevProduct + (*Attestation)(nil), // 4: sevsnp.Attestation } var file_sevsnp_proto_depIdxs = []int32{ - 0, // 0: sevsnp.Attestation.report:type_name -> sevsnp.Report - 1, // 1: sevsnp.Attestation.certificate_chain:type_name -> sevsnp.CertificateChain - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 0, // 0: sevsnp.SevProduct.name:type_name -> sevsnp.SevProduct.SevProductName + 1, // 1: sevsnp.Attestation.report:type_name -> sevsnp.Report + 2, // 2: sevsnp.Attestation.certificate_chain:type_name -> sevsnp.CertificateChain + 3, // 3: sevsnp.Attestation.product:type_name -> sevsnp.SevProduct + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_sevsnp_proto_init() } @@ -582,6 +718,18 @@ func file_sevsnp_proto_init() { } } file_sevsnp_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SevProduct); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sevsnp_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Attestation); i { case 0: return &v.state @@ -599,13 +747,14 @@ func file_sevsnp_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_sevsnp_proto_rawDesc, - NumEnums: 0, - NumMessages: 3, + NumEnums: 1, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, GoTypes: file_sevsnp_proto_goTypes, DependencyIndexes: file_sevsnp_proto_depIdxs, + EnumInfos: file_sevsnp_proto_enumTypes, MessageInfos: file_sevsnp_proto_msgTypes, }.Build() File_sevsnp_proto = out.File diff --git a/testing/mocks.go b/testing/mocks.go index 2c7cb66..7afabb7 100644 --- a/testing/mocks.go +++ b/testing/mocks.go @@ -18,9 +18,11 @@ import ( "encoding/hex" "fmt" "syscall" + "testing" "github.com/google/go-sev-guest/abi" labi "github.com/google/go-sev-guest/client/linuxabi" + spb "github.com/google/go-sev-guest/proto/sevsnp" "github.com/pkg/errors" "golang.org/x/sys/unix" ) @@ -39,6 +41,7 @@ type Device struct { Keys map[string][]byte Certs []byte Signer *AmdSigner + SevProduct *spb.SevProduct } // Open changes the mock device's state to open. @@ -137,16 +140,72 @@ func (d *Device) Ioctl(command uintptr, req any) (uintptr, error) { return 0, fmt.Errorf("unexpected request: %v", req) } -// Getter represents a static server for request/respond url -> body contents. +// Product returns the mocked product info or the default. +func (d *Device) Product() *spb.SevProduct { + if d.SevProduct == nil { + return &spb.SevProduct{ + Name: spb.SevProduct_SEV_PRODUCT_MILAN, + ModelStepping: 0xB0, + } + } + return d.SevProduct +} + +// GetResponse controls how often (Occurrences) a certain response should be +// provided. +type GetResponse struct { + Occurrences uint + Body []byte + Error error +} + +// Getter is a mock for HTTPSGetter interface that sequentially +// returns the configured responses for the provided URL. Responses are returned +// as a queue, i.e., always serving from index 0. type Getter struct { - Responses map[string][]byte + Responses map[string][]GetResponse } -// Get returns a registered response for a given URL. +// SimpleGetter constructs a static server from url -> body responses. +// For more elaborate tests, construct a custom Getter. +func SimpleGetter(responses map[string][]byte) *Getter { + getter := &Getter{ + Responses: make(map[string][]GetResponse), + } + for key, value := range responses { + getter.Responses[key] = []GetResponse{ + { + Occurrences: ^uint(0), + Body: value, + Error: nil, + }, + } + } + return getter +} + +// Get the next response body and error. The response is also removed, +// if it has been requested the configured number of times. func (g *Getter) Get(url string) ([]byte, error) { - v, ok := g.Responses[url] - if !ok { + resp, ok := g.Responses[url] + if !ok || len(resp) == 0 { return nil, fmt.Errorf("404: %s", url) } - return v, nil + body := resp[0].Body + err := resp[0].Error + resp[0].Occurrences-- + if resp[0].Occurrences == 0 { + g.Responses[url] = resp[1:] + } + return body, err +} + +// Done checks that all configured responses have been consumed, and errors +// otherwise. +func (g *Getter) Done(t testing.TB) { + for key := range g.Responses { + if len(g.Responses[key]) != 0 { + t.Errorf("Prepared response for '%s' not retrieved.", key) + } + } } diff --git a/validate/validate.go b/validate/validate.go index 1a6fe2f..0b978d8 100644 --- a/validate/validate.go +++ b/validate/validate.go @@ -91,7 +91,7 @@ type Options struct { // provided. TrustedIDKeys []*x509.Certificate // TrustedIDKeyHashes is an array of SHA-384 hashes of trusted ID signer keys's public key in - // SEV-SNP API format. Not required if TrustedKeyKeys is provided. + // SEV-SNP API format. Not required if TrustedIDKeys is provided. TrustedIDKeyHashes [][]byte } diff --git a/validate/validate_test.go b/validate/validate_test.go index 41ec2be..30cdc59 100644 --- a/validate/validate_test.go +++ b/validate/validate_test.go @@ -235,12 +235,12 @@ func TestValidateSnpAttestation(t *testing.T) { if err != nil { t.Fatal(err) } - getter := &test.Getter{ - Responses: map[string][]byte{ + getter := test.SimpleGetter( + map[string][]byte{ "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": rootBytes, "https://kdsintf.amd.com/vcek/v1/Milan/0a0b0c0d0e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010203040506?blSPL=31&teeSPL=127&snpSPL=112&ucodeSPL=146": sign.Vcek.Raw, }, - } + ) attestationFn := func(nonce [64]byte) *spb.Attestation { report, err := sg.GetReport(device, nonce) if err != nil { @@ -271,12 +271,14 @@ func TestValidateSnpAttestation(t *testing.T) { if err != nil { t.Fatal(err) } - return &spb.Attestation{Report: report, + return &spb.Attestation{ + Report: report, CertificateChain: &spb.CertificateChain{ AskCert: sign0.Ask.Raw, ArkCert: sign0.Ark.Raw, VcekCert: sign0.Vcek.Raw, - }} + }, + } }(), opts: &Options{ReportData: nonce0s1[:], GuestPolicy: abi.SnpPolicy{Debug: true}}, }, diff --git a/verify/trust/trust.go b/verify/trust/trust.go index e3b8fbc..ee88586 100644 --- a/verify/trust/trust.go +++ b/verify/trust/trust.go @@ -19,6 +19,7 @@ import ( "context" "crypto/x509" _ "embed" + "encoding/pem" "fmt" "io" "net/http" @@ -29,11 +30,12 @@ import ( "github.com/google/go-sev-guest/abi" "github.com/google/go-sev-guest/kds" "github.com/google/logger" + "go.uber.org/multierr" ) var ( // DefaultRootCerts holds AMD's SEV API certificate format for ASK and ARK keys as published here - // https://developer.amd.com/wp-content/resources/ask_ark_milan.cert + // https://download.amd.com/developer/eula/sev/ask_ark_milan.cert DefaultRootCerts map[string]*AMDRootCerts // The ASK and ARK certificates are embedded since they do not have an expiration date. The KDS @@ -102,7 +104,7 @@ func (n *SimpleHTTPSGetter) Get(url string) ([]byte, error) { if err != nil { return nil, err } else if resp.StatusCode >= 300 { - return nil, fmt.Errorf("failed to retrieve %s", url) + return nil, fmt.Errorf("failed to retrieve '%s' status %d", url, resp.StatusCode) } body, err := io.ReadAll(resp.Body) @@ -127,12 +129,14 @@ type RetryHTTPSGetter struct { func (n *RetryHTTPSGetter) Get(url string) ([]byte, error) { delay := initialDelay ctx, cancel := context.WithTimeout(context.Background(), n.Timeout) + var returnedError error for { body, err := n.Getter.Get(url) if err == nil { cancel() return body, nil } + returnedError = multierr.Append(returnedError, err) delay = delay + delay if delay > n.MaxRetryDelay { delay = n.MaxRetryDelay @@ -140,7 +144,7 @@ func (n *RetryHTTPSGetter) Get(url string) ([]byte, error) { select { case <-ctx.Done(): cancel() - return nil, fmt.Errorf("timeout") // context cancelled + return nil, multierr.Append(returnedError, fmt.Errorf("timeout")) // context cancelled case <-time.After(delay): // wait to retry } } @@ -171,15 +175,29 @@ func (r *AMDRootCerts) Unmarshal(data []byte) error { return nil } -// FromDER populates the ProductCerts from DER-formatted certificates for both the ASK and the ARK. -func (r *ProductCerts) FromDER(ask []byte, ark []byte) error { - askCert, err := x509.ParseCertificate(ask) +// ParseCert returns an X.509 Certificate type for a PEM[CERTIFICATE]- or DER-encoded cert. +func ParseCert(cert []byte) (*x509.Certificate, error) { + raw := cert + b, rest := pem.Decode(cert) + if b != nil { + if len(rest) > 0 || b.Type != "CERTIFICATE" { + return nil, fmt.Errorf("bad type %q or trailing bytes (%d). Expected a single certificate when in PEM format", + b.Type, len(rest)) + } + raw = b.Bytes + } + return x509.ParseCertificate(raw) +} + +// Decode populates the ProductCerts from DER-formatted certificates for both the ASK and the ARK. +func (r *ProductCerts) Decode(ask []byte, ark []byte) error { + askCert, err := ParseCert(ask) if err != nil { return fmt.Errorf("could not parse ASK certificate: %v", err) } r.Ask = askCert - arkCert, err := x509.ParseCertificate(ark) + arkCert, err := ParseCert(ark) if err != nil { logger.Errorf("could not parse ARK certificate: %v", err) } @@ -195,7 +213,7 @@ func (r *ProductCerts) FromKDSCertBytes(data []byte) error { if err != nil { return err } - return r.FromDER(ask, ark) + return r.Decode(ask, ark) } // FromKDSCert populates r's AskX509 and ArkX509 certificates from the certificate format AMD's Key @@ -218,7 +236,6 @@ func (r *ProductCerts) X509Options(now time.Time) *x509.VerifyOptions { roots.AddCert(r.Ark) intermediates := x509.NewCertPool() intermediates.AddCert(r.Ask) - fmt.Printf("but now is %s\n", now.Format(time.RFC3339)) return &x509.VerifyOptions{Roots: roots, Intermediates: intermediates, CurrentTime: now} } @@ -271,10 +288,10 @@ func GetProductChain(product string, getter HTTPSGetter) (*ProductCerts, error) // Forward all the ProductCerts operations from the AMDRootCerts struct to follow the // Law of Demeter. -// FromDER populates the AMDRootCerts from DER-formatted certificates for both the ASK and the ARK. -func (r *AMDRootCerts) FromDER(ask []byte, ark []byte) error { +// Decode populates the AMDRootCerts from DER-formatted certificates for both the ASK and the ARK. +func (r *AMDRootCerts) Decode(ask []byte, ark []byte) error { r.ProductCerts = &ProductCerts{} - return r.ProductCerts.FromDER(ask, ark) + return r.ProductCerts.Decode(ask, ark) } // FromKDSCertBytes populates r's AskX509 and ArkX509 certificates from the two PEM-encoded diff --git a/verify/trust/trust_test.go b/verify/trust/trust_test.go new file mode 100644 index 0000000..c89fefe --- /dev/null +++ b/verify/trust/trust_test.go @@ -0,0 +1,136 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package trust_test + +import ( + "bytes" + "errors" + "testing" + "time" + + test "github.com/google/go-sev-guest/testing" + "github.com/google/go-sev-guest/verify/trust" +) + +func TestRetryHTTPSGetter(t *testing.T) { + testCases := map[string]struct { + getter *test.Getter + timeout time.Duration + maxRetryDelay time.Duration + }{ + "immediate success": { + getter: &test.Getter{ + Responses: map[string][]test.GetResponse{ + "https://fetch.me": { + { + Occurrences: 1, + Body: []byte("content"), + Error: nil, + }, + }, + }, + }, + timeout: time.Second, + maxRetryDelay: time.Millisecond, + }, + "second success": { + getter: &test.Getter{ + Responses: map[string][]test.GetResponse{ + "https://fetch.me": { + { + Occurrences: 1, + Body: []byte(""), + Error: errors.New("fail"), + }, + { + Occurrences: 1, + Body: []byte("content"), + Error: nil, + }, + }, + }, + }, + timeout: time.Second, + maxRetryDelay: time.Millisecond, + }, + "third success": { + getter: &test.Getter{ + Responses: map[string][]test.GetResponse{ + "https://fetch.me": { + { + Occurrences: 2, + Body: []byte(""), + Error: errors.New("fail"), + }, + { + Occurrences: 1, + Body: []byte("content"), + Error: nil, + }, + }, + }, + }, + timeout: time.Second, + maxRetryDelay: time.Millisecond, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + r := &trust.RetryHTTPSGetter{ + Timeout: tc.timeout, + MaxRetryDelay: tc.maxRetryDelay, + Getter: tc.getter, + } + + body, err := r.Get("https://fetch.me") + if !bytes.Equal(body, []byte("content")) { + t.Errorf("expected '%s' but got '%s'", "content", body) + } + if err != nil { + t.Errorf("expected no error, but got %s", err.Error()) + } + tc.getter.Done(t) + }) + } +} + +func TestRetryHTTPSGetterAllFail(t *testing.T) { + testGetter := &test.Getter{ + Responses: map[string][]test.GetResponse{ + "https://fetch.me": { + { + Occurrences: 1, + Body: []byte(""), + Error: errors.New("fail"), + }, + }, + }, + } + r := &trust.RetryHTTPSGetter{ + Timeout: 1 * time.Millisecond, + MaxRetryDelay: 1 * time.Millisecond, + Getter: testGetter, + } + + body, err := r.Get("https://fetch.me") + if !bytes.Equal(body, []byte("")) { + t.Errorf("expected '%s' but got '%s'", "content", body) + } + if err == nil { + t.Errorf("expected error, but got none") + } + testGetter.Done(t) +} diff --git a/verify/verify.go b/verify/verify.go index 965f55e..c01b1ea 100644 --- a/verify/verify.go +++ b/verify/verify.go @@ -43,13 +43,6 @@ const ( arkX509Version = 3 ) -// The VCEK productName in includes the specific silicon stepping -// corresponding to the supplied hwID. For example, “Milan-B0”. -// The product should inform what product keys we expect the key to be certified by. -var vcekProductMap = map[string]string{ - "Milan-B0": "Milan", -} - func askVerifiedBy(signee, signer *abi.AskCert, signeeName, signerName string) error { if !uuid.Equal(signee.CertifyingID[:], signer.KeyID[:]) { return fmt.Errorf("%s's certifying ID (%s) is not %s's key ID (%s) ", @@ -327,13 +320,11 @@ func validateCRLlink(x *x509.Certificate, product, role string) error { return nil } -// ValidateVcekExtensions checks if the certificate extensions match +// validateVcekExtensions checks if the certificate extensions match // wellformedness expectations. -func ValidateVcekExtensions(exts *kds.VcekExtensions) error { - if _, ok := vcekProductMap[exts.ProductName]; !ok { - return fmt.Errorf("unknown VCEK product name: %v", exts.ProductName) - } - return nil +func validateVcekExtensions(exts *kds.VcekExtensions) error { + _, err := kds.ParseProductName(exts.ProductName) + return err } // validateVcekCertificateProductNonspecific returns an error if the given certificate doesn't have @@ -372,7 +363,7 @@ func validateVcekCertificateProductNonspecific(cert *x509.Certificate) (*kds.Vce if err != nil { return nil, err } - if err := ValidateVcekExtensions(exts); err != nil { + if err := validateVcekExtensions(exts); err != nil { return nil, err } return exts, nil @@ -382,7 +373,6 @@ func validateVcekCertificateProductSpecifics(r *trust.AMDRootCerts, cert *x509.C if err := ValidateVcekCertIssuer(r, cert.Issuer); err != nil { return err } - fmt.Printf("now is %s\n", opts.Now.Format(time.RFC3339)) if _, err := cert.Verify(*r.X509Options(opts.Now)); err != nil { return fmt.Errorf("error verifying VCEK certificate: %v (%v)", err, r.ProductCerts.Ask.IsCA) } @@ -390,11 +380,28 @@ func validateVcekCertificateProductSpecifics(r *trust.AMDRootCerts, cert *x509.C return nil } -// VcekDER checks that the VCEK certificate matches expected fields +func checkProductName(got, want *spb.SevProduct) error { + // No constraint + if want == nil { + return nil + } + if got == nil { + return fmt.Errorf("internal error: no product name") + } + if got.Name != want.Name { + return fmt.Errorf("VCEK cert product name %v is not %v", got, want) + } + if got.ModelStepping != want.ModelStepping { + return fmt.Errorf("VCEK cert product model-stepping number %02X is not %02X", got.ModelStepping, want.ModelStepping) + } + return nil +} + +// decodeCerts checks that the VCEK certificate matches expected fields // from the KDS specification and also that its certificate chain matches // hardcoded trusted root certificates from AMD. -func VcekDER(vcek []byte, ask []byte, ark []byte, options *Options) (*x509.Certificate, *trust.AMDRootCerts, error) { - vcekCert, err := x509.ParseCertificate(vcek) +func decodeCerts(vcek []byte, ask []byte, ark []byte, options *Options) (*x509.Certificate, *trust.AMDRootCerts, error) { + vcekCert, err := trust.ParseCert(vcek) if err != nil { return nil, nil, fmt.Errorf("could not interpret VCEK DER bytes: %v", err) } @@ -403,27 +410,36 @@ func VcekDER(vcek []byte, ask []byte, ark []byte, options *Options) (*x509.Certi return nil, nil, err } roots := options.TrustedRoots - product := vcekProductMap[exts.ProductName] + + product, err := kds.ParseProductName(exts.ProductName) + if err != nil { + return nil, nil, err + } + productName := kds.ProductString(product) + // Ensure the extension product info matches expectations. + if err := checkProductName(product, options.Product); err != nil { + return nil, nil, err + } if len(roots) == 0 { logger.Warning("Using embedded AMD certificates for SEV-SNP attestation root of trust") root := &trust.AMDRootCerts{ - Product: product, + Product: productName, // Require that the root matches embedded root certs. - AskSev: trust.DefaultRootCerts[product].AskSev, - ArkSev: trust.DefaultRootCerts[product].ArkSev, + AskSev: trust.DefaultRootCerts[productName].AskSev, + ArkSev: trust.DefaultRootCerts[productName].ArkSev, } - if err := root.FromDER(ask, ark); err != nil { + if err := root.Decode(ask, ark); err != nil { return nil, nil, err } if err := ValidateX509(root); err != nil { return nil, nil, err } roots = map[string][]*trust.AMDRootCerts{ - product: {root}, + productName: {root}, } } var lastErr error - for _, productRoot := range roots[product] { + for _, productRoot := range roots[productName] { if err := validateVcekCertificateProductSpecifics(productRoot, vcekCert, options); err != nil { lastErr = err continue @@ -480,6 +496,10 @@ type Options struct { // then verification will fall back on embedded AMD-published root certificates. // Maps the product name to an array of allowed roots. TrustedRoots map[string][]*trust.AMDRootCerts + // Product is a forced value for the attestation product name when verifying or retrieving + // VCEK certificates. An attestation should carry the product of the reporting + // machine. + Product *spb.SevProduct } // DefaultOptions returns a useful default verification option setting @@ -532,14 +552,17 @@ func SnpAttestation(attestation *spb.Attestation, options *Options) error { if attestation == nil { return fmt.Errorf("attestation cannot be nil") } - // Make sure we have the whole certificate chain if we're allowed. - if !options.DisableCertFetching { - if err := fillInAttestation(attestation, options); err != nil { - return err - } + // Make sure we have the whole certificate chain, or at least the product + // info. + if err := fillInAttestation(attestation, options); err != nil { + return err } + // Pass along the expected product information for VcekDER. fillInAttestation will ensure + // that this is a noop if options.Product began as non-nil. + options.Product = attestation.Product + chain := attestation.GetCertificateChain() - vcek, root, err := VcekDER(chain.GetVcekCert(), chain.GetAskCert(), chain.GetArkCert(), options) + vcek, root, err := decodeCerts(chain.GetVcekCert(), chain.GetAskCert(), chain.GetArkCert(), options) if err != nil { return err } @@ -554,8 +577,20 @@ func SnpAttestation(attestation *spb.Attestation, options *Options) error { // fillInAttestation uses AMD's KDS to populate any empty certificate field in the attestation's // certificate chain. func fillInAttestation(attestation *spb.Attestation, options *Options) error { - // TODO(Issue #11): Determine the product a report was fetched from, or make this an option. - product := "Milan" + if options.Product != nil { + attestation.Product = options.Product + } + if attestation.Product == nil { + // The default product is the first launched SEV-SNP product value. + attestation.Product = &spb.SevProduct{ + Name: spb.SevProduct_SEV_PRODUCT_MILAN, + ModelStepping: 0xB0, + } + } + if options.DisableCertFetching { + return nil + } + product := kds.ProductString(options.Product) getter := options.Getter if getter == nil { getter = trust.DefaultHTTPSGetter() @@ -580,7 +615,7 @@ func fillInAttestation(attestation *spb.Attestation, options *Options) error { } } if len(chain.GetVcekCert()) == 0 { - vcekURL := kds.VCEKCertURL(product, report.GetChipId(), kds.TCBVersion(report.GetCurrentTcb())) + vcekURL := kds.VCEKCertURL(product, report.GetChipId(), kds.TCBVersion(report.GetReportedTcb())) vcek, err := getter.Get(vcekURL) if err != nil { return &trust.AttestationRecreationErr{ diff --git a/verify/verify_test.go b/verify/verify_test.go index e2bd040..1821f47 100644 --- a/verify/verify_test.go +++ b/verify/verify_test.go @@ -20,6 +20,7 @@ import ( "crypto/x509/pkix" _ "embed" "encoding/asn1" + "encoding/pem" "math/big" "math/rand" "os" @@ -40,8 +41,10 @@ import ( const product = "Milan" -var signMu sync.Once -var signer *test.AmdSigner +var ( + signMu sync.Once + signer *test.AmdSigner +) func initSigner() { newSigner, err := test.DefaultCertChain(product, time.Now()) @@ -283,7 +286,7 @@ func TestKdsMetadataLogic(t *testing.T) { }, }, }, - wantErr: "unknown VCEK product name: Cookie-B0", + wantErr: "unknown AMD SEV product: \"Cookie\"", }, } for _, tc := range tests { @@ -295,23 +298,25 @@ func TestKdsMetadataLogic(t *testing.T) { } // Trust the test-generated root if the test should pass. Otherwise, other root logic // won't get tested. - options := &Options{TrustedRoots: map[string][]*trust.AMDRootCerts{ - "Milan": {&trust.AMDRootCerts{ - Product: "Milan", - ProductCerts: &trust.ProductCerts{ - Ark: newSigner.Ark, - Ask: newSigner.Ask, - }, - }}, - }, + options := &Options{ + TrustedRoots: map[string][]*trust.AMDRootCerts{ + "Milan": {&trust.AMDRootCerts{ + Product: "Milan", + ProductCerts: &trust.ProductCerts{ + Ark: newSigner.Ark, + Ask: newSigner.Ask, + }, + }}, + }, Now: time.Date(1, time.January, 5, 0, 0, 0, 0, time.UTC), } if tc.wantErr != "" { options = &Options{} } - vcek, _, err := VcekDER(newSigner.Vcek.Raw, newSigner.Ask.Raw, newSigner.Ark.Raw, options) + vcekPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: newSigner.Vcek.Raw}) + vcek, _, err := decodeCerts(vcekPem, newSigner.Ask.Raw, newSigner.Ark.Raw, options) if !test.Match(err, tc.wantErr) { - t.Errorf("%s: VcekDER(...) = %+v, %v did not error as expected. Want %q", tc.name, vcek, err, tc.wantErr) + t.Errorf("%s: decodeCerts(...) = %+v, %v did not error as expected. Want %q", tc.name, vcek, err, tc.wantErr) } } } @@ -372,11 +377,11 @@ func TestCRLRootValidity(t *testing.T) { if err != nil { t.Fatal(err) } - g2 := &test.Getter{ - Responses: map[string][]byte{ + g2 := test.SimpleGetter( + map[string][]byte{ "https://kdsintf.amd.com/vcek/v1/Milan/crl": crl, }, - } + ) wantErr := "CRL is not signed by ARK" if err := VcekNotRevoked(root, signer2.Vcek, &Options{Getter: g2}); !test.Match(err, wantErr) { t.Errorf("Bad Root: VcekNotRevoked(%v) did not error as expected. Got %v, want %v", signer.Vcek, err, wantErr) @@ -450,13 +455,13 @@ func TestRealAttestationVerification(t *testing.T) { trust.ClearProductCertCache() var nonce [64]byte copy(nonce[:], []byte{1, 2, 3, 4, 5}) - getter := &test.Getter{ - Responses: map[string][]byte{ + getter := test.SimpleGetter( + map[string][]byte{ "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": testdata.MilanBytes, // Use the VCEK's hwID and known TCB values to specify the URL its VCEK cert would be fetched from. "https://kdsintf.amd.com/vcek/v1/Milan/3ac3fe21e13fb0990eb28a802e3fb6a29483a6b0753590c951bdd3b8e53786184ca39e359669a2b76a1936776b564ea464cdce40c05f63c9b610c5068b006b5d?blSPL=2&teeSPL=0&snpSPL=5&ucodeSPL=68": testdata.VcekBytes, }, - } + ) if err := RawSnpReport(testdata.AttestationBytes, &Options{Getter: getter}); err != nil { t.Error(err) }