diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index d5e004fc6a..4ea57be5b1 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -16,6 +16,7 @@ import ( "syscall" "time" + "github.com/Microsoft/hcsshim/internal/guest/snp" "github.com/mattn/go-shellwords" "github.com/pkg/errors" @@ -95,6 +96,15 @@ func (h *Host) SetSecurityPolicy(base64Policy string) error { return err } + hostData, err := securitypolicy.NewSecurityPolicyDigest(base64Policy) + if err != nil { + return err + } + + if err := snp.ValidateHostData(hostData, ""); err != nil { + return err + } + h.securityPolicyEnforcer = p h.securityPolicyEnforcerSet = true diff --git a/internal/guest/snp/fake_report.go b/internal/guest/snp/fake_report.go new file mode 100644 index 0000000000..3d012ab5ca --- /dev/null +++ b/internal/guest/snp/fake_report.go @@ -0,0 +1,36 @@ +//go:build linux +// +build linux + +package snp + +import ( + "bytes" + "encoding/binary" + "encoding/hex" +) + +const fakeSNPReport = "01000000010000001f00030000000000010000000000000000000000000000000200000000000000000000000000000000000000010000000000000000000028010000000000000000000000000000007ab000a323b3c873f5b81bbe584e7c1a26bcf40dc27e00f8e0d144b1ed2d14f10000000000000000000000000000000000000000000000000000000000000000e29af700e85b39996fa38226d2804b78cad746ffef4477360a61b47874bdecd640f9d32f5ff64a55baad3c545484d9ed28603a3ea835a83bd688b0ec1dcb36b6b8c22412e5b63115b75db8628b989bc598c475ca5f7683e8d351e7e789a1baff19041750567161ad52bf0d152bd76d7c6f313d0a0fd72d0089692c18f521155800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040aea62690b08eb6d680392c9a9b3db56a9b3cc44083b9da31fb88bcfc493407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000028000000000000000000000000000000000000000000000000e6c86796cd44b0bc6b7c0d4fdab33e2807e14b5fc4538b3750921169d97bcf4447c7d3ab2a7c25f74c1641e2885c1011d025cc536f5c9a2504713136c7877f480000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003131c0f3e7be5c6e400f22404596e1874381e99d03de45ef8b97eee0a0fa93a4911550330343f14dddbbd6c0db83744f000000000000000000000000000000000000000000000000db07c83c5e6162c2387f3b76cd547672657f6a5df99df98efee7c15349320d83e086c5003ec43050a9b18d1c39dedc340000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + +func fakeGetSNPReport() (snpReport, error) { + reportBytes, err := hex.DecodeString(fakeSNPReport) + if err != nil { + return snpReport{}, err + } + + report := snpReport{} + if err := binary.Read(bytes.NewBuffer(reportBytes), binary.LittleEndian, &report); err != nil { + return snpReport{}, err + } + return report, nil +} + +func FetchFakeSNPReport(hostData string) (Report, error) { + fakeRaw, err := fakeGetSNPReport() + if err != nil { + return Report{}, err + } + if hostData != "" { + copy(fakeRaw.HostData[:], hostData) + } + return convertRawReport(&fakeRaw), nil +} diff --git a/internal/guest/snp/report.go b/internal/guest/snp/report.go new file mode 100644 index 0000000000..403a97e6f8 --- /dev/null +++ b/internal/guest/snp/report.go @@ -0,0 +1,321 @@ +//go:build linux +// +build linux + +// package snp contains minimal functionality required to fetch +// attestation reports inside an enlightened guest. +package snp + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" + "os" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + MsgTypeInvalid = iota + MsgCPUIDRequest + MsgCPUIDResponse + MsgKeyRequest + MsgKeyResponse + MsgReportRequest + MsgReportResponse + MsgExportRequest + MsgExportResponse + MsgImportRequest + MsgImportResponse + MsgAbsorbRequest + MsgAbsorbResponse + MsgVMRKRequest + MsgVMRKResponse + MsgTypeMax +) + +type SevSNPGuestRequest struct { + RequestMsgType byte + ResponseMsgType byte + MsgVersion byte + RequestLength uint16 + RequestUAddr uint64 + ResponseLength uint16 + ResponseUAddr uint64 + Error uint32 +} + +// Below is ported from Linux kernel code: +// https://github.com/torvalds/linux/blob/5bfc75d92efd494db37f5c4c173d3639d4772966/include/uapi/asm-generic/ioctl.h +const ( + iocNone = 0 + iocWrite = 1 + iocRead = 2 + iocNRBits = 8 + iocTypeBits = 8 + iocSizeBits = 14 + iocDirBits = 2 + + // bit shifts + iocNRShift = 0 + iocTypeShift = iocNRBits + iocSizeShift = iocTypeShift + iocTypeBits + iocDirShift = iocSizeShift + iocSizeBits + + // SEV-SNP IOCTL type + sevGuestType = 'S' + // SEV-SNP IOCTL size, same as unsafe.Sizeof(SevSNPGuestRequest{}) + sevGuestSize = 40 + + // SEV-SNP requests + sevSNPReportCode = 0x1 +) + +// _IOC macros equivalent +func ioc(dir, t, nr, size int) int { + return dir< len(msgReportIn.ReportData) { + return nil, fmt.Errorf("reportData too large: %s", reportData) + } + rd, err := hex.DecodeString(reportData) + if err != nil { + return nil, err + } + copy(msgReportIn.ReportData[:], rd[:]) + } + + payload := SevSNPGuestRequest{ + RequestMsgType: MsgReportRequest, + ResponseMsgType: MsgReportResponse, + MsgVersion: 1, + RequestLength: uint16(unsafe.Sizeof(msgReportIn)), + RequestUAddr: uint64(uintptr(unsafe.Pointer(&msgReportIn))), + ResponseLength: uint16(unsafe.Sizeof(msgReportOut)), + ResponseUAddr: uint64(uintptr(unsafe.Pointer(&msgReportOut))), + Error: 0, + } + + if err := snpIoctl(f, sevSNPReportCode, &payload); err != nil { + return nil, err + } + return msgReportOut.Report[:], nil +} + +// Report represents parsed attestation report. +type Report struct { + Version uint32 + GuestSVN uint32 + Policy uint64 + FamilyID string + ImageID string + VMPL uint32 + SignatureAlgo uint32 + PlatformVersion uint64 + PlatformInfo uint64 + AuthorKeyEn uint32 + ReportData string + Measurement string + HostData string + IDKeyDigest string + AuthorKeyDigest string + ReportID string + ReportIDMA string + ReportTCB uint64 + ChipID string + CommittedSVN string + CommittedVersion string + LaunchSVN string + Signature string +} + +// PrettyString returns formatted attestation report. +func (r Report) PrettyString() string { + fieldNameFmt := "%-20s" + pretty := "" + pretty += fmt.Sprintf(fieldNameFmt+"%08x\n", "Version", r.Version) + pretty += fmt.Sprintf(fieldNameFmt+"%08x\n", "GuestSVN", r.GuestSVN) + pretty += fmt.Sprintf(fieldNameFmt+"%016x\n", "Policy", r.Policy) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "FamilyID", r.FamilyID) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "ImageID", r.ImageID) + pretty += fmt.Sprintf(fieldNameFmt+"%08x\n", "VMPL", r.VMPL) + pretty += fmt.Sprintf(fieldNameFmt+"%08x\n", "SignatureAlgo", r.SignatureAlgo) + pretty += fmt.Sprintf(fieldNameFmt+"%016x\n", "PlatformVersion", r.PlatformVersion) + pretty += fmt.Sprintf(fieldNameFmt+"%016x\n", "PlatformInfo", r.PlatformInfo) + pretty += fmt.Sprintf(fieldNameFmt+"%08x\n", "AuthorKeyEn", r.AuthorKeyEn) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "ReportData", r.ReportData) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "Measurement", r.Measurement) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "HostData", r.HostData) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "IDKeyDigest", r.IDKeyDigest) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "AuthorKeyDigest", r.AuthorKeyDigest) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "ReportID", r.ReportID) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "ReportIDMA", r.ReportIDMA) + pretty += fmt.Sprintf(fieldNameFmt+"%016x\n", "ReportTCB", r.ReportTCB) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "ChipID", r.ChipID) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "CommittedSVN", r.CommittedSVN) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "CommittedVersion", r.CommittedVersion) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "LaunchSVN", r.LaunchSVN) + pretty += fmt.Sprintf(fieldNameFmt+"%s\n", "Signature", r.Signature) + return pretty +} + +// mirrorBytes mirrors the byte ordering so that hex-encoding little endian +// ordered bytes come out in the readable order. +func mirrorBytes(b []byte) []byte { + for i := 0; i < len(b)/2; i++ { + mirrorIndex := len(b) - i - 1 + b[i], b[mirrorIndex] = b[mirrorIndex], b[i] + } + return b +} + +func convertRawReport(rawReport *snpReport) Report { + return Report{ + Version: rawReport.Version, + GuestSVN: rawReport.GuestSVN, + Policy: rawReport.Policy, + FamilyID: hex.EncodeToString(mirrorBytes(rawReport.FamilyID[:])[:]), + ImageID: hex.EncodeToString(mirrorBytes(rawReport.ImageID[:])[:]), + VMPL: rawReport.VMPL, + SignatureAlgo: rawReport.SignatureAlgo, + PlatformVersion: rawReport.PlatformVersion, + PlatformInfo: rawReport.PlatformInfo, + AuthorKeyEn: rawReport.AuthorKeyEn, + ReportData: hex.EncodeToString(rawReport.ReportData[:]), + Measurement: hex.EncodeToString(rawReport.Measurement[:]), + HostData: hex.EncodeToString(rawReport.HostData[:]), + IDKeyDigest: hex.EncodeToString(rawReport.IDKeyDigest[:]), + AuthorKeyDigest: hex.EncodeToString(rawReport.AuthorKeyDigest[:]), + ReportID: hex.EncodeToString(rawReport.ReportID[:]), + ReportIDMA: hex.EncodeToString(rawReport.ReportIDMA[:]), + ReportTCB: rawReport.ReportTCB, + ChipID: hex.EncodeToString(rawReport.ChipID[:]), + CommittedSVN: hex.EncodeToString(rawReport.CommittedSVN[:]), + CommittedVersion: hex.EncodeToString(rawReport.CommittedVersion[:]), + LaunchSVN: hex.EncodeToString(rawReport.LaunchSVN[:]), + Signature: hex.EncodeToString(rawReport.Signature[:]), + } +} + +// FetchParsedSNPReport parses raw attestation response into proper structs. +func FetchParsedSNPReport(reportData string) (Report, error) { + rawBytes, err := FetchRawSNPReport(reportData) + if err != nil { + return Report{}, err + } + + var report snpReport + buf := bytes.NewBuffer(rawBytes) + if err := binary.Read(buf, binary.LittleEndian, &report); err != nil { + return Report{}, err + } + return convertRawReport(&report), nil +} + +// ValidateHostData fetches SNP report (if applicable) and validates `hostData` against +// HostData set at UVM launch. +func ValidateHostData(hostData, reportData string) error { + report, err := FetchParsedSNPReport(reportData) + if err != nil { + // For non-SNP hardware /dev/sev will not exist + if os.IsNotExist(err) { + return nil + } + return err + } + + if report.HostData != hostData { + return fmt.Errorf( + "security policy digest %q doesn't match HostData provided at launch %q", + hostData, + report.HostData, + ) + } + return nil +} diff --git a/internal/tools/snp-report/main.go b/internal/tools/snp-report/main.go new file mode 100644 index 0000000000..8ce1ee9bcb --- /dev/null +++ b/internal/tools/snp-report/main.go @@ -0,0 +1,70 @@ +//go:build linux +// +build linux + +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/Microsoft/hcsshim/internal/guest/snp" +) + +func main() { + fakeReportFlag := flag.Bool( + "fake-report", + false, + "If true, don't issue an actual syscall to /dev/sev and return a fake predefined report", + ) + hostDataFlag := flag.String( + "host-data", + "", + "Use together with 'fake-report', to set 'HostData' field of fake SNP report.", + ) + reportDataFlag := flag.String( + "report-data", + "", + "Report data to use when fetching SNP attestation report", + ) + binaryFmtFlag := flag.Bool( + "binary", + false, + "Fetch report in binary format", + ) + prettyPrintFlag := flag.Bool( + "pretty", + false, + "Print report in a prettier format", + ) + + flag.Parse() + + if *binaryFmtFlag { + binaryReport, err := snp.FetchRawSNPReport(*reportDataFlag) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Printf("%x\n", binaryReport) + os.Exit(0) + } + + var report snp.Report + var err error + if *fakeReportFlag { + report, err = snp.FetchFakeSNPReport(*hostDataFlag) + } else { + report, err = snp.FetchParsedSNPReport(*reportDataFlag) + } + if err != nil { + fmt.Printf("failed to fetch SNP report: %s", err) + os.Exit(1) + } + + if !*prettyPrintFlag { + fmt.Printf("%+v\n", report) + } else { + fmt.Println(report.PrettyString()) + } +} diff --git a/internal/uvm/create_lcow.go b/internal/uvm/create_lcow.go index 8df0e743e0..8bb0085590 100644 --- a/internal/uvm/create_lcow.go +++ b/internal/uvm/create_lcow.go @@ -2,8 +2,6 @@ package uvm import ( "context" - "crypto/sha256" - "encoding/base64" "fmt" "io" "net" @@ -13,6 +11,7 @@ import ( "github.com/Microsoft/go-winio" "github.com/Microsoft/go-winio/pkg/guid" + "github.com/Microsoft/hcsshim/pkg/securitypolicy" "github.com/pkg/errors" "github.com/sirupsen/logrus" "go.opencensus.io/trace" @@ -403,28 +402,19 @@ func makeLCOWSecurityDoc(ctx context.Context, opts *OptionsLCOW, uvm *UtilityVM) // and can be used to check that the policy used by opengcs is the required one as // a condition of releasing secrets to the container. - // First, decode the base64 string into a human readable (json) string . - jsonPolicy, err := base64.StdEncoding.DecodeString(opts.SecurityPolicy) + hostData, err := securitypolicy.NewSecurityPolicyDigest(opts.SecurityPolicy) if err != nil { - return nil, fmt.Errorf("failed to decode base64 SecurityPolicy") + return nil, err } - // make a sha256 hashing object - hostData := sha256.New() - // give it the jsaon string to measure - hostData.Write(jsonPolicy) - // get the measurement out - securityPolicyHash := base64.StdEncoding.EncodeToString(hostData.Sum(nil)) - // Put the measurement into the LaunchData field of the HCS creation command. - // This will endup in HOST_DATA of SNP_LAUNCH_FINISH command the and ATTESTATION_REPORT + // This will end-up in HOST_DATA of SNP_LAUNCH_FINISH command the and ATTESTATION_REPORT // retrieved by the guest later. - doc.VirtualMachine.SecuritySettings = &hcsschema.SecuritySettings{ EnableTpm: false, Isolation: &hcsschema.IsolationSettings{ IsolationType: "SecureNestedPaging", - LaunchData: securityPolicyHash, + LaunchData: hostData, // HclEnabled: true, /* Not available in schema 2.5 - REQUIRED when using BlockStorage in 2.6 */ }, } diff --git a/pkg/securitypolicy/securitypolicy.go b/pkg/securitypolicy/securitypolicy.go index 2a539ed9db..746d26cc93 100644 --- a/pkg/securitypolicy/securitypolicy.go +++ b/pkg/securitypolicy/securitypolicy.go @@ -1,8 +1,10 @@ package securitypolicy import ( + "crypto/sha256" "encoding/base64" "encoding/json" + "fmt" "regexp" "strconv" @@ -86,6 +88,18 @@ func NewOpenDoorPolicy() *SecurityPolicy { } } +// NewSecurityPolicyDigest converts base64policy string into JSON and computes +// sha256-digest on the JSON string. +func NewSecurityPolicyDigest(base64policy string) (string, error) { + jsonPolicy, err := base64.StdEncoding.DecodeString(base64policy) + if err != nil { + return "", fmt.Errorf("failed to decode base64 security policy: %w", err) + } + digest := sha256.New() + digest.Write(jsonPolicy) + return base64.StdEncoding.EncodeToString(digest.Sum(nil)), nil +} + // Internal version of SecurityPolicyContainer type securityPolicyContainer struct { // The command that we will allow the container to execute diff --git a/test/vendor/github.com/Microsoft/hcsshim/internal/uvm/create_lcow.go b/test/vendor/github.com/Microsoft/hcsshim/internal/uvm/create_lcow.go index 8df0e743e0..8bb0085590 100644 --- a/test/vendor/github.com/Microsoft/hcsshim/internal/uvm/create_lcow.go +++ b/test/vendor/github.com/Microsoft/hcsshim/internal/uvm/create_lcow.go @@ -2,8 +2,6 @@ package uvm import ( "context" - "crypto/sha256" - "encoding/base64" "fmt" "io" "net" @@ -13,6 +11,7 @@ import ( "github.com/Microsoft/go-winio" "github.com/Microsoft/go-winio/pkg/guid" + "github.com/Microsoft/hcsshim/pkg/securitypolicy" "github.com/pkg/errors" "github.com/sirupsen/logrus" "go.opencensus.io/trace" @@ -403,28 +402,19 @@ func makeLCOWSecurityDoc(ctx context.Context, opts *OptionsLCOW, uvm *UtilityVM) // and can be used to check that the policy used by opengcs is the required one as // a condition of releasing secrets to the container. - // First, decode the base64 string into a human readable (json) string . - jsonPolicy, err := base64.StdEncoding.DecodeString(opts.SecurityPolicy) + hostData, err := securitypolicy.NewSecurityPolicyDigest(opts.SecurityPolicy) if err != nil { - return nil, fmt.Errorf("failed to decode base64 SecurityPolicy") + return nil, err } - // make a sha256 hashing object - hostData := sha256.New() - // give it the jsaon string to measure - hostData.Write(jsonPolicy) - // get the measurement out - securityPolicyHash := base64.StdEncoding.EncodeToString(hostData.Sum(nil)) - // Put the measurement into the LaunchData field of the HCS creation command. - // This will endup in HOST_DATA of SNP_LAUNCH_FINISH command the and ATTESTATION_REPORT + // This will end-up in HOST_DATA of SNP_LAUNCH_FINISH command the and ATTESTATION_REPORT // retrieved by the guest later. - doc.VirtualMachine.SecuritySettings = &hcsschema.SecuritySettings{ EnableTpm: false, Isolation: &hcsschema.IsolationSettings{ IsolationType: "SecureNestedPaging", - LaunchData: securityPolicyHash, + LaunchData: hostData, // HclEnabled: true, /* Not available in schema 2.5 - REQUIRED when using BlockStorage in 2.6 */ }, } diff --git a/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go b/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go index 2a539ed9db..746d26cc93 100644 --- a/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go +++ b/test/vendor/github.com/Microsoft/hcsshim/pkg/securitypolicy/securitypolicy.go @@ -1,8 +1,10 @@ package securitypolicy import ( + "crypto/sha256" "encoding/base64" "encoding/json" + "fmt" "regexp" "strconv" @@ -86,6 +88,18 @@ func NewOpenDoorPolicy() *SecurityPolicy { } } +// NewSecurityPolicyDigest converts base64policy string into JSON and computes +// sha256-digest on the JSON string. +func NewSecurityPolicyDigest(base64policy string) (string, error) { + jsonPolicy, err := base64.StdEncoding.DecodeString(base64policy) + if err != nil { + return "", fmt.Errorf("failed to decode base64 security policy: %w", err) + } + digest := sha256.New() + digest.Write(jsonPolicy) + return base64.StdEncoding.EncodeToString(digest.Sum(nil)), nil +} + // Internal version of SecurityPolicyContainer type securityPolicyContainer struct { // The command that we will allow the container to execute