From ad71804a488649b027fef35860d3c6e14fcd73e7 Mon Sep 17 00:00:00 2001 From: Alex Wu Date: Thu, 10 Apr 2025 19:20:47 +0000 Subject: [PATCH] Add support for empty SecureBoot variables This allows for successful firmware event log parsing for firmwares that don't set the SecureBoot variable (like TDVF). --- ccel/cceventlog_test.go | 112 +++++++++++ ccel/replay_test.go | 46 +++-- extract/extract.go | 9 +- extract/secureboot.go | 12 +- extract/secureboot_test.go | 144 +++++++++++++- legacy/secureboot.go.bak | 296 ---------------------------- tcg/events.go | 27 +++ testdata/eventlogs/ccel/gdc-tdx.bin | Bin 0 -> 65536 bytes 8 files changed, 323 insertions(+), 323 deletions(-) delete mode 100644 legacy/secureboot.go.bak create mode 100644 testdata/eventlogs/ccel/gdc-tdx.bin diff --git a/ccel/cceventlog_test.go b/ccel/cceventlog_test.go index 415d369..dc86151 100644 --- a/ccel/cceventlog_test.go +++ b/ccel/cceventlog_test.go @@ -27,6 +27,40 @@ import ( type eventLog struct { fname string mrs []register.MR + // TODO: migrate off of the slice based bank type and move to a map-based representation. + rtmrs []register.RTMR +} + +var COS113TDX = eventLog{ + fname: "../testdata/eventlogs/ccel/cos-113-intel-tdx.bin", + mrs: []register.MR{ + register.RTMR{ + Index: 0, + Digest: []byte("?\xa2\xf6\x1f9[\x7f_\xee\xfbN\xc2\xdfa)\x7f\x10\x9aث\xcdd\x10\xc1\xb7\xdf`\xf2\x1f7\xb1\x92\x97\xfc5\xe5D\x03\x9c~\x1e\xde\xceu*\xfd\x17\xf6"), + }, + register.RTMR{ + Index: 1, + Digest: []byte("\xf6-\xbc\a+\xd5\xd3\xf3C\x8b{5Úr\x7fZ\xea/\xfc$s\xf47#\x95?S\r\xafbPO\nyD\xaab\xc4\x1a\x86\xe8\xa8x±\"\xc1"), + }, + register.RTMR{ + Index: 2, + Digest: []byte("IihM\xc8s\x81\xfc;14\x17l\x8d\x88\x06\xea\xf0\xa9\x01\x85\x9f_pϮ\x8d\x17qKF\xc1\n\x8d\xe2\x19\x04\x8c\x9f\xc0\x9f\x11\xf3\x81\xa6\xfb\xe7\xc1"), + }, + }, + rtmrs: []register.RTMR{ + { + Index: 0, + Digest: []byte("?\xa2\xf6\x1f9[\x7f_\xee\xfbN\xc2\xdfa)\x7f\x10\x9aث\xcdd\x10\xc1\xb7\xdf`\xf2\x1f7\xb1\x92\x97\xfc5\xe5D\x03\x9c~\x1e\xde\xceu*\xfd\x17\xf6"), + }, + { + Index: 1, + Digest: []byte("\xf6-\xbc\a+\xd5\xd3\xf3C\x8b{5Úr\x7fZ\xea/\xfc$s\xf47#\x95?S\r\xafbPO\nyD\xaab\xc4\x1a\x86\xe8\xa8x±\"\xc1"), + }, + { + Index: 2, + Digest: []byte("IihM\xc8s\x81\xfc;14\x17l\x8d\x88\x06\xea\xf0\xa9\x01\x85\x9f_pϮ\x8d\x17qKF\xc1\n\x8d\xe2\x19\x04\x8c\x9f\xc0\x9f\x11\xf3\x81\xa6\xfb\xe7\xc1"), + }, + }, } var COS113TDXUnpadded = eventLog{ @@ -45,6 +79,20 @@ var COS113TDXUnpadded = eventLog{ Digest: []byte("IihM\xc8s\x81\xfc;14\x17l\x8d\x88\x06\xea\xf0\xa9\x01\x85\x9f_pϮ\x8d\x17qKF\xc1\n\x8d\xe2\x19\x04\x8c\x9f\xc0\x9f\x11\xf3\x81\xa6\xfb\xe7\xc1"), }, }, + rtmrs: []register.RTMR{ + { + Index: 0, + Digest: []byte("\xa4\xde-\xf2>\x96\x11)\x91#\xbaCY\xc4*^W\x8b\x0f\x84\x88\xbf\x1b\xba\x8e\xf5`m\x9e\xa5\xd8\x1c\x97\xc0d\xb4\x82\xa5\xea\xc57\xd1f\xbd\x0f\x0fu-"), + }, + { + Index: 1, + Digest: []byte("\x0e\xe96l\x92\x8aw\t/U\xe9\xe1\x14\xc79A\x81\xfd&F\x99\x15_\r\xf7}#Wv\x18\xd5\xf6PV\x8a\x17\xd3y5Z\a\xbd\x84nU/N "), + }, + { + Index: 2, + Digest: []byte("IihM\xc8s\x81\xfc;14\x17l\x8d\x88\x06\xea\xf0\xa9\x01\x85\x9f_pϮ\x8d\x17qKF\xc1\n\x8d\xe2\x19\x04\x8c\x9f\xc0\x9f\x11\xf3\x81\xa6\xfb\xe7\xc1"), + }, + }, } var COS113TDXPadded = eventLog{ @@ -63,6 +111,20 @@ var COS113TDXPadded = eventLog{ Digest: []byte("IihM\xc8s\x81\xfc;14\x17l\x8d\x88\x06\xea\xf0\xa9\x01\x85\x9f_pϮ\x8d\x17qKF\xc1\n\x8d\xe2\x19\x04\x8c\x9f\xc0\x9f\x11\xf3\x81\xa6\xfb\xe7\xc1"), }, }, + rtmrs: []register.RTMR{ + { + Index: 0, + Digest: []byte("\xa4\xde-\xf2>\x96\x11)\x91#\xbaCY\xc4*^W\x8b\x0f\x84\x88\xbf\x1b\xba\x8e\xf5`m\x9e\xa5\xd8\x1c\x97\xc0d\xb4\x82\xa5\xea\xc57\xd1f\xbd\x0f\x0fu-"), + }, + { + Index: 1, + Digest: []byte("\x0e\xe96l\x92\x8aw\t/U\xe9\xe1\x14\xc79A\x81\xfd&F\x99\x15_\r\xf7}#Wv\x18\xd5\xf6PV\x8a\x17\xd3y5Z\a\xbd\x84nU/N "), + }, + { + Index: 2, + Digest: []byte("IihM\xc8s\x81\xfc;14\x17l\x8d\x88\x06\xea\xf0\xa9\x01\x85\x9f_pϮ\x8d\x17qKF\xc1\n\x8d\xe2\x19\x04\x8c\x9f\xc0\x9f\x11\xf3\x81\xa6\xfb\xe7\xc1"), + }, + }, } var IntelTestCCEL = eventLog{ @@ -81,6 +143,46 @@ var IntelTestCCEL = eventLog{ Digest: []byte("\x80\x83\xcdh\x98\xccR\xa9\x021\xcd\xf9\xc0S+\xf9Q<@F\\oq\xe5l\xbe2\xee,\x11\xa9\xdf\xc00)|\xa3\xca\x0fbG}m\x1fa\r?\xdb"), }, }, + rtmrs: []register.RTMR{ + { + Index: 0, + Digest: []byte("\x80\x83\xcdh\x98\xccR\xa9\x021\xcd\xf9\xc0S+\xf9Q<@F\\oq\xe5l\xbe2\xee,\x11\xa9\xdf\xc00)|\xa3\xca\x0fbG}m\x1fa\r?\xdb"), + }, + { + Index: 1, + Digest: []byte("\x80\x83\xcdh\x98\xccR\xa9\x021\xcd\xf9\xc0S+\xf9Q<@F\\oq\xe5l\xbe2\xee,\x11\xa9\xdf\xc00)|\xa3\xca\x0fbG}m\x1fa\r?\xdb"), + }, + { + Index: 2, + Digest: []byte("\x80\x83\xcdh\x98\xccR\xa9\x021\xcd\xf9\xc0S+\xf9Q<@F\\oq\xe5l\xbe2\xee,\x11\xa9\xdf\xc00)|\xa3\xca\x0fbG}m\x1fa\r?\xdb"), + }, + }, +} + +var GDCCCEL = eventLog{ + fname: "../testdata/eventlogs/ccel/gdc-tdx.bin", + mrs: []register.MR{ + register.RTMR{ + Index: 0, + Digest: []byte("FU\xef\x03\xc8w\xb3\xd7Jf >F\x85\x8f\xb9\x90۩t\xa4\\\xa6P\x85\xbcFE\x943n\x04\xebI\xca\x10\x0ej\x1c\xeb\xe7\xae2/2\x88\xb0\x8f")}, + register.RTMR{ + Index: 1, + Digest: []byte("\xbf\x86\xaa\xc1@\xc1\x05\a\xb7<#\xd2\xf3\xa6v\xb6\xa3iZ\x9a\xad\xe3c5s1\x80\xb0K\x0e\xec\xd2\r\x05\xab\xe2\xe3\xaa^\x8b\v\xads\xfa\xe3\x0f4\xf4")}, + register.RTMR{ + Index: 2, + Digest: []byte("\xb6_\x82\x02\xd0\xd3\xc9g\x9f\xe0\xb1\xf3\xf3A\xa5\xc8\ue91e\xa4\x93\x14d\x16\xde\xed\x8a\xe3c\xd7c%D\xd4)BN* \x824\xc7n\xd5\xc1\xba\t\xce")}, + }, + rtmrs: []register.RTMR{ + { + Index: 0, + Digest: []byte("FU\xef\x03\xc8w\xb3\xd7Jf >F\x85\x8f\xb9\x90۩t\xa4\\\xa6P\x85\xbcFE\x943n\x04\xebI\xca\x10\x0ej\x1c\xeb\xe7\xae2/2\x88\xb0\x8f")}, + { + Index: 1, + Digest: []byte("\xbf\x86\xaa\xc1@\xc1\x05\a\xb7<#\xd2\xf3\xa6v\xb6\xa3iZ\x9a\xad\xe3c5s1\x80\xb0K\x0e\xec\xd2\r\x05\xab\xe2\xe3\xaa^\x8b\v\xads\xfa\xe3\x0f4\xf4")}, + { + Index: 2, + Digest: []byte("\xb6_\x82\x02\xd0\xd3\xc9g\x9f\xe0\xb1\xf3\xf3A\xa5\xc8\ue91e\xa4\x93\x14d\x16\xde\xed\x8a\xe3c\xd7c%D\xd4)BN* \x824\xc7n\xd5\xc1\xba\t\xce")}, + }, } func TestParseAndReplay(t *testing.T) { @@ -109,6 +211,16 @@ func TestParseAndReplay(t *testing.T) { allowPadding: false, wantErr: true, }, + { + el: GDCCCEL, + allowPadding: true, + wantErr: false, + }, + { + el: GDCCCEL, + allowPadding: false, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.el.fname+"_allowPadding_"+strconv.FormatBool(tt.allowPadding), func(t *testing.T) { diff --git a/ccel/replay_test.go b/ccel/replay_test.go index 337a3c0..f07af27 100644 --- a/ccel/replay_test.go +++ b/ccel/replay_test.go @@ -16,6 +16,7 @@ package ccel import ( "os" + "strconv" "strings" "testing" @@ -24,26 +25,41 @@ import ( ) func TestReplayAndExtract(t *testing.T) { - elBytes, err := os.ReadFile("../testdata/eventlogs/ccel/cos-113-intel-tdx.bin") - if err != nil { - t.Fatal(err) - } tableBytes, err := os.ReadFile("../testdata/eventlogs/ccel/cos-113-intel-tdx.table.bin") if err != nil { t.Fatal(err) } + tests := []struct { + el eventLog + opts extract.Opts + wantErr bool + }{ + { + el: COS113TDX, + opts: extract.Opts{Loader: extract.GRUB}, + }, + { + el: GDCCCEL, + opts: extract.Opts{Loader: extract.GRUB, AllowEmptySBVar: true}, + }, + { + el: GDCCCEL, + opts: extract.Opts{Loader: extract.GRUB}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.el.fname+strconv.FormatBool(tt.wantErr), func(t *testing.T) { + elBytes, err := os.ReadFile(tt.el.fname) + if err != nil { + t.Fatal(err) + } - rtmr0 := []byte("?\xa2\xf6\x1f9[\x7f_\xee\xfbN\xc2\xdfa)\x7f\x10\x9aث\xcdd\x10\xc1\xb7\xdf`\xf2\x1f7\xb1\x92\x97\xfc5\xe5D\x03\x9c~\x1e\xde\xceu*\xfd\x17\xf6") - rtmr1 := []byte("\xf6-\xbc\a+\xd5\xd3\xf3C\x8b{5Úr\x7fZ\xea/\xfc$s\xf47#\x95?S\r\xafbPO\nyD\xaab\xc4\x1a\x86\xe8\xa8x±\"\xc1") - rtmr2 := []byte("IihM\xc8s\x81\xfc;14\x17l\x8d\x88\x06\xea\xf0\xa9\x01\x85\x9f_pϮ\x8d\x17qKF\xc1\n\x8d\xe2\x19\x04\x8c\x9f\xc0\x9f\x11\xf3\x81\xa6\xfb\xe7\xc1") - bank := register.RTMRBank{RTMRs: []register.RTMR{ - {Index: 0, Digest: rtmr0}, - {Index: 1, Digest: rtmr1}, - {Index: 2, Digest: rtmr2}, - }} - _, err = ReplayAndExtract(tableBytes, elBytes, bank, extract.Opts{Loader: extract.GRUB}) - if err != nil { - t.Errorf("failed to ReplayAndExtract from CCEL: %v", err) + _, err = ReplayAndExtract(tableBytes, elBytes, register.RTMRBank{RTMRs: tt.el.rtmrs}, tt.opts) + if (err != nil) != tt.wantErr { + t.Errorf("ReplayAndExtract: got %v, wantErr %v", err, tt.wantErr) + } + }) } } diff --git a/extract/extract.go b/extract/extract.go index 31d9985..7246d86 100644 --- a/extract/extract.go +++ b/extract/extract.go @@ -57,6 +57,9 @@ const ( // Opts gives options for extracting information from an event log. type Opts struct { Loader Bootloader + // AllowEmptySBVar allows the SecureBoot variable to be empty in addition to length 1 (0 or 1). + // This can be used when the SecureBoot variable is not initialized. + AllowEmptySBVar bool } // FirmwareLogState extracts event info from a verified TCG PC Client event @@ -81,7 +84,7 @@ func FirmwareLogState(events []tcg.Event, hash crypto.Hash, registerCfg register if err != nil { joined = errors.Join(joined, err) } - sbState, err := SecureBootState(events, registerCfg) + sbState, err := SecureBootState(events, registerCfg, opts) if err != nil { joined = errors.Join(joined, err) } @@ -209,8 +212,8 @@ func matchWellKnown(cert x509.Certificate) (pb.WellKnownCertificate, error) { // SecureBootState extracts Secure Boot information from a UEFI TCG2 // firmware event log. -func SecureBootState(replayEvents []tcg.Event, registerCfg registerConfig) (*pb.SecureBootState, error) { - attestSbState, err := ParseSecurebootState(replayEvents, registerCfg) +func SecureBootState(replayEvents []tcg.Event, registerCfg registerConfig, opts Opts) (*pb.SecureBootState, error) { + attestSbState, err := ParseSecurebootState(replayEvents, registerCfg, opts) if err != nil { return nil, fmt.Errorf("failed to parse SecureBootState: %v", err) } diff --git a/extract/secureboot.go b/extract/secureboot.go index b0f28da..48ca162 100644 --- a/extract/secureboot.go +++ b/extract/secureboot.go @@ -97,7 +97,7 @@ func ParseSecurebootStateLegacy(events []tcg.Event) (*SecurebootState, error) { // - If SecureBoot was 1 (enabled), platform + exchange + database keys // were specified. // - No UEFI debugger was attached. - return ParseSecurebootState(events, TPMRegisterConfig) + return ParseSecurebootState(events, TPMRegisterConfig, Opts{}) } // ParseSecurebootState parses a series of events to determine the @@ -105,7 +105,7 @@ func ParseSecurebootStateLegacy(events []tcg.Event) (*SecurebootState, error) { // the state cannot be determined, or if the event log is structured // in such a way that it may have been tampered post-execution of // platform firmware. -func ParseSecurebootState(events []tcg.Event, registerCfg registerConfig) (*SecurebootState, error) { +func ParseSecurebootState(events []tcg.Event, registerCfg registerConfig, opts Opts) (*SecurebootState, error) { var ( out SecurebootState seenSeparator7 bool @@ -173,10 +173,14 @@ func ParseSecurebootState(events []tcg.Event, registerCfg registerConfig) (*Secu switch v.VarName() { case "SecureBoot": - if len(v.VariableData) != 1 { + if len(v.VariableData) == 1 { + out.Enabled = v.VariableData[0] == 1 + } else if len(v.VariableData) == 0 && opts.AllowEmptySBVar { + out.Enabled = false + } else { return nil, fmt.Errorf("event %d: SecureBoot data len is %d, expected 1", e.Num(), len(v.VariableData)) } - out.Enabled = v.VariableData[0] == 1 + case "PK": if out.PlatformKeys, out.PlatformKeyHashes, err = v.SignatureData(); err != nil { return nil, fmt.Errorf("event %d: failed parsing platform keys: %v", e.Num(), err) diff --git a/extract/secureboot_test.go b/extract/secureboot_test.go index 7f86233..63c8ab7 100644 --- a/extract/secureboot_test.go +++ b/extract/secureboot_test.go @@ -15,12 +15,15 @@ package extract_test import ( + "bytes" "crypto" + "crypto/sha256" "encoding/base64" "encoding/json" "os" "testing" + "github.com/google/go-cmp/cmp" "github.com/google/go-eventlog/extract" "github.com/google/go-eventlog/internal/testutil" "github.com/google/go-eventlog/proto/state" @@ -50,7 +53,7 @@ func TestSecureBoot(t *testing.T) { t.Fatalf("validating event log: %v", err) } - sbState, err := extract.ParseSecurebootState(events, extract.TPMRegisterConfig) + sbState, err := extract.ParseSecurebootState(events, extract.TPMRegisterConfig, extract.Opts{}) if err != nil { t.Fatalf("ExtractSecurebootState() failed: %v", err) } @@ -127,7 +130,7 @@ func TestSecureBootBug157(t *testing.T) { t.Fatalf("failed to verify log: %v", err) } - sbs, err := extract.ParseSecurebootState(events, extract.TPMRegisterConfig) + sbs, err := extract.ParseSecurebootState(events, extract.TPMRegisterConfig, extract.Opts{}) if err != nil { t.Fatalf("failed parsing secureboot state: %v", err) } @@ -170,7 +173,7 @@ func TestSecureBootOptionRom(t *testing.T) { t.Errorf("failed to verify log: %v", err) } - sbs, err := extract.ParseSecurebootState(events, extract.TPMRegisterConfig) + sbs, err := extract.ParseSecurebootState(events, extract.TPMRegisterConfig, extract.Opts{}) if err != nil { t.Errorf("failed parsing secureboot state: %v", err) } @@ -199,7 +202,7 @@ func TestSecureBootEventLogUbuntu(t *testing.T) { if err != nil { t.Fatalf("verifying event log: %v", err) } - _, err = extract.ParseSecurebootState(evts, extract.TPMRegisterConfig) + _, err = extract.ParseSecurebootState(evts, extract.TPMRegisterConfig, extract.Opts{}) if err != nil { t.Errorf("parsing sb state: %v", err) } @@ -218,8 +221,139 @@ func TestSecureBootEventLogFedora36(t *testing.T) { if err != nil { t.Fatalf("verifying event log: %v", err) } - _, err = extract.ParseSecurebootState(evts, extract.TPMRegisterConfig) + _, err = extract.ParseSecurebootState(evts, extract.TPMRegisterConfig, extract.Opts{}) if err != nil { t.Errorf("parsing sb state: %v", err) } } + +func TestEncodeUEFIVariableData(t *testing.T) { + data, err := os.ReadFile("../testdata/legacydata/coreos_36_shielded_vm_no_secure_boot_eventlog") + if err != nil { + t.Fatalf("reading test data: %v", err) + } + el, err := tcg.ParseEventLog(data, tcg.ParseOpts{}) + if err != nil { + t.Fatalf("parsing event log: %v", err) + } + evts := el.Events(register.HashSHA256) + if err != nil { + t.Fatalf("verifying event log: %v", err) + } + for _, evt := range evts { + if evt.Type != tcg.EFIVariableDriverConfig { + continue + } + v, err := tcg.ParseUEFIVariableData(bytes.NewReader(evt.RawData())) + if err != nil { + t.Fatal(err) + } + data, err := v.Encode() + if err != nil { + t.Fatal(err) + } + newv, err := tcg.ParseUEFIVariableData(bytes.NewReader(data)) + if err != nil { + t.Errorf("failed to parse after encoding: %v", err) + } + if diff := cmp.Diff(v, newv); diff != "" { + t.Errorf("Encode() produced different encodings: %v", diff) + } + } + +} + +func TestSecureBootAllowEmptySBVar(t *testing.T) { + data, err := os.ReadFile("../testdata/legacydata/coreos_36_shielded_vm_no_secure_boot_eventlog") + if err != nil { + t.Fatalf("reading test data: %v", err) + } + el, err := tcg.ParseEventLog(data, tcg.ParseOpts{}) + if err != nil { + t.Fatalf("parsing event log: %v", err) + } + evts := el.Events(register.HashSHA256) + if err != nil { + t.Fatalf("verifying event log: %v", err) + } + tests := []struct { + name string + newVar []byte + allowEmpty bool + wantErr bool + }{ + { + name: "emptyAllowed", + allowEmpty: true, + }, + { + name: "emptyNotAllowed", + wantErr: true, + }, + { + name: "1emptyAllowed", + newVar: []byte{1}, + allowEmpty: true, + }, + { + name: "1emptyNotAllowed", + newVar: []byte{1}, + }, + { + name: "0emptyAllowed", + newVar: []byte{0}, + allowEmpty: true, + }, + { + name: "0emptyNotAllowed", + newVar: []byte{0}, + }, + { + name: "len2emptyAllowed", + newVar: []byte{0, 1}, + allowEmpty: true, + wantErr: true, + }, + { + name: "len2emptyNotAllowed", + newVar: []byte{0, 1}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for i, evt := range evts { + if evt.Type != tcg.EFIVariableDriverConfig { + continue + } + v, err := tcg.ParseUEFIVariableData(bytes.NewReader(evt.RawData())) + if err != nil { + t.Fatal(err) + } + if v.VarName() == "SecureBoot" { + v.VariableData = tt.newVar + } + data, err := v.Encode() + if err != nil { + t.Fatal(err) + } + evt.Data = data + dgst := sha256.Sum256(evt.Data) + evt.Digest = dgst[:] + evts[i] = evt + } + opts := extract.Opts{} + if tt.allowEmpty { + opts.AllowEmptySBVar = true + } + _, err = extract.ParseSecurebootState(evts, extract.TPMRegisterConfig, opts) + if (err != nil) != tt.wantErr { + t.Errorf("ParseSecurebootState() = %v, wantErr %v", err, tt.wantErr) + + } + + }) + } + +} diff --git a/legacy/secureboot.go.bak b/legacy/secureboot.go.bak deleted file mode 100644 index 004dfb8..0000000 --- a/legacy/secureboot.go.bak +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2024 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 legacy - -import ( - "bytes" - "crypto/x509" - "errors" - "fmt" - - "github.com/google/go-eventlog/tcg" - "github.com/google/go-eventlog/tpmeventlog" -) - -// SecurebootState describes the secure boot status of a machine, as determined -// by processing its event log. -type SecurebootState struct { - Enabled bool - - // PlatformKeys enumerates keys which can sign a key exchange key. - PlatformKeys []x509.Certificate - // PlatformKeys enumerates key hashes which can sign a key exchange key. - PlatformKeyHashes [][]byte - - // ExchangeKeys enumerates keys which can sign a database of permitted or - // forbidden keys. - ExchangeKeys []x509.Certificate - // ExchangeKeyHashes enumerates key hashes which can sign a database or - // permitted or forbidden keys. - ExchangeKeyHashes [][]byte - - // PermittedKeys enumerates keys which may sign binaries to run. - PermittedKeys []x509.Certificate - // PermittedHashes enumerates hashes which permit binaries to run. - PermittedHashes [][]byte - - // ForbiddenKeys enumerates keys which must not permit a binary to run. - ForbiddenKeys []x509.Certificate - // ForbiddenKeys enumerates hashes which must not permit a binary to run. - ForbiddenHashes [][]byte - - // PreSeparatorAuthority describes the use of a secure-boot key to authorize - // the execution of a binary before the separator. - PreSeparatorAuthority []x509.Certificate - // PostSeparatorAuthority describes the use of a secure-boot key to authorize - // the execution of a binary after the separator. - PostSeparatorAuthority []x509.Certificate - - // DriverLoadSourceHints describes the origin of boot services drivers. - // This data is not tamper-proof and must only be used as a hint. - DriverLoadSourceHints []DriverLoadSource - - // DMAProtectionDisabled is true if the platform reports during boot that - // DMA protection is supported but disabled. - // - // See: https://docs.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-kernel-dma-protection - DMAProtectionDisabled bool -} - -// DriverLoadSource describes the logical origin of a boot services driver. -type DriverLoadSource uint8 - -const ( - UnknownSource DriverLoadSource = iota - PciMmioSource -) - -// ParseSecurebootState parses a series of events to determine the -// configuration of secure boot on a device. An error is returned if -// the state cannot be determined, or if the event log is structured -// in such a way that it may have been tampered post-execution of -// platform firmware. -func ParseSecurebootState(events []tpmeventlog.Event) (*SecurebootState, error) { - // This algorithm verifies the following: - // - All events in PCR 7 have event types which are expected in PCR 7. - // - All events are parsable according to their event type. - // - All events have digests values corresponding to their data/event type. - // - No unverifiable events were present. - // - All variables are specified before the separator and never duplicated. - // - The SecureBoot variable has a value of 0 or 1. - // - If SecureBoot was 1 (enabled), authority events were present indicating - // keys were used to perform verification. - // - If SecureBoot was 1 (enabled), platform + exchange + database keys - // were specified. - // - No UEFI debugger was attached. - - var ( - out SecurebootState - seenSeparator7 bool - seenSeparator2 bool - seenAuthority bool - seenVars = map[string]bool{} - driverSources [][]tcg.EFIDevicePathElement - ) - - for _, e := range events { - if e.Index != 7 && e.Index != 2 { - continue - } - - et, err := tcg.UntrustedParseEventType(uint32(e.Type)) - if err != nil { - return nil, fmt.Errorf("unrecognised event type: %v", err) - } - digestVerify := DigestEquals(&e, e.Data) - - switch e.Index { - case 7: - switch et { - case tcg.Separator: - if seenSeparator7 { - return nil, fmt.Errorf("duplicate separator at event %d", e.Sequence) - } - seenSeparator7 = true - if !bytes.Equal(e.Data, []byte{0, 0, 0, 0}) { - return nil, fmt.Errorf("invalid separator data at event %d: %v", e.Sequence, e.Data) - } - if digestVerify != nil { - return nil, fmt.Errorf("invalid separator digest at event %d: %v", e.Sequence, digestVerify) - } - - case tcg.EFIAction: - switch string(e.Data) { - case "UEFI Debug Mode": - return nil, errors.New("a UEFI debugger was present during boot") - case "DMA Protection Disabled": - if digestVerify != nil { - return nil, fmt.Errorf("invalid digest for EFI Action 'DMA Protection Disabled' on event %d: %v", e.Sequence, digestVerify) - } - out.DMAProtectionDisabled = true - default: - return nil, fmt.Errorf("event %d: unexpected EFI action event", e.Sequence) - } - - case tcg.EFIVariableDriverConfig: - v, err := tcg.ParseUEFIVariableData(bytes.NewReader(e.Data)) - if err != nil { - return nil, fmt.Errorf("failed parsing EFI variable at event %d: %v", e.Sequence, err) - } - if _, seenBefore := seenVars[v.VarName()]; seenBefore { - return nil, fmt.Errorf("duplicate EFI variable %q at event %d", v.VarName(), e.Sequence) - } - seenVars[v.VarName()] = true - if seenSeparator7 { - return nil, fmt.Errorf("event %d: variable %q specified after separator", e.Sequence, v.VarName()) - } - - if digestVerify != nil { - return nil, fmt.Errorf("invalid digest for variable %q on event %d: %v", v.VarName(), e.Sequence, digestVerify) - } - - switch v.VarName() { - case "SecureBoot": - if len(v.VariableData) != 1 { - return nil, fmt.Errorf("event %d: SecureBoot data len is %d, expected 1", e.Sequence, len(v.VariableData)) - } - out.Enabled = v.VariableData[0] == 1 - case "PK": - if out.PlatformKeys, out.PlatformKeyHashes, err = v.SignatureData(); err != nil { - return nil, fmt.Errorf("event %d: failed parsing platform keys: %v", e.Sequence, err) - } - case "KEK": - if out.ExchangeKeys, out.ExchangeKeyHashes, err = v.SignatureData(); err != nil { - return nil, fmt.Errorf("event %d: failed parsing key exchange keys: %v", e.Sequence, err) - } - case "db": - if out.PermittedKeys, out.PermittedHashes, err = v.SignatureData(); err != nil { - return nil, fmt.Errorf("event %d: failed parsing signature database: %v", e.Sequence, err) - } - case "dbx": - if out.ForbiddenKeys, out.ForbiddenHashes, err = v.SignatureData(); err != nil { - return nil, fmt.Errorf("event %d: failed parsing forbidden signature database: %v", e.Sequence, err) - } - } - - case tcg.EFIVariableAuthority: - v, err := tcg.ParseUEFIVariableData(bytes.NewReader(e.Data)) - if err != nil { - return nil, fmt.Errorf("failed parsing UEFI variable data: %v", err) - } - - a, err := tcg.ParseUEFIVariableAuthority(v) - if err != nil { - // Workaround for: https://github.com/google/go-attestation/issues/157 - if err == tcg.ErrSigMissingGUID { - // Versions of shim which do not carry - // https://github.com/rhboot/shim/commit/8a27a4809a6a2b40fb6a4049071bf96d6ad71b50 - // have an erroneous additional byte in the event, which breaks digest - // verification. If verification failed, we try removing the last byte. - if digestVerify != nil && len(e.Data) > 0 { - digestVerify = DigestEquals(&e, e.Data[:len(e.Data)-1]) - } - } else { - return nil, fmt.Errorf("failed parsing EFI variable authority at event %d: %v", e.Sequence, err) - } - } - seenAuthority = true - if digestVerify != nil { - return nil, fmt.Errorf("invalid digest for authority on event %d: %v", e.Sequence, digestVerify) - } - if !seenSeparator7 { - out.PreSeparatorAuthority = append(out.PreSeparatorAuthority, a.Certs...) - } else { - out.PostSeparatorAuthority = append(out.PostSeparatorAuthority, a.Certs...) - } - - default: - return nil, fmt.Errorf("unexpected event type in PCR7: %v", et) - } - - case 2: - switch et { - case tcg.Separator: - if seenSeparator2 { - return nil, fmt.Errorf("duplicate separator at event %d", e.Sequence) - } - seenSeparator2 = true - if !bytes.Equal(e.Data, []byte{0, 0, 0, 0}) { - return nil, fmt.Errorf("invalid separator data at event %d: %v", e.Sequence, e.Data) - } - if digestVerify != nil { - return nil, fmt.Errorf("invalid separator digest at event %d: %v", e.Sequence, digestVerify) - } - - case tcg.EFIBootServicesDriver: - if !seenSeparator2 { - imgLoad, err := tcg.ParseEFIImageLoad(bytes.NewReader(e.Data)) - if err != nil { - return nil, fmt.Errorf("failed parsing EFI image load at boot services driver event %d: %v", e.Sequence, err) - } - dp, err := imgLoad.DevicePath() - if err != nil { - return nil, fmt.Errorf("failed to parse device path for driver load event %d: %v", e.Sequence, err) - } - driverSources = append(driverSources, dp) - } - } - } - } - - // Compute driver source hints based on the EFI device path observed in - // EFI Boot-services driver-load events. -sourceLoop: - for _, source := range driverSources { - // We consider a driver to have originated from PCI-MMIO if any number - // of elements in the device path [1] were PCI devices, and are followed by - // an element representing a "relative offset range" read. - // In the wild, we have typically observed 4-tuple device paths for such - // devices: ACPI device -> PCI device -> PCI device -> relative offset. - // - // [1]: See section 9 of the UEFI specification v2.6 or greater. - var seenPCI bool - for _, e := range source { - // subtype 0x1 corresponds to a PCI device (See: 9.3.2.1) - if e.Type == tcg.HardwareDevice && e.Subtype == 0x1 { - seenPCI = true - } - // subtype 0x8 corresponds to "relative offset range" (See: 9.3.6.8) - if seenPCI && e.Type == tcg.MediaDevice && e.Subtype == 0x8 { - out.DriverLoadSourceHints = append(out.DriverLoadSourceHints, PciMmioSource) - continue sourceLoop - } - } - out.DriverLoadSourceHints = append(out.DriverLoadSourceHints, UnknownSource) - } - - if !out.Enabled { - return &out, nil - } - - if !seenAuthority { - return nil, errors.New("secure boot was enabled but no key was used") - } - if len(out.PlatformKeys) == 0 && len(out.PlatformKeyHashes) == 0 { - return nil, errors.New("secure boot was enabled but no platform keys were known") - } - if len(out.ExchangeKeys) == 0 && len(out.ExchangeKeyHashes) == 0 { - return nil, errors.New("secure boot was enabled but no key exchange keys were known") - } - if len(out.PermittedKeys) == 0 && len(out.PermittedHashes) == 0 { - return nil, errors.New("secure boot was enabled but no keys or hashes were permitted") - } - return &out, nil -} diff --git a/tcg/events.go b/tcg/events.go index a2ab16d..abe41e6 100644 --- a/tcg/events.go +++ b/tcg/events.go @@ -314,6 +314,33 @@ type UEFIVariableData struct { VariableData []byte // []int8 } +// Encode encodes the UEFIVariableData struct into raw bytes. +func (v *UEFIVariableData) Encode() ([]byte, error) { + buf := new(bytes.Buffer) + + v.Header.UnicodeNameLength = uint64(len(v.UnicodeName)) + v.Header.VariableDataLength = uint64(len(v.VariableData)) + + err := binary.Write(buf, binary.LittleEndian, v.Header) + if err != nil { + return nil, fmt.Errorf("error encoding header: %w", err) + } + + for _, nameChar := range v.UnicodeName { + err = binary.Write(buf, binary.LittleEndian, nameChar) + if err != nil { + return nil, fmt.Errorf("error encoding unicode name: %w", err) + } + } + + _, err = buf.Write(v.VariableData) + if err != nil { + return nil, fmt.Errorf("error encoding variable data: %w", err) + } + + return buf.Bytes(), nil +} + // ParseUEFIVariableData parses the data section of an event structured as // a UEFI variable. // diff --git a/testdata/eventlogs/ccel/gdc-tdx.bin b/testdata/eventlogs/ccel/gdc-tdx.bin new file mode 100644 index 0000000000000000000000000000000000000000..c5e544dbf77664dfa59d40e7f2f7b3860adfa851 GIT binary patch literal 65536 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 literal 0 HcmV?d00001