Skip to content

Commit

Permalink
Add more CosTlv ops related to CEL, able to parse a COS CEL to a Mach…
Browse files Browse the repository at this point in the history
…ineState

Add digest verification when replaying/parsing the CEL
  • Loading branch information
jkl73 committed Dec 22, 2021
1 parent 7f40f44 commit 9c8faa2
Show file tree
Hide file tree
Showing 12 changed files with 513 additions and 84 deletions.
48 changes: 34 additions & 14 deletions cel/canonical_eventlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,25 @@ type TLV struct {
}

// MarshalBinary marshals a TLV to a byte slice.
func (tlv TLV) MarshalBinary() (data []byte, err error) {
buf := make([]byte, len(tlv.Value)+tlvTypeFieldLength+tlvLengthFieldLength)
func (t TLV) MarshalBinary() (data []byte, err error) {
buf := make([]byte, len(t.Value)+tlvTypeFieldLength+tlvLengthFieldLength)

buf[0] = tlv.Type
binary.BigEndian.PutUint32(buf[tlvTypeFieldLength:], uint32(len(tlv.Value)))
copy(buf[tlvTypeFieldLength+tlvLengthFieldLength:], tlv.Value)
buf[0] = t.Type
binary.BigEndian.PutUint32(buf[tlvTypeFieldLength:], uint32(len(t.Value)))
copy(buf[tlvTypeFieldLength+tlvLengthFieldLength:], t.Value)

return buf, nil
}

// UnmarshalBinary unmarshal a byte slice to a TLV.
func (tlv *TLV) UnmarshalBinary(data []byte) error {
func (t *TLV) UnmarshalBinary(data []byte) error {
valueLength := binary.BigEndian.Uint32(data[tlvTypeFieldLength : tlvTypeFieldLength+tlvLengthFieldLength])

if valueLength != uint32(len(data[tlvTypeFieldLength+tlvLengthFieldLength:])) {
return fmt.Errorf("TLV Length doesn't match the size of its Value")
}
tlv.Type = data[0]
tlv.Value = data[tlvTypeFieldLength+tlvLengthFieldLength:]
t.Type = data[0]
t.Value = data[tlvTypeFieldLength+tlvLengthFieldLength:]

return nil
}
Expand Down Expand Up @@ -109,7 +109,7 @@ type Record struct {
// Content is a interface for the content in CELR.
type Content interface {
GenerateDigest(crypto.Hash) ([]byte, error)
GetTLV() TLV
GetTLV() (TLV, error)
}

// CEL represents a Canonical Eventlog, which contains a list of Records.
Expand Down Expand Up @@ -140,11 +140,16 @@ func (c *CEL) AppendEvent(tpm io.ReadWriteCloser, pcr int, hashAlgos []crypto.Ha
}
}

eventTlv, err := event.GetTLV()
if err != nil {
return err
}

celr := Record{
RecNum: uint64(len(c.Records)),
PCR: uint8(pcr),
Digests: digestsMap,
Content: event.GetTLV(),
Content: eventTlv,
}

c.Records = append(c.Records, celr)
Expand Down Expand Up @@ -349,10 +354,10 @@ func DecodeToCELR(buf *bytes.Buffer) (r Record, err error) {
return r, nil
}

// replay takes the digests from a Canonical Event Log and carries out the
// Replay takes the digests from a Canonical Event Log and carries out the
// extend sequence for each PCR in the log. It then compares the final digests
// against a bank of PCR values to see if they match.
func (c *CEL) replay(bank *pb.PCRs) error {
func (c *CEL) Replay(bank *pb.PCRs) error {
tpm2Alg := tpm2.Algorithm(bank.GetHash())
cryptoHash, err := tpm2Alg.Hash()
if err != nil {
Expand All @@ -367,7 +372,7 @@ func (c *CEL) replay(bank *pb.PCRs) error {
digestsMap := record.Digests
digest, ok := digestsMap[cryptoHash]
if !ok {
return fmt.Errorf("the CEL record did not contain a %v digest", tpm2Alg)
return fmt.Errorf("the CEL record did not contain a %v digest", cryptoHash)
}
hasher.Write(replayed[record.PCR])
hasher.Write(digest)
Expand All @@ -389,5 +394,20 @@ func (c *CEL) replay(bank *pb.PCRs) error {
return nil
}

return fmt.Errorf("CEL replay failed for these PCRs in bank %v: %v", tpm2Alg, failedReplayPcrs)
return fmt.Errorf("CEL replay failed for these PCRs in bank %v: %v", cryptoHash, failedReplayPcrs)
}

// VerifyDigests checks the digest generated by the given record's content to make sure they are equal to
// the digests in the digestMap.
func VerifyDigests(c Content, digestMap map[crypto.Hash][]byte) error {
for hash, digest := range digestMap {
generatedDigest, err := c.GenerateDigest(hash)
if err != nil {
return err
}
if !bytes.Equal(generatedDigest, digest) {
return fmt.Errorf("CEL record content digest verification failed for %s", hash)
}
}
return nil
}
31 changes: 10 additions & 21 deletions cel/canonical_eventlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ func TestCELEncodingDecoding(t *testing.T) {
hashAlgoList := []crypto.Hash{crypto.SHA256, crypto.SHA1, crypto.SHA512}
cel := &CEL{}

someEvent := make([]byte, 10)
cosEvent := CosTlv{someEvent}
cosEvent := CosTlv{ImageDigestType, []byte("sha256:781d8dfdd92118436bd914442c8339e653b83f6bf3c1a7a98efcfb7c4fed7483")}
appendOrFatal(t, cel, tpm, test.DebugPCR, hashAlgoList, cosEvent)

cosEvent2 := CosTlv{someEvent}
cosEvent2 := CosTlv{ImageRefType, []byte("docker.io/bazel/experimental/test:latest")}
appendOrFatal(t, cel, tpm, test.ApplicationPCR, hashAlgoList, cosEvent2)

var buf bytes.Buffer
Expand All @@ -52,13 +51,6 @@ func TestCELEncodingDecoding(t *testing.T) {
t.Errorf("pcr value mismatch")
}

digestsMap := decodedcel.Records[0].Digests
if len(digestsMap[crypto.SHA256]) != 32 {
t.Errorf("SHA256 digest length doesn't match")
}
if len(digestsMap[crypto.SHA1]) != 20 {
t.Errorf("SHA1 digest length doesn't match")
}
if !reflect.DeepEqual(decodedcel.Records, cel.Records) {
t.Errorf("decoded CEL doesn't equal to the original one")
}
Expand All @@ -71,12 +63,10 @@ func TestCELMeasureAndReplay(t *testing.T) {
cel := &CEL{}
measuredHashes := []crypto.Hash{crypto.SHA256, crypto.SHA1, crypto.SHA512}

someEvent := make([]byte, 10)
cosEvent := CosTlv{someEvent}
cosEvent := CosTlv{ImageRefType, []byte("docker.io/bazel/experimental/test:latest")}
someEvent2 := make([]byte, 10)

rand.Read(someEvent2)
cosEvent2 := CosTlv{someEvent2}
cosEvent2 := CosTlv{ImageDigestType, someEvent2}
appendOrFatal(t, cel, tpm, test.DebugPCR, measuredHashes, cosEvent)
appendOrFatal(t, cel, tpm, test.DebugPCR, measuredHashes, cosEvent2)

Expand All @@ -93,19 +83,18 @@ func TestCELMeasureAndReplay(t *testing.T) {
/*shouldSucceed=*/ true)
}

func TestCELReplayFailTampered(t *testing.T) {
func TestCELReplayFailTamperedDigest(t *testing.T) {
tpm := test.GetTPM(t)
defer client.CheckedClose(t, tpm)

cel := &CEL{}
measuredHashes := []crypto.Hash{crypto.SHA256, crypto.SHA1, crypto.SHA512}

someEvent := make([]byte, 10)
cosEvent := CosTlv{someEvent}
cosEvent := CosTlv{ImageRefType, []byte("docker.io/bazel/experimental/test:latest")}
someEvent2 := make([]byte, 10)

rand.Read(someEvent2)
cosEvent2 := CosTlv{someEvent2}
cosEvent2 := CosTlv{ImageDigestType, someEvent2}
appendOrFatal(t, cel, tpm, test.DebugPCR, measuredHashes, cosEvent)
appendOrFatal(t, cel, tpm, test.DebugPCR, measuredHashes, cosEvent2)

Expand Down Expand Up @@ -144,8 +133,8 @@ func TestCELReplayFailMissingPCRsInBank(t *testing.T) {
someEvent := make([]byte, 10)
someEvent2 := make([]byte, 10)
rand.Read(someEvent2)
appendOrFatal(t, cel, tpm, test.DebugPCR, measuredHashes, CosTlv{someEvent})
appendOrFatal(t, cel, tpm, test.ApplicationPCR, measuredHashes, CosTlv{someEvent2})
appendOrFatal(t, cel, tpm, test.DebugPCR, measuredHashes, CosTlv{ImageRefType, someEvent})
appendOrFatal(t, cel, tpm, test.ApplicationPCR, measuredHashes, CosTlv{ImageDigestType, someEvent2})
replay(t, cel, tpm, measuredHashes,
[]int{test.DebugPCR},
/*shouldSucceed=*/ false)
Expand All @@ -170,7 +159,7 @@ func replay(t *testing.T, cel *CEL, tpm io.ReadWriteCloser, measuredHashes []cry
for index, val := range pcrMap {
pbPcr.Pcrs[uint32(index)] = val
}
if err := cel.replay(pbPcr); shouldSucceed && err != nil {
if err := cel.Replay(pbPcr); shouldSucceed && err != nil {
t.Errorf("failed to replay CEL on %v bank: %v",
pb.HashAlgo_name[int32(pbPcr.Hash)], err)
}
Expand Down
60 changes: 51 additions & 9 deletions cel/cos_tlv.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,54 @@
package cel

import "crypto"
import (
"crypto"
"fmt"
)

const (
// CosTypeValue indicate the CELR event is a COS content
// CosEventType indicate the CELR event is a COS content
// TODO: the value needs to be reserved in the CEL spec
CosTypeValue uint8 = 80
CosEventType uint8 = 80
)

// CosType represent a COS content type in a CEL record content.
type CosType uint8

// Type for COS nested events
const (
ImageRefType CosType = iota
ImageDigestType
RestartPolicyType
)

// CosTlv is a specific event type created for the COS (Google Container-Optimized OS),
// used as a CEL content.
type CosTlv struct {
data []byte // nested TLV
EventType CosType
EventContent []byte
}

// GetTLV returns the TLV representation of the COS TLV.
func (c CosTlv) GetTLV() TLV {
return TLV{
Type: CosTypeValue,
Value: c.data,
func (c CosTlv) GetTLV() (TLV, error) {
data, err := TLV{uint8(c.EventType), c.EventContent}.MarshalBinary()
if err != nil {
return TLV{}, err
}

return TLV{
Type: CosEventType,
Value: data,
}, nil
}

// GenerateDigest generates the digest for the given COS TLV. The whole TLV struct will
// be marshaled to bytes and feed into the hash algo.
func (c CosTlv) GenerateDigest(hashAlgo crypto.Hash) ([]byte, error) {
contentTLV := c.GetTLV()
contentTLV, err := c.GetTLV()
if err != nil {
return nil, err
}

b, err := contentTLV.MarshalBinary()
if err != nil {
return nil, err
Expand All @@ -37,3 +60,22 @@ func (c CosTlv) GenerateDigest(hashAlgo crypto.Hash) ([]byte, error) {
}
return hash.Sum(nil), nil
}

// ParseToCosTlv constructs a CosTlv from a TLV. It will check for the correct COS event
// type, and unmarshal the nested event.
func (t TLV) ParseToCosTlv() (CosTlv, error) {
if !t.IsCosTlv() {
return CosTlv{}, fmt.Errorf("TLV type %v is not a COS event", t.Type)
}
nestedEvent := TLV{}
err := nestedEvent.UnmarshalBinary(t.Value)
if err != nil {
return CosTlv{}, err
}
return CosTlv{CosType(nestedEvent.Type), nestedEvent.Value}, nil
}

// IsCosTlv check whether a TLV is a COS TLV by its Type value.
func (t TLV) IsCosTlv() bool {
return t.Type == CosEventType
}
62 changes: 62 additions & 0 deletions cel/cos_tlv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cel

import (
"bytes"
"crypto"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm-tools/internal/test"
pb "github.com/google/go-tpm-tools/proto/attest"
)

func TestCosEventlog(t *testing.T) {
tpm := test.GetTPM(t)
defer client.CheckedClose(t, tpm)

hashAlgoList := []crypto.Hash{crypto.SHA256, crypto.SHA1, crypto.SHA512}
cel := &CEL{}

testEvents := []struct {
cosNestedEventType CosType
pcr int
eventPayload []byte
}{
{ImageRefType, test.DebugPCR, []byte("docker.io/bazel/experimental/test:latest")},
{ImageDigestType, test.DebugPCR, []byte("sha256:781d8dfdd92118436bd914442c8339e653b83f6bf3c1a7a98efcfb7c4fed7483")},
{RestartPolicyType, test.DebugPCR, []byte(pb.RestartPolicy_NEVER.String())},
}

for _, testEvent := range testEvents {
cos := CosTlv{testEvent.cosNestedEventType, testEvent.eventPayload}
if err := cel.AppendEvent(tpm, testEvent.pcr, hashAlgoList, cos); err != nil {
t.Fatal(err.Error())
}
}

var buf bytes.Buffer
if err := cel.EncodeCEL(&buf); err != nil {
t.Fatal(err)
}
decodedcel, err := DecodeToCEL(&buf)
if err != nil {
t.Fatal(err)
}

if len(decodedcel.Records) != 3 {
t.Errorf("should have three records")
}

for i, testEvent := range testEvents {
extractedCos, err := decodedcel.Records[i].Content.ParseToCosTlv()
if err != nil {
t.Fatal(err)
}

want := CosTlv{testEvent.cosNestedEventType, testEvent.eventPayload}
if !cmp.Equal(extractedCos, want) {
t.Errorf("decoded COS TLV got %+v, want %+v", extractedCos, want)
}
}
}
17 changes: 6 additions & 11 deletions client/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,21 +123,16 @@ func ExampleKey_Attest() {
log.Fatalf("failed to attest: %v", err)
}

// On verifier, verify the quote(s) against a stored public key/AK
// certificate's public part and the nonce passed.
// TODO: establish trust in the AK (typically via an AK certificate signed
// by the manufacturer).
for i, quote := range attestation.Quotes {
if err := internal.VerifyQuote(quote, attestation.AkPub, nonce); err != nil {
// TODO: handle verify error.
log.Fatalf("failed to verify quote with index %v in attestation", i)
}
}

// On verifier, replay event log.
// On verifier, verify the Attestation message. This:
// - checks the quote(s) against a stored public key/AK
// certificate's public part and the expected nonce.
// - replays the event log against the quoted PCRs
// - extracts events into a MachineState message.
// TODO: decide which hash algorithm to use in the quotes. SHA1 is
// typically undesirable but is the only event log option on some distros.
_, err = server.ParseMachineState(attestation.EventLog, attestation.Quotes[0].Pcrs)
_, err = server.VerifyAttestation(attestation, server.VerifyOpts{Nonce: nonce, TrustedAKs: []crypto.PublicKey{ak.PublicKey()}})
if err != nil {
// TODO: handle parsing or replay error.
log.Fatalf("failed to read PCRs: %v", err)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ go 1.16
require (
github.com/google/certificate-transparency-go v1.1.1
github.com/google/go-attestation v0.3.2
github.com/google/go-cmp v0.5.5
github.com/google/go-tpm v0.3.2
github.com/spf13/cobra v1.1.3
google.golang.org/protobuf v1.27.1
Expand Down
Loading

0 comments on commit 9c8faa2

Please sign in to comment.