From 8a28c5bd1c5dc0467e6a4adf6b9ab7081c57b592 Mon Sep 17 00:00:00 2001 From: Alex Wu Date: Fri, 13 Jan 2023 14:40:40 -0800 Subject: [PATCH] Parse EFI App state from the TCG event log The TCG PCClient firmware event log contains digests of all EFI applications. These are measured by UEFI whenever launching an application prior to handing off control to the boot loader or OS after ExitBootServices(). This introduces a change to pull those EFI measurements out of the event log so that VerifyAttestation users can validate the PCR4 values are exactly what they expect. This may be useful in cases where the Secure Boot hierarchy does not provide enough trust (e.g., a vulnerability in a signed component that requires a Secure Boot update). --- server/eventlog.go | 181 ++++++++++++++++++++++++++++++++----- server/eventlog_test.go | 177 +++++++++++++++++++++++++++++++++++- server/policy_constants.go | 23 +++-- 3 files changed, 352 insertions(+), 29 deletions(-) diff --git a/server/eventlog.go b/server/eventlog.go index 0618bd31a..3c70243c9 100644 --- a/server/eventlog.go +++ b/server/eventlog.go @@ -59,6 +59,10 @@ func parsePCClientEventLog(rawEventLog []byte, pcrs *tpmpb.PCRs, loader Bootload if err != nil { errors = append(errors, err) } + efiState, err := getEfiState(cryptoHash, rawEvents) + if err != nil { + errors = append(errors, err) + } var grub *pb.GrubState var kernel *pb.LinuxKernelState @@ -76,6 +80,7 @@ func parsePCClientEventLog(rawEventLog []byte, pcrs *tpmpb.PCRs, loader Bootload return &pb.MachineState{ Platform: platform, SecureBoot: sbState, + Efi: efiState, RawEvents: rawEvents, Hash: pcrs.GetHash(), Grub: grub, @@ -201,19 +206,59 @@ func getVerifiedCosState(coscel cel.CEL, pcrs *tpmpb.PCRs) (*pb.AttestedCosState return cosState, nil } -func getPlatformState(hash crypto.Hash, events []*pb.Event) (*pb.PlatformState, error) { - // We pre-compute the separator event hash, and check if the event type has - // been modified. We only trust events that come before a valid separator. +type separatorInfo struct { + separatorData [][]byte + separatorDigests [][]byte +} + +// getSeparatorInfo is used to return the valid event data and their corresponding +// digests. This is useful for events like separators, where the data is known +// ahead of time. +func getSeparatorInfo(hash crypto.Hash) *separatorInfo { hasher := hash.New() // From the PC Client Firmware Profile spec, on the separator event: // The event field MUST contain the hex value 00000000h or FFFFFFFFh. - separatorData := [][]byte{{0, 0, 0, 0}, {0xff, 0xff, 0xff, 0xff}} - separatorDigests := make([][]byte, 0, len(separatorData)) - for _, value := range separatorData { + sepData := [][]byte{{0, 0, 0, 0}, {0xff, 0xff, 0xff, 0xff}} + sepDigests := make([][]byte, 0, len(sepData)) + for _, value := range sepData { hasher.Write(value) - separatorDigests = append(separatorDigests, hasher.Sum(nil)) + sepDigests = append(sepDigests, hasher.Sum(nil)) } + return &separatorInfo{separatorData: sepData, separatorDigests: sepDigests} +} +// checkIfValidSeparator returns true if both the separator event's type and +// digest match the expected event data. +// If the event type is Separator, but the data is invalid, it returns false +// and an error. +// checkIfValidSeparator returns false and a nil error on other event types. +func checkIfValidSeparator(event *pb.Event, sepInfo *separatorInfo) (bool, error) { + evtType := event.GetUntrustedType() + index := event.GetPcrIndex() + if (evtType != Separator) && !contains(sepInfo.separatorDigests, event.GetDigest()) { + return false, nil + } + // To make sure we have a valid event, we check any event (e.g., separator) + // that claims to be of the event type or "looks like" the event to prevent + // certain vulnerabilities in event parsing. For more info see: + // https://github.com/google/go-attestation/blob/master/docs/event-log-disclosure.md + if evtType != Separator { + return false, fmt.Errorf("PCR%d event contains separator data but non-separator type %d", index, evtType) + } + if !event.GetDigestVerified() { + return false, fmt.Errorf("unverified separator digest for PCR%d", index) + } + if !contains(sepInfo.separatorData, event.GetData()) { + return false, fmt.Errorf("invalid separator data for PCR%d", index) + } + return true, nil +} + +func getPlatformState(hash crypto.Hash, events []*pb.Event) (*pb.PlatformState, error) { + // We pre-compute the separator and EFI Action event hash. + // We check if these events have been modified, since the event type is + // untrusted. + sepInfo := getSeparatorInfo(hash) var versionString []byte var nonHostInfo []byte for _, event := range events { @@ -223,20 +268,11 @@ func getPlatformState(hash crypto.Hash, events []*pb.Event) (*pb.PlatformState, } evtType := event.GetUntrustedType() - // Make sure we have a valid separator event, we check any event that - // claims to be a Separator or "looks like" a separator to prevent - // certain vulnerabilities in event parsing. For more info see: - // https://github.com/google/go-attestation/blob/master/docs/event-log-disclosure.md - if (evtType == Separator) || contains(separatorDigests, event.GetDigest()) { - if evtType != Separator { - return nil, fmt.Errorf("PCR%d event contains separator data but non-separator type %d", index, evtType) - } - if !event.GetDigestVerified() { - return nil, fmt.Errorf("unverified separator digest for PCR%d", index) - } - if !contains(separatorData, event.GetData()) { - return nil, fmt.Errorf("invalid separator data for PCR%d", index) - } + isSeparator, err := checkIfValidSeparator(event, sepInfo) + if err != nil { + return nil, err + } + if isSeparator { // Don't trust any PCR0 events after the separator break } @@ -422,6 +458,109 @@ func getGrubState(hash crypto.Hash, events []*pb.Event) (*pb.GrubState, error) { return &pb.GrubState{Files: files, Commands: commands}, nil } +func getEfiState(hash crypto.Hash, events []*pb.Event) (*pb.EfiState, error) { + // We pre-compute various event digests, and check if those event type have + // been modified. We only trust events that come before the + // ExitBootServices() request. + separatorInfo := getSeparatorInfo(hash) + + hasher := hash.New() + hasher.Write([]byte(CallingEFIApplication)) + callingEFIAppDigest := hasher.Sum(nil) + + hasher.Reset() + hasher.Write([]byte(ExitBootServicesInvocation)) + exitBootSvcDigest := hasher.Sum(nil) + + var efiAppStates []*pb.EfiApp + var seenSeparator4 bool + var seenSeparator5 bool + var seenCallingEfiApp bool + var seenExitBootServices bool + for _, event := range events { + index := event.GetPcrIndex() + evtType := event.GetUntrustedType() + + switch index { + default: + // Only process PCRs 4 and 5. + continue + case 4: + // Process Calling EFI Application event. + if bytes.Equal(callingEFIAppDigest, event.GetDigest()) { + if evtType != EFIAction { + return nil, fmt.Errorf("PCR%d contains CallingEFIApp event but non EFIAction type: %d", + index, evtType) + } + if !event.GetDigestVerified() { + return nil, fmt.Errorf("unverified CallingEFIApp digest for PCR%d", index) + } + // We don't support calling more than one boot device. + if seenCallingEfiApp { + return nil, fmt.Errorf("found duplicate CallingEFIApp event in PCR%d", index) + } + if seenSeparator4 { + return nil, fmt.Errorf("found CallingEFIApp event in PCR%d after separator event", index) + } + seenCallingEfiApp = true + } + + if evtType == EFIBootServicesApplication { + if !seenCallingEfiApp { + return nil, fmt.Errorf("found EFIBootServicesApplication in PCR%d before CallingEFIApp event", index) + } + efiAppStates = append(efiAppStates, &pb.EfiApp{Digest: event.GetDigest()}) + } + + isSeparator, err := checkIfValidSeparator(event, separatorInfo) + if err != nil { + return nil, err + } + if !isSeparator { + continue + } + if seenSeparator4 { + return nil, errors.New("found duplicate Separator event in PCR4") + } + seenSeparator4 = true + case 5: + // Process ExitBootServices event. + if bytes.Equal(exitBootSvcDigest, event.GetDigest()) { + if evtType != EFIAction { + return nil, fmt.Errorf("PCR%d contains ExitBootServices event but non EFIAction type: %d", + index, evtType) + } + if !event.GetDigestVerified() { + return nil, fmt.Errorf("unverified ExitBootServices digest for PCR%d", index) + } + // Don't process any PCR4 or PCR5 events after Boot Manager has + // requested ExitBootServices(). + seenExitBootServices = true + break + } + + isSeparator, err := checkIfValidSeparator(event, separatorInfo) + if err != nil { + return nil, err + } + if !isSeparator { + continue + } + if seenSeparator5 { + return nil, errors.New("found duplicate Separator event in PCR5") + } + seenSeparator5 = true + } + } + // Only write EFI digests if we see an ExitBootServices invocation. + // Otherwise, software further down the bootchain could extend bad + // PCR4 measurements. + if seenExitBootServices { + return &pb.EfiState{Apps: efiAppStates}, nil + } + return nil, nil +} + func getLinuxKernelStateFromGRUB(grub *pb.GrubState) (*pb.LinuxKernelState, error) { var cmdline string seen := false diff --git a/server/eventlog_test.go b/server/eventlog_test.go index 530c58bbd..5abb06937 100644 --- a/server/eventlog_test.go +++ b/server/eventlog_test.go @@ -20,8 +20,9 @@ import ( ) type eventLog struct { - RawLog []byte - Banks []*pb.PCRs + RawLog []byte + Banks []*pb.PCRs + ExpectedEFIAppDigests map[pb.HashAlgo][]string } var archLinuxBadSecureBoot = "SecureBoot data len is 0, expected 1" @@ -60,6 +61,23 @@ var Rhel8GCE = eventLog{ 14: decodeHex("d8f57ebcc1a23cc46832696e1a657f720e1be8f5b405bb7204682114e363b455"), }, }}, + ExpectedEFIAppDigests: map[pb.HashAlgo][]string{ + pb.HashAlgo_SHA1: { + "95f400d9003b4e8c0cb4734efcf547e36fc4100c", + "4f60d11ad6ac9a76837834f1371bc9521d018779", + "075f3bc8c7363c35a87ce56c604fa9201a97f79d", + }, + pb.HashAlgo_SHA256: { + "40d6cae02973789080cf4c3a9ad11b5a0a4d8bba4438ab96e276cc784454dee7", + "e8a268c431da72caaae407f729f602b9dbf5d1d43492d4a51cc2b688a08586e3", + "e4c0382f98feaebfd43923a85fd6da9a20e1a48524a4d5928c31850ca1a96a6e", + }, + pb.HashAlgo_SHA384: { + "66de9a210659294720af06838309fc1f4d0de82c646a62c1dd9f068cd331d2e05fd666377dbc11e84a796ce00108ab19", + "c1d031b07446588fa50f4eec3d8520d99ed01f21350b9c581e13f4c5a8c712cb5e3cbecc41ccab74465543439f7eb1e6", + "d844e63b32a73aadde4f78dda7cb7df73d75114f3a5964401847eb716142a06607ea95efee20f51283e85afca8da3afd", + }, + }, } // Agile Event Log from a Ubuntu 18.04 GCE instance with Secure Boot and @@ -95,6 +113,20 @@ var UbuntuAmdSevGCE = eventLog{ 9: decodeHex("2d334f1eeb9a16dabaccaa746ff1c0dce2e9aeb3f3a4a314e5e1e61b01e940d0"), }, }}, + ExpectedEFIAppDigests: map[pb.HashAlgo][]string{ + pb.HashAlgo_SHA1: { + "21e79438580ec89df674dfe12653d77d132c3936", + "9a4c7c895a5d40c3906121ff59c6fe267a4c32e0", + }, + pb.HashAlgo_SHA256: { + "2ea4cb6a1f1eb1d3dce82d54fde26ded243ba3e18de7c6d211902a594fe56788", + "835f940e97bac2f7c171819b1fcc4bebe72a1c4ea7d7245088ef32d253085bb3", + }, + pb.HashAlgo_SHA384: { + "9b2baf7073fd9b7df3091b69ae7e48453450ae7b5311b37de11b79da75f175b8b2ed69f7d39406501653b35cbe90a030", + "b0a19b24395a4690eea97916483dc291a38c6023df20aa296d85064194cebe9097f6b5e8490fd57a4e6b01167a8c9c7c", + }, + }, } // Agile Event Log from a Ubuntu 21.04 GCE instance without a DBX and with Secure Boot disabled @@ -131,6 +163,20 @@ var Ubuntu2104NoDbxGCE = eventLog{ 14: decodeHex("8351c65483c5419079e8c96758dd2130bee075d71fea226f68ec4eb5bfc71983"), }, }}, + ExpectedEFIAppDigests: map[pb.HashAlgo][]string{ + pb.HashAlgo_SHA1: { + "92e6ec17937f600b9ec7f23adf4ea5553b4e2364", + "4f9604e61091095594c206c8a404afe187a92586", + }, + pb.HashAlgo_SHA256: { + "d99c93fcb042dbe52707bbde371c75fcf081dd5b0c88a195d44cc57536f6f521", + "b0a836fec2faf4a9bea0e1a5f1945bc86ddc03ac98ce0ae172ed9b1e536d7595", + }, + pb.HashAlgo_SHA384: { + "d8811e9c08119168b156255c6d695614d1593422bc5044186d29c1aaaa86fff0a633f324ac1ac1122e547479ce50a75a", + "bbcdda8a6d872385b10802434eb8de1ac7b92dbaddf18bc1d7ea24fcc71b45291db5cc7b930a29c93405d6aecdb70683", + }, + }, } // Agile Event Log from a Ubuntu 21.04 GCE instance with Secure Boot disabled @@ -167,6 +213,20 @@ var Ubuntu2104NoSecureBootGCE = eventLog{ 14: decodeHex("8351c65483c5419079e8c96758dd2130bee075d71fea226f68ec4eb5bfc71983"), }, }}, + ExpectedEFIAppDigests: map[pb.HashAlgo][]string{ + pb.HashAlgo_SHA1: { + "22df40d6e32d4721f1b2406b2b4a3bb0ca10ead5", + "4f9604e61091095594c206c8a404afe187a92586", + }, + pb.HashAlgo_SHA256: { + "6265b732b005b3f330bcd1843374e5ec6ec5aef27cdb97a23daeb8580abbf526", + "b0a836fec2faf4a9bea0e1a5f1945bc86ddc03ac98ce0ae172ed9b1e536d7595", + }, + pb.HashAlgo_SHA384: { + "4f491210da8f59f09cd16523b44db22e83d8b611c3b14656d3b078dd451347ab195177fc78cf8d5578376f1f5f9bb821", + "bbcdda8a6d872385b10802434eb8de1ac7b92dbaddf18bc1d7ea24fcc71b45291db5cc7b930a29c93405d6aecdb70683", + }, + }, } // Agile Event Log from Alex's gLinux laptop with secure boot disabled @@ -247,6 +307,16 @@ var Debian10GCE = eventLog{ 7: decodeHex("9e6c57e850f371c2a7fe02bca552149363952318"), }, }}, + // We shouldn't use these digests, as this Debian firmware does not measure + // ExitBootService events, which means an attacker could extend additional + // events after UEFI hands off the event log. + ExpectedEFIAppDigests: map[pb.HashAlgo][]string{ + pb.HashAlgo_SHA1: { + "47263679db883d7ad9adbc93d6a1fbf8095f0133", + "3fae23b18d72350207661af3875f2c492e97621c", + "89b08941b47dcfbd4c8b3f2bc0fad984cd836b21", + }, + }, } // Agile Event Log from a Ubuntu 21.04 GCE instance with Secure Boot disabled @@ -281,6 +351,23 @@ var COS85AmdSev = eventLog{ 9: decodeHex("f4f2d92d6d54f6c41f2706fd98091317642e0680a7902c72893d41e3464a93b7"), }, }}, + ExpectedEFIAppDigests: map[pb.HashAlgo][]string{ + pb.HashAlgo_SHA1: { + "bfeec15d9359fe0aa8b5fb6451d1f73e5144c6d3", + "860848ad3f129051f1e252749011cf7f7df837ee", + "91cd5aa9c3e407237e8aeb122d4ab94494034a90", + }, + pb.HashAlgo_SHA256: { + "dba8d69ffb244496ac8ab2950695d3da539d6ac5ec660fc6b4bdde245284cf23", + "f7bad83f87940312e4642530a9a6242e88529dc37a497d7d4e7c1c070566d542", + "6f6afb3caed004e727200a0c310731bd8ab4cd391b2d95cedf67d08e1e8e5e7e", + }, + pb.HashAlgo_SHA384: { + "778bd7d6385d8ca0da5e504e3e554b67d98d9a712d957cb4cbb4d9b2e66ca96e31ddc18680af02b03a3a8a1b08da6aca", + "d014c8c69b17ceb0f46be22b928f52684e717f40288246a61dadba00b1368c883cdde4e98762cc6788d94d0bcbd3f7ca", + "ff8ff1db8fc98d02d944a90c58103b1b2ad3ba893ba4f302a006a572951491622341bb9387de20dd072cb8b6b3583cd0", + }, + }, } var COS93AmdSev = eventLog{ @@ -314,6 +401,23 @@ var COS93AmdSev = eventLog{ 9: decodeHex("0b1e4f9ca7bc8535c4c33f0025969d7abea008aa51dcd7f7c2d1068470e4bce4"), }, }}, + ExpectedEFIAppDigests: map[pb.HashAlgo][]string{ + pb.HashAlgo_SHA1: { + "d582c2803fd716f09e50c82967079ff593e1bc6b", + "e3de6a97421ba8f329d4ba55e39df80013415a23", + "03221584436f78e488cdaec3c691b7a18ff2f621", + }, + pb.HashAlgo_SHA256: { + "27cce48e55b3bfb6eb6206a4cc2b53a497846496a6264495006ab28dffa5623e", + "e3e226fb8c8e3b3fdb56c706a0fbfda080f34068aef5a1889c1bfa95f04c2e72", + "dc0aca594caee03705bcfa817e7f666692d89b713815f4793b7abbc2a0e00b6c", + }, + pb.HashAlgo_SHA384: { + "da419d9c92eb55b6e14f5665d81644fa163b908b1b1e317740f7a605f1734994dd90f4ea3373400c59fd7683751e30ef", + "794e6206fe520d3b0bcbfd3e14b0dc8e41f6a8c3b131faef69442a11625fde690a1b77c46dcddcb443a8d3c1e3ea669c", + "64b218ab263625b49da1172a9ab37cedbcd20d668beac1c3baac4cae640a1a7f77a07c05682b4147ec649c51243f6bbf", + }, + }, } var COS101AmdSev = eventLog{ @@ -349,6 +453,26 @@ var COS101AmdSev = eventLog{ 14: decodeHex("d0d95459205afae879514db7b85630f5d6b8272ed8c731bf92933dbc9fe99969"), }, }}, + ExpectedEFIAppDigests: map[pb.HashAlgo][]string{ + pb.HashAlgo_SHA1: { + "dc41c297c4ed857e9b6354cad8b448995c3052ea", + "06ae09413b5107bb26aa68602ba4fe787d22f82e", + "f894ac3a351baa3a5ce4dd8d6f497eb616723461", + "f894ac3a351baa3a5ce4dd8d6f497eb616723461", + }, + pb.HashAlgo_SHA256: { + "c7ac5d44444affd8d4a7c5d3dea0ce20a71e05812fc18777a428d092f78ae3ff", + "c5d3b47de11a9a2a4a15ef5cb7202d7800a10609c0dcecc46e3e963d476b76ce", + "af4161084115c9d5c1872f4473fe974b535e3a9a767688293720ac2cc6f7f9a3", + "af4161084115c9d5c1872f4473fe974b535e3a9a767688293720ac2cc6f7f9a3", + }, + pb.HashAlgo_SHA384: { + "72bf185794a865eb14fcdf93a2daa8ed281c932e2a7009d8489c38056389b3f3776d755ec703c95fb9c396f79dbd52c7", + "5b38df39c7beec3bfd9c4cbd40c217bcbee190d1fa099a64c5f063d20efc3def26e48cbbd86d730c8eb4696a29759490", + "968f2f6cb5bae537adfca30942803ddcda773bae368c042258e8818788265cd0e119936c9fcdb782785154a6705c5143", + "968f2f6cb5bae537adfca30942803ddcda773bae368c042258e8818788265cd0e119936c9fcdb782785154a6705c5143", + }, + }, } func TestParseEventLogs(t *testing.T) { @@ -749,6 +873,55 @@ func TestParseGrubStateFail(t *testing.T) { } } +func TestParseEfiState(t *testing.T) { + logs := []struct { + eventLog + name string + }{ + {Rhel8GCE, "Rhel8GCE"}, + {UbuntuAmdSevGCE, "UbuntuAmdSevGCE"}, + {Ubuntu2104NoSecureBootGCE, "Ubuntu2104NoSecureBootGCE"}, + {COS85AmdSev, "COS85AmdSev"}, + {COS93AmdSev, "COS93AmdSev"}, + {COS101AmdSev, "COS101AmdSev"}, + } + for _, log := range logs { + for _, bank := range log.Banks { + hashName := pb.HashAlgo_name[int32(bank.Hash)] + subtestName := fmt.Sprintf("%s-%s", log.name, hashName) + t.Run(subtestName, func(t *testing.T) { + msState, err := parsePCClientEventLog(log.RawLog, bank, UnsupportedLoader) + if err != nil { + t.Errorf("parsePCClientEventLog(%v, %v) got err = %v, want nil", log.name, bank.GetHash().String(), err) + } + + if msState.GetEfi() == nil { + t.Error("msState.GetEfi() returned nil, want EFI state") + } + efiApps := msState.GetEfi().GetApps() + if len(efiApps) == 0 { + t.Error("msState.GetEfi().GetApps() returned empty, want non-zero length") + } + expectedDigestStrs := log.ExpectedEFIAppDigests[bank.Hash] + if len(expectedDigestStrs) == 0 { + t.Fatalf("%v log used to test EFIState, but it has no expected EFI App digests", log.name) + } + expectedDigests := make([][]byte, 0, len(expectedDigestStrs)) + for _, digestStr := range log.ExpectedEFIAppDigests[bank.Hash] { + expectedDigests = append(expectedDigests, decodeHex(digestStr)) + } + gotDigests := make([][]byte, 0, len(efiApps)) + for _, app := range efiApps { + gotDigests = append(gotDigests, app.GetDigest()) + } + if !cmp.Equal(gotDigests, expectedDigests) { + t.Errorf("msState.GetEfi().GetApps() digests got %v, want %v", gotDigests, expectedDigests) + } + }) + } + } +} + func decodeHex(hexStr string) []byte { bytes, err := hex.DecodeString(hexStr) if err != nil { diff --git a/server/policy_constants.go b/server/policy_constants.go index 728541f33..bb0f7df55 100644 --- a/server/policy_constants.go +++ b/server/policy_constants.go @@ -11,16 +11,27 @@ import ( pb "github.com/google/go-tpm-tools/proto/attest" ) -// Expected Firmware/PCR0 Event Types. +// Expected TCG Event Log Event Types. // // Taken from TCG PC Client Platform Firmware Profile Specification, // Table 14 Events. const ( - NoAction uint32 = 0x00000003 - Separator uint32 = 0x00000004 - SCRTMVersion uint32 = 0x00000008 - NonhostInfo uint32 = 0x00000011 - IPL uint32 = 0x0000000D + NoAction uint32 = 0x00000003 + Separator uint32 = 0x00000004 + SCRTMVersion uint32 = 0x00000008 + IPL uint32 = 0x0000000D + NonhostInfo uint32 = 0x00000011 + EFIBootServicesApplication uint32 = 0x80000003 + EFIAction uint32 = 0x80000007 +) + +// Constant events used with type "EV_EFI_ACTION". +// Taken from TCG PC Client Platform Firmware Profile Specification, +// Table 17 EV_EFI_ACTION Strings. +const ( + // Measured when Boot Manager attempts to execute code from a Boot Option. + CallingEFIApplication string = "Calling EFI Application from Boot Option" + ExitBootServicesInvocation string = "Exit Boot Services Invocation" ) var (