mirror of
https://github.com/google/go-attestation.git
synced 2025-02-20 17:02:47 +00:00
attest: add event log parsing logic
This PR adds event log parsing logic. It's main goal is to require validation at the same time as parsing, so structured events are always verified against a quote. This new API replaces the exisitng "verifier" package. It's not a goal of this PR to parse the event data. This will be a follow up, but since different users might want to parse different events based on the OS, this API lets users of this package implement custom event data parsing if they absolutely need to.
This commit is contained in:
parent
e6f0fc6196
commit
262599a8df
410
attest/eventlog.go
Normal file
410
attest/eventlog.go
Normal file
@ -0,0 +1,410 @@
|
||||
package attest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
// Ensure hashes are available.
|
||||
_ "crypto/sha256"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
"github.com/google/go-tpm/tpmutil"
|
||||
)
|
||||
|
||||
// TPM algorithms. See the TPM 2.0 specification section 6.3.
|
||||
//
|
||||
// https://trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf#page=42
|
||||
const (
|
||||
algSHA1 uint16 = 0x0004
|
||||
algSHA256 uint16 = 0x000B
|
||||
)
|
||||
|
||||
// EventType indicates what kind of data an event is reporting.
|
||||
type EventType uint32
|
||||
|
||||
// Event is a single event from a TCG event log. This reports descrete items such
|
||||
// as BIOs measurements or EFI states.
|
||||
type Event struct {
|
||||
// PCR index of the event.
|
||||
Index int
|
||||
// Type of the event.
|
||||
Type EventType
|
||||
|
||||
// Data of the event. For certain kinds of events, this must match the event
|
||||
// digest to be valid.
|
||||
Data []byte
|
||||
// Digest is the verified digest of the event data. While an event can have
|
||||
// multiple for different hash values, this is the one that was matched to the
|
||||
// PCR value.
|
||||
Digest []byte
|
||||
|
||||
// TODO(ericchiang): Provide examples or links for which event types must
|
||||
// match their data to their digest.
|
||||
}
|
||||
|
||||
// EventLog contains the data required to parse and validate an event log.
|
||||
type EventLog struct {
|
||||
// AIKPublic is the activated public key that has been proven to be under the
|
||||
// control of the TPM.
|
||||
AIKPublic crypto.PublicKey
|
||||
// AIKHash is the hash used to generate the quote.
|
||||
AIKHash crypto.Hash
|
||||
|
||||
// Quote is a signature over the values of a PCR.
|
||||
Quote *Quote
|
||||
// PCRs are the hash values in a given number of registers.
|
||||
PCRs []PCR
|
||||
// Nonce is additional data used to validate the quote signature. It's used
|
||||
// by the server to prevent clients from re-playing quotes.
|
||||
Nonce []byte
|
||||
|
||||
// MeasurementLog contains the raw event log data, which is matched against
|
||||
// the PCRs for validation.
|
||||
MeasurementLog []byte
|
||||
}
|
||||
|
||||
// Validate verifies the signature of the quote agains the public key, that the
|
||||
// quote matches the PCRs, parses the measurement log, and replays the PCRs.
|
||||
//
|
||||
// Events for PCRs not in the quote are dropped.
|
||||
func (e *EventLog) Validate() (events []Event, err error) {
|
||||
var pcrs []PCR
|
||||
switch e.Quote.Version {
|
||||
case TPMVersion12:
|
||||
pcrs, err = e.validate12Quote()
|
||||
case TPMVersion20:
|
||||
pcrs, err = e.validate20Quote()
|
||||
default:
|
||||
return nil, fmt.Errorf("quote used unknown tpm version 0x%x", e.Quote.Version)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid quote: %v", err)
|
||||
}
|
||||
rawEvents, err := parseEventLog(e.MeasurementLog)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing measurement log: %v", err)
|
||||
}
|
||||
events, err = replayEvents(rawEvents, pcrs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pcrs failed to replay: %v", err)
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
type rawAttestationData struct {
|
||||
Version [4]byte // This MUST be 1.1.0.0
|
||||
Fixed [4]byte // This SHALL always be the string ‘QUOT’
|
||||
Digest [20]byte // PCR Composite Hash
|
||||
Nonce [20]byte // Nonce Hash
|
||||
}
|
||||
|
||||
var (
|
||||
fixedQuote = [4]byte{'Q', 'U', 'O', 'T'}
|
||||
)
|
||||
|
||||
type rawPCRComposite struct {
|
||||
Size uint16 // always 3
|
||||
PCRMask [3]byte
|
||||
Values tpmutil.U32Bytes
|
||||
}
|
||||
|
||||
func (e *EventLog) validate12Quote() (pcrs []PCR, err error) {
|
||||
pub, ok := e.AIKPublic.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported public key type: %T", e.AIKPublic)
|
||||
}
|
||||
quote := sha1.Sum(e.Quote.Quote)
|
||||
if err := rsa.VerifyPKCS1v15(pub, crypto.SHA1, quote[:], e.Quote.Signature); err != nil {
|
||||
return nil, fmt.Errorf("invalid quote signature: %v", err)
|
||||
}
|
||||
|
||||
var att rawAttestationData
|
||||
if _, err := tpmutil.Unpack(e.Quote.Quote, &att); err != nil {
|
||||
return nil, fmt.Errorf("parsing quote: %v", err)
|
||||
}
|
||||
// TODO(ericchiang): validate Version field.
|
||||
if att.Nonce != sha1.Sum(e.Nonce) {
|
||||
return nil, fmt.Errorf("invalid nonce")
|
||||
}
|
||||
if att.Fixed != fixedQuote {
|
||||
return nil, fmt.Errorf("quote wasn't a QUOT object: %x", att.Fixed)
|
||||
}
|
||||
|
||||
// See 5.4.1 Creating a PCR composite hash
|
||||
sort.Slice(e.PCRs, func(i, j int) bool { return e.PCRs[i].Index < e.PCRs[j].Index })
|
||||
var (
|
||||
pcrMask [3]byte // bitmap indicating which PCRs are active
|
||||
values []byte // appended values of all PCRs
|
||||
)
|
||||
for _, pcr := range e.PCRs {
|
||||
if pcr.Index < 0 || pcr.Index >= 24 {
|
||||
return nil, fmt.Errorf("invalid PCR index: %d", pcr.Index)
|
||||
}
|
||||
pcrMask[pcr.Index/8] |= 1 << uint(pcr.Index%8)
|
||||
values = append(values, pcr.Digest...)
|
||||
}
|
||||
composite, err := tpmutil.Pack(rawPCRComposite{3, pcrMask, values})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshaling PCRss: %v", err)
|
||||
}
|
||||
if att.Digest != sha1.Sum(composite) {
|
||||
return nil, fmt.Errorf("PCRs passed didn't match quote: %v", err)
|
||||
}
|
||||
return e.PCRs, nil
|
||||
}
|
||||
|
||||
func (e *EventLog) validate20Quote() (pcrs []PCR, err error) {
|
||||
sig, err := tpm2.DecodeSignature(bytes.NewBuffer(e.Quote.Signature))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse quote signature: %v", err)
|
||||
}
|
||||
|
||||
sigHash := e.AIKHash.New()
|
||||
sigHash.Write(e.Quote.Quote)
|
||||
|
||||
switch pub := e.AIKPublic.(type) {
|
||||
case *rsa.PublicKey:
|
||||
if sig.RSA == nil {
|
||||
return nil, fmt.Errorf("rsa public key provided for ec signature")
|
||||
}
|
||||
sigBytes := []byte(sig.RSA.Signature)
|
||||
if err := rsa.VerifyPKCS1v15(pub, e.AIKHash, sigHash.Sum(nil), sigBytes); err != nil {
|
||||
return nil, fmt.Errorf("invalid quote signature: %v", err)
|
||||
}
|
||||
default:
|
||||
// TODO(ericchiang): support ecdsa
|
||||
return nil, fmt.Errorf("unsupported public key type %T", pub)
|
||||
}
|
||||
|
||||
att, err := tpm2.DecodeAttestationData(e.Quote.Quote)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing quote signature: %v", err)
|
||||
}
|
||||
if att.Type != tpm2.TagAttestQuote {
|
||||
return nil, fmt.Errorf("attestation isn't a quote, tag of type 0x%x", att.Type)
|
||||
}
|
||||
if !bytes.Equal([]byte(att.ExtraData), e.Nonce) {
|
||||
return nil, fmt.Errorf("nonce didn't match: %v", err)
|
||||
}
|
||||
|
||||
pcrByIndex := map[int][]byte{}
|
||||
for _, pcr := range e.PCRs {
|
||||
pcrByIndex[pcr.Index] = pcr.Digest
|
||||
}
|
||||
|
||||
n := len(att.AttestedQuoteInfo.PCRDigest)
|
||||
hash, ok := hashBySize[n]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("quote used unsupported hash algorithm length: %d", n)
|
||||
}
|
||||
var validatedPCRs []PCR
|
||||
h := hash.New()
|
||||
for _, index := range att.AttestedQuoteInfo.PCRSelection.PCRs {
|
||||
digest, ok := pcrByIndex[index]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("quote was over PCR %d which wasn't provided", index)
|
||||
}
|
||||
if len(digest) != hash.Size() {
|
||||
return nil, fmt.Errorf("mismatch pcr and quote hash, pcr hash length=%d, quote hash length=%d", len(digest), hash.Size())
|
||||
}
|
||||
h.Write(digest)
|
||||
validatedPCRs = append(validatedPCRs, PCR{Index: index, Digest: digest})
|
||||
}
|
||||
|
||||
if !bytes.Equal(h.Sum(nil), att.AttestedQuoteInfo.PCRDigest) {
|
||||
return nil, fmt.Errorf("quote digest didn't match pcrs provided")
|
||||
}
|
||||
return validatedPCRs, nil
|
||||
}
|
||||
|
||||
var hashBySize = map[int]crypto.Hash{
|
||||
crypto.SHA1.Size(): crypto.SHA1,
|
||||
crypto.SHA256.Size(): crypto.SHA256,
|
||||
}
|
||||
|
||||
func extend(pcr, replay []byte, e rawEvent) ([]byte, Event, error) {
|
||||
h, ok := hashBySize[len(pcr)]
|
||||
if !ok {
|
||||
return nil, Event{}, fmt.Errorf("pcr %d was not a known hash size: %d", e.index, len(pcr))
|
||||
}
|
||||
for _, digest := range e.digests {
|
||||
if len(digest) != len(pcr) {
|
||||
continue
|
||||
}
|
||||
hash := h.New()
|
||||
if len(replay) != 0 {
|
||||
hash.Write(replay)
|
||||
} else {
|
||||
b := make([]byte, h.Size())
|
||||
hash.Write(b)
|
||||
}
|
||||
hash.Write(digest)
|
||||
return hash.Sum(nil), Event{e.index, e.typ, e.data, digest}, nil
|
||||
}
|
||||
return nil, Event{}, fmt.Errorf("no event digest matches pcr length: %d", len(pcr))
|
||||
}
|
||||
|
||||
func replayEvents(rawEvents []rawEvent, pcrs []PCR) ([]Event, error) {
|
||||
events := []Event{}
|
||||
replay := map[int][]byte{}
|
||||
pcrByIndex := map[int][]byte{}
|
||||
for _, pcr := range pcrs {
|
||||
pcrByIndex[pcr.Index] = pcr.Digest
|
||||
}
|
||||
|
||||
for i, e := range rawEvents {
|
||||
pcrValue, ok := pcrByIndex[e.index]
|
||||
if !ok {
|
||||
// Ignore events for PCRs that weren't included in the quote.
|
||||
continue
|
||||
}
|
||||
replayValue, event, err := extend(pcrValue, replay[e.index], e)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("replaying event %d: %v", i, err)
|
||||
}
|
||||
replay[e.index] = replayValue
|
||||
events = append(events, event)
|
||||
}
|
||||
|
||||
var invalidReplays []int
|
||||
for i, value := range replay {
|
||||
if !bytes.Equal(value, pcrByIndex[i]) {
|
||||
invalidReplays = append(invalidReplays, i)
|
||||
}
|
||||
}
|
||||
if len(invalidReplays) > 0 {
|
||||
return nil, fmt.Errorf("the following registers failed to replay: %d", invalidReplays)
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// EV_NO_ACTION is a special event type that indicates information to the parser
|
||||
// instead of holding a measurement. For TPM 2.0, this event type is used to signal
|
||||
// switching from SHA1 format to a variable length digest.
|
||||
//
|
||||
// https://trustedcomputinggroup.org/wp-content/uploads/TCG_PCClientSpecPlat_TPM_2p0_1p04_pub.pdf#page=110
|
||||
const eventTypeNoAction = 0x03
|
||||
|
||||
func parseEventLog(b []byte) ([]rawEvent, error) {
|
||||
r := bytes.NewBuffer(b)
|
||||
parseFn := parseRawEvent
|
||||
e, err := parseFn(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse first event: %v", err)
|
||||
}
|
||||
var events []rawEvent
|
||||
if e.typ == eventTypeNoAction {
|
||||
// Switch to parsing crypto agile events. Don't include this in the
|
||||
// replayed events since it's intentionally switching from SHA1 to
|
||||
// SHA256 and will fail to extend a SHA256 PCR value.
|
||||
//
|
||||
// NOTE(ericchiang): to be strict, we could parse the event data as a
|
||||
// TCG_EfiSpecIDEventStruct and validate the algorithms. But for now,
|
||||
// assume this indicates a switch from SHA1 format to SHA1/SHA256.
|
||||
//
|
||||
// https://trustedcomputinggroup.org/wp-content/uploads/EFI-Protocol-Specification-rev13-160330final.pdf#page=18
|
||||
parseFn = parseRawEvent2
|
||||
} else {
|
||||
events = append(events, e)
|
||||
}
|
||||
for r.Len() != 0 {
|
||||
e, err := parseFn(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, e)
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
type rawEvent struct {
|
||||
index int
|
||||
typ EventType
|
||||
data []byte
|
||||
digests [][]byte
|
||||
}
|
||||
|
||||
// TPM 1.2 event log format. See "5.1 SHA1 Event Log Entry Format"
|
||||
// https://trustedcomputinggroup.org/wp-content/uploads/EFI-Protocol-Specification-rev13-160330final.pdf#page=15
|
||||
type rawEventHeader struct {
|
||||
PCRIndex uint32
|
||||
Type uint32
|
||||
Digest [20]byte
|
||||
EventSize uint32
|
||||
}
|
||||
|
||||
func parseRawEvent(r io.Reader) (event rawEvent, err error) {
|
||||
var h rawEventHeader
|
||||
if err = binary.Read(r, binary.LittleEndian, &h); err != nil {
|
||||
return event, err
|
||||
}
|
||||
data := make([]byte, int(h.EventSize))
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
return event, err
|
||||
}
|
||||
return rawEvent{
|
||||
typ: EventType(h.Type),
|
||||
data: data,
|
||||
index: int(h.PCRIndex),
|
||||
digests: [][]byte{h.Digest[:]},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TPM 2.0 event log format. See "5.2 Crypto Agile Log Entry Format"
|
||||
// https://trustedcomputinggroup.org/wp-content/uploads/EFI-Protocol-Specification-rev13-160330final.pdf#page=15
|
||||
type rawEvent2Header struct {
|
||||
PCRIndex uint32
|
||||
Type uint32
|
||||
}
|
||||
|
||||
func parseRawEvent2(r io.Reader) (event rawEvent, err error) {
|
||||
var h rawEvent2Header
|
||||
if err = binary.Read(r, binary.LittleEndian, &h); err != nil {
|
||||
return event, err
|
||||
}
|
||||
event.typ = EventType(h.Type)
|
||||
event.index = int(h.PCRIndex)
|
||||
|
||||
// parse the event digests
|
||||
var numDigests uint32
|
||||
if err := binary.Read(r, binary.LittleEndian, &numDigests); err != nil {
|
||||
return event, err
|
||||
}
|
||||
for i := 0; i < int(numDigests); i++ {
|
||||
var algID uint16
|
||||
if err := binary.Read(r, binary.LittleEndian, &algID); err != nil {
|
||||
return event, err
|
||||
}
|
||||
var digest []byte
|
||||
switch algID {
|
||||
case algSHA1:
|
||||
digest = make([]byte, crypto.SHA1.Size())
|
||||
case algSHA256:
|
||||
digest = make([]byte, crypto.SHA256.Size())
|
||||
default:
|
||||
// ignore signatures that aren't SHA1 or SHA256
|
||||
continue
|
||||
}
|
||||
if _, err := io.ReadFull(r, digest); err != nil {
|
||||
return event, err
|
||||
}
|
||||
event.digests = append(event.digests, digest)
|
||||
}
|
||||
|
||||
// parse event data
|
||||
var eventSize uint32
|
||||
if err = binary.Read(r, binary.LittleEndian, &eventSize); err != nil {
|
||||
return event, err
|
||||
}
|
||||
event.data = make([]byte, int(eventSize))
|
||||
if _, err := io.ReadFull(r, event.data); err != nil {
|
||||
return event, err
|
||||
}
|
||||
return event, err
|
||||
}
|
94
attest/eventlog_test.go
Normal file
94
attest/eventlog_test.go
Normal file
@ -0,0 +1,94 @@
|
||||
package attest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-tpm/tpm2"
|
||||
)
|
||||
|
||||
// Dump describes the layout of serialized information from the dump command.
|
||||
type Dump struct {
|
||||
Static struct {
|
||||
TPMVersion TPMVersion
|
||||
EKPem []byte
|
||||
}
|
||||
|
||||
AIK AttestationParameters
|
||||
|
||||
Quote struct {
|
||||
Nonce []byte
|
||||
Alg HashAlg
|
||||
Quote []byte
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
Log struct {
|
||||
PCRs []PCR
|
||||
PCRAlg tpm2.Algorithm
|
||||
Raw []byte // The measured boot log in binary form.
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEventLogWindows(t *testing.T) {
|
||||
testParseEventLog(t, "testdata/windows_gcp_shielded_vm.json")
|
||||
}
|
||||
|
||||
func TestParseEventLogLinux(t *testing.T) {
|
||||
testParseEventLog(t, "testdata/linux_tpm12.json")
|
||||
}
|
||||
|
||||
func testParseEventLog(t *testing.T, testdata string) {
|
||||
data, err := ioutil.ReadFile(testdata)
|
||||
if err != nil {
|
||||
t.Fatalf("reading test data: %v", err)
|
||||
}
|
||||
var dump Dump
|
||||
if err := json.Unmarshal(data, &dump); err != nil {
|
||||
t.Fatalf("parsing test data: %v", err)
|
||||
}
|
||||
if _, err := parseEventLog(dump.Log.Raw); err != nil {
|
||||
t.Fatalf("parsing event log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventLogLinux(t *testing.T) {
|
||||
testEventLog(t, "testdata/linux_tpm12.json")
|
||||
}
|
||||
|
||||
func TestEventLog(t *testing.T) {
|
||||
testEventLog(t, "testdata/windows_gcp_shielded_vm.json")
|
||||
}
|
||||
|
||||
func testEventLog(t *testing.T, testdata string) {
|
||||
data, err := ioutil.ReadFile(testdata)
|
||||
if err != nil {
|
||||
t.Fatalf("reading test data: %v", err)
|
||||
}
|
||||
var dump Dump
|
||||
if err := json.Unmarshal(data, &dump); err != nil {
|
||||
t.Fatalf("parsing test data: %v", err)
|
||||
}
|
||||
|
||||
aik, err := ParseAIKPublic(dump.Static.TPMVersion, dump.AIK.Public)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing AIK: %v", err)
|
||||
}
|
||||
|
||||
el := EventLog{
|
||||
AIKPublic: aik.Public,
|
||||
AIKHash: aik.Hash,
|
||||
Quote: &Quote{
|
||||
Version: dump.Static.TPMVersion,
|
||||
Quote: dump.Quote.Quote,
|
||||
Signature: dump.Quote.Signature,
|
||||
},
|
||||
Nonce: dump.Quote.Nonce,
|
||||
PCRs: dump.Log.PCRs,
|
||||
MeasurementLog: dump.Log.Raw,
|
||||
}
|
||||
if _, err := el.Validate(); err != nil {
|
||||
t.Fatalf("validating event log: %v", err)
|
||||
}
|
||||
}
|
1
attest/testdata/linux_tpm12.json
vendored
Normal file
1
attest/testdata/linux_tpm12.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
attest/testdata/windows_gcp_shielded_vm.json
vendored
Normal file
1
attest/testdata/windows_gcp_shielded_vm.json
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user