diff --git a/Makefile b/Makefile index e1a0f51129..c11786ce3a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ BASE:=base.tar.gz +DEV_BUILD:=0 GO:=go GO_FLAGS:=-ldflags "-s -w" # strip Go binaries @@ -16,6 +17,12 @@ GO_BUILD:=CGO_ENABLED=$(CGO_ENABLED) $(GO) build $(GO_FLAGS) $(GO_FLAGS_EXTRA) SRCROOT=$(dir $(abspath $(firstword $(MAKEFILE_LIST)))) +DELTA_TARGET=out/delta.tar.gz + +ifeq "$(DEV_BUILD)" "1" +DELTA_TARGET=out/delta-dev.tar.gz +endif + # The link aliases for gcstools GCS_TOOLS=\ generichook \ @@ -55,6 +62,15 @@ out/delta.tar.gz: bin/init bin/vsockexec bin/cmd/gcs bin/cmd/gcstools bin/cmd/ho tar -zcf $@ -C rootfs . rm -rf rootfs +# This target includes utilities which may be useful for testing purposes. +out/delta-dev.tar.gz: out/delta.tar.gz bin/internal/tools/snp-report + rm -rf rootfs-dev + mkdir rootfs-dev + tar -xzf out/delta.tar.gz -C rootfs-dev + cp bin/internal/tools/snp-report rootfs-dev/bin/ + tar -zcf $@ -C rootfs-dev . + rm -rf rootfs-dev + out/rootfs.tar.gz: out/initrd.img rm -rf rootfs-conv mkdir rootfs-conv @@ -62,8 +78,8 @@ out/rootfs.tar.gz: out/initrd.img tar -zcf $@ -C rootfs-conv . rm -rf rootfs-conv -out/initrd.img: $(BASE) out/delta.tar.gz $(SRCROOT)/hack/catcpio.sh - $(SRCROOT)/hack/catcpio.sh "$(BASE)" out/delta.tar.gz > out/initrd.img.uncompressed +out/initrd.img: $(BASE) $(DELTA_TARGET) $(SRCROOT)/hack/catcpio.sh + $(SRCROOT)/hack/catcpio.sh "$(BASE)" $(DELTA_TARGET) > out/initrd.img.uncompressed gzip -c out/initrd.img.uncompressed > $@ rm out/initrd.img.uncompressed @@ -71,6 +87,7 @@ out/initrd.img: $(BASE) out/delta.tar.gz $(SRCROOT)/hack/catcpio.sh -include deps/cmd/gcstools.gomake -include deps/cmd/hooks/wait-paths.gomake -include deps/cmd/tar2ext4.gomake +-include deps/internal/tools/snp-report.gomake # Implicit rule for includes that define Go targets. %.gomake: $(SRCROOT)/Makefile diff --git a/internal/guest/linux/ioctl.go b/internal/guest/linux/ioctl.go new file mode 100644 index 0000000000..c87a3da9f5 --- /dev/null +++ b/internal/guest/linux/ioctl.go @@ -0,0 +1,49 @@ +//go:build linux +// +build linux + +// Package linux contains definitions required for making a linux ioctl. +package linux + +import ( + "os" + "unsafe" + + "golang.org/x/sys/unix" +) + +// 32 bits to describe an ioctl: +// 0-7: NR (command for a given ioctl type) +// 8-15: TYPE (ioctl type) +// 16-29: SIZE (payload size) +// 30-31: DIR (direction of ioctl, can be: none/write/read/write-read) +const ( + IocWrite = 1 + IocRead = 2 + IocNRBits = 8 + IocTypeBits = 8 + IocSizeBits = 14 + IocDirBits = 2 + + IocNRMask = (1 << IocNRBits) - 1 + IocTypeMask = (1 << IocTypeBits) - 1 + IocSizeMask = (1 << IocSizeBits) - 1 + IocDirMask = (1 << IocDirBits) - 1 + IocTypeShift = IocNRBits + IocSizeShift = IocTypeShift + IocTypeBits + IocDirShift = IocSizeShift + IocSizeBits + IocWRBase = (IocRead | IocWrite) << IocDirShift +) + +// Ioctl makes a syscall described by `command` with data `dataPtr` to device +// driver file `f`. +func Ioctl(f *os.File, command int, dataPtr unsafe.Pointer) error { + if _, _, err := unix.Syscall( + unix.SYS_IOCTL, + f.Fd(), + uintptr(command), + uintptr(dataPtr), + ); err != 0 { + return err + } + return nil +} diff --git a/internal/guest/runtime/hcsv2/hostdata.go b/internal/guest/runtime/hcsv2/hostdata.go new file mode 100644 index 0000000000..562ed006e1 --- /dev/null +++ b/internal/guest/runtime/hcsv2/hostdata.go @@ -0,0 +1,34 @@ +//go:build linux +// +build linux + +package hcsv2 + +import ( + "bytes" + "fmt" + "os" + + "github.com/Microsoft/hcsshim/pkg/amdsevsnp" +) + +// validateHostData fetches SNP report (if applicable) and validates `hostData` against +// HostData set at UVM launch. +func validateHostData(hostData []byte) error { + report, err := amdsevsnp.FetchParsedSNPReport(nil) + if err != nil { + // For non-SNP hardware /dev/sev will not exist + if os.IsNotExist(err) { + return nil + } + return err + } + + if bytes.Compare(hostData, report.HostData) != 0 { + return fmt.Errorf( + "security policy digest %q doesn't match HostData provided at launch %q", + hostData, + report.HostData, + ) + } + return nil +} diff --git a/internal/guest/runtime/hcsv2/uvm.go b/internal/guest/runtime/hcsv2/uvm.go index d5e004fc6a..b1abecc811 100644 --- a/internal/guest/runtime/hcsv2/uvm.go +++ b/internal/guest/runtime/hcsv2/uvm.go @@ -95,6 +95,15 @@ func (h *Host) SetSecurityPolicy(base64Policy string) error { return err } + hostData, err := securitypolicy.NewSecurityPolicyDigest(base64Policy) + if err != nil { + return err + } + + if err := validateHostData(hostData[:]); err != nil { + return err + } + h.securityPolicyEnforcer = p h.securityPolicyEnforcerSet = true diff --git a/internal/guest/storage/devicemapper/devicemapper.go b/internal/guest/storage/devicemapper/devicemapper.go index 3b313284c1..3f9b6797e7 100644 --- a/internal/guest/storage/devicemapper/devicemapper.go +++ b/internal/guest/storage/devicemapper/devicemapper.go @@ -12,6 +12,8 @@ import ( "unsafe" "golang.org/x/sys/unix" + + "github.com/Microsoft/hcsshim/internal/guest/linux" ) // CreateFlags modify the operation of CreateDevice @@ -28,24 +30,9 @@ var ( ) const ( - _IOC_WRITE = 1 - _IOC_READ = 2 - _IOC_NRBITS = 8 - _IOC_TYPEBITS = 8 - _IOC_SIZEBITS = 14 - _IOC_DIRBITS = 2 - - _IOC_NRMASK = ((1 << _IOC_NRBITS) - 1) - _IOC_TYPEMASK = ((1 << _IOC_TYPEBITS) - 1) - _IOC_SIZEMASK = ((1 << _IOC_SIZEBITS) - 1) - _IOC_DIRMASK = ((1 << _IOC_DIRBITS) - 1) - _IOC_TYPESHIFT = (_IOC_NRBITS) - _IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS) - _IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS) - _DM_IOCTL = 0xfd _DM_IOCTL_SIZE = 312 - _DM_IOCTL_BASE = (_IOC_READ|_IOC_WRITE)<<_IOC_DIRSHIFT | _DM_IOCTL<<_IOC_TYPESHIFT | _DM_IOCTL_SIZE<<_IOC_SIZESHIFT + _DM_IOCTL_BASE = linux.IocWRBase | _DM_IOCTL< len(msgReportIn.ReportData) { + return nil, fmt.Errorf("reportData too large: %s", reportData) + } + copy(msgReportIn.ReportData[:], reportData) + } + + payload := &guestRequest{ + RequestMsgType: msgReportRequest, + ResponseMsgType: msgReportResponse, + MsgVersion: 1, + RequestLength: uint16(unsafe.Sizeof(msgReportIn)), + RequestUAddr: unsafe.Pointer(&msgReportIn), + ResponseLength: uint16(unsafe.Sizeof(msgReportOut)), + ResponseUAddr: unsafe.Pointer(&msgReportOut), + Error: 0, + } + + if err := linux.Ioctl(f, reportCode|ioctlBase, unsafe.Pointer(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 []byte + IDKeyDigest string + AuthorKeyDigest string + ReportID string + ReportIDMA string + ReportTCB uint64 + ChipID string + CommittedSVN string + CommittedVersion string + LaunchSVN string + Signature string +} + +// 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 +} + +// FetchParsedSNPReport parses raw attestation response into proper structs. +func FetchParsedSNPReport(reportData []byte) (Report, error) { + rawBytes, err := FetchRawSNPReport(reportData) + if err != nil { + return Report{}, err + } + + var r report + buf := bytes.NewBuffer(rawBytes) + if err := binary.Read(buf, binary.LittleEndian, &r); err != nil { + return Report{}, err + } + return r.report(), nil +} diff --git a/pkg/amdsevsnp/report_test.go b/pkg/amdsevsnp/report_test.go new file mode 100644 index 0000000000..e42af6de1f --- /dev/null +++ b/pkg/amdsevsnp/report_test.go @@ -0,0 +1,53 @@ +//go:build linux +// +build linux + +package amdsevsnp + +import ( + "testing" +) + +func Test_Mirror_NonEmpty_Byte_Slices(t *testing.T) { + type config struct { + name string + input []byte + expected []byte + } + + for _, conf := range []config{ + { + name: "Length0", + input: []byte{}, + expected: []byte{}, + }, + { + name: "Length1", + input: []byte{100}, + expected: []byte{100}, + }, + { + name: "LengthOdd", + input: []byte{100, 101, 102, 103, 104}, + expected: []byte{104, 103, 102, 101, 100}, + }, + { + name: "LengthEven", + input: []byte{100, 101, 102, 103, 104, 105}, + expected: []byte{105, 104, 103, 102, 101, 100}, + }, + } { + t.Run(conf.name, func(t *testing.T) { + result := mirrorBytes(conf.input) + if string(result[:]) != string(conf.expected[:]) { + t.Fatalf("the ipnut byte array %+v was not mirrored; %+v", conf.input, result) + } + }) + } +} + +func Test_Mirror_Nil_Slice(t *testing.T) { + result := mirrorBytes(nil) + if result != nil { + t.Fatalf("expected nil slice, got: %+v", result) + } +} diff --git a/pkg/securitypolicy/securitypolicy.go b/pkg/securitypolicy/securitypolicy.go index 2a539ed9db..a75ada7030 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,19 @@ func NewOpenDoorPolicy() *SecurityPolicy { } } +// NewSecurityPolicyDigest decodes base64 encoded policy string, computes +// and returns sha256 digest +func NewSecurityPolicyDigest(base64policy string) ([]byte, error) { + jsonPolicy, err := base64.StdEncoding.DecodeString(base64policy) + if err != nil { + return nil, fmt.Errorf("failed to decode base64 security policy: %w", err) + } + digest := sha256.New() + digest.Write(jsonPolicy) + digestBytes := digest.Sum(nil) + return digestBytes, 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/Makefile b/test/vendor/github.com/Microsoft/hcsshim/Makefile index e1a0f51129..c11786ce3a 100644 --- a/test/vendor/github.com/Microsoft/hcsshim/Makefile +++ b/test/vendor/github.com/Microsoft/hcsshim/Makefile @@ -1,4 +1,5 @@ BASE:=base.tar.gz +DEV_BUILD:=0 GO:=go GO_FLAGS:=-ldflags "-s -w" # strip Go binaries @@ -16,6 +17,12 @@ GO_BUILD:=CGO_ENABLED=$(CGO_ENABLED) $(GO) build $(GO_FLAGS) $(GO_FLAGS_EXTRA) SRCROOT=$(dir $(abspath $(firstword $(MAKEFILE_LIST)))) +DELTA_TARGET=out/delta.tar.gz + +ifeq "$(DEV_BUILD)" "1" +DELTA_TARGET=out/delta-dev.tar.gz +endif + # The link aliases for gcstools GCS_TOOLS=\ generichook \ @@ -55,6 +62,15 @@ out/delta.tar.gz: bin/init bin/vsockexec bin/cmd/gcs bin/cmd/gcstools bin/cmd/ho tar -zcf $@ -C rootfs . rm -rf rootfs +# This target includes utilities which may be useful for testing purposes. +out/delta-dev.tar.gz: out/delta.tar.gz bin/internal/tools/snp-report + rm -rf rootfs-dev + mkdir rootfs-dev + tar -xzf out/delta.tar.gz -C rootfs-dev + cp bin/internal/tools/snp-report rootfs-dev/bin/ + tar -zcf $@ -C rootfs-dev . + rm -rf rootfs-dev + out/rootfs.tar.gz: out/initrd.img rm -rf rootfs-conv mkdir rootfs-conv @@ -62,8 +78,8 @@ out/rootfs.tar.gz: out/initrd.img tar -zcf $@ -C rootfs-conv . rm -rf rootfs-conv -out/initrd.img: $(BASE) out/delta.tar.gz $(SRCROOT)/hack/catcpio.sh - $(SRCROOT)/hack/catcpio.sh "$(BASE)" out/delta.tar.gz > out/initrd.img.uncompressed +out/initrd.img: $(BASE) $(DELTA_TARGET) $(SRCROOT)/hack/catcpio.sh + $(SRCROOT)/hack/catcpio.sh "$(BASE)" $(DELTA_TARGET) > out/initrd.img.uncompressed gzip -c out/initrd.img.uncompressed > $@ rm out/initrd.img.uncompressed @@ -71,6 +87,7 @@ out/initrd.img: $(BASE) out/delta.tar.gz $(SRCROOT)/hack/catcpio.sh -include deps/cmd/gcstools.gomake -include deps/cmd/hooks/wait-paths.gomake -include deps/cmd/tar2ext4.gomake +-include deps/internal/tools/snp-report.gomake # Implicit rule for includes that define Go targets. %.gomake: $(SRCROOT)/Makefile 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 3acfa875c3..79a065454e 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 @@ -4,7 +4,6 @@ package uvm import ( "context" - "crypto/sha256" "encoding/base64" "fmt" "io" @@ -15,6 +14,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" @@ -402,28 +402,23 @@ 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) + policyDigest, 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)) + // HCS API expect a base64 encoded string as LaunchData. Internally it + // decodes it to bytes. SEV later returns the decoded byte blob as HostData + // field of the report. + hostData := base64.StdEncoding.EncodeToString(policyDigest) // 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..a75ada7030 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,19 @@ func NewOpenDoorPolicy() *SecurityPolicy { } } +// NewSecurityPolicyDigest decodes base64 encoded policy string, computes +// and returns sha256 digest +func NewSecurityPolicyDigest(base64policy string) ([]byte, error) { + jsonPolicy, err := base64.StdEncoding.DecodeString(base64policy) + if err != nil { + return nil, fmt.Errorf("failed to decode base64 security policy: %w", err) + } + digest := sha256.New() + digest.Write(jsonPolicy) + digestBytes := digest.Sum(nil) + return digestBytes, nil +} + // Internal version of SecurityPolicyContainer type securityPolicyContainer struct { // The command that we will allow the container to execute