-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
attest: add event log parsing logic (WIP)
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. TODO: - [ ] Add tests once #62 is merged
- Loading branch information
1 parent
f3f0803
commit 04aac17
Showing
2 changed files
with
400 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,399 @@ | ||
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 | ||
// MeasurementLog contains the raw event log data, which is matched against | ||
// the PCRs for validation. | ||
MeasurementLog []byte | ||
|
||
// 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 | ||
} | ||
|
||
// 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") | ||
} | ||
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 | ||
Nonce [20]byte | ||
} | ||
|
||
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 pcrs: %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 | ||
} | ||
|
||
var validatedPCRs []PCR | ||
h := e.AIKHash.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) | ||
} | ||
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 replay != nil { | ||
hash.Write(replay) | ||
} | ||
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 | ||
} |
Oops, something went wrong.